From b62a10c8d4100c8f1f97b1426eec2b3312666487 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 15 Apr 2026 11:45:56 -0400 Subject: [PATCH 1/4] remove users store plumbing --- shared/constants/init/shared.tsx | 33 -------------------------------- shared/stores/chat.tsx | 17 +++++----------- shared/stores/team-building.tsx | 13 +++---------- shared/stores/teams.tsx | 7 ++----- shared/stores/tracker.tsx | 25 ++++++++++-------------- 5 files changed, 20 insertions(+), 75 deletions(-) diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 25c6ed206853..f901f83d679f 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -51,8 +51,6 @@ import {usePushState} from '@/stores/push' import {useSettingsContactsState} from '@/stores/settings-contacts' import {useState as useRecoverPasswordState} from '@/stores/recover-password' import {useTeamsState} from '@/stores/teams' -import {useTrackerState} from '@/stores/tracker' -import {useUsersState} from '@/stores/users' import {useRouterState} from '@/stores/router' import * as Util from '@/constants/router' import {setConvoDefer} from '@/stores/convostate' @@ -133,12 +131,6 @@ export const initTeamBuildingCallbacks = () => { onShowUserProfile: (username: string) => { navToProfile(username) }, - onUsersGetBlockState: (usernames: ReadonlyArray) => { - useUsersState.getState().dispatch.getBlockState(usernames) - }, - onUsersUpdates: (infos: ReadonlyArray<{name: string; info: Partial}>) => { - useUsersState.getState().dispatch.updates(infos) - }, } const namespaces: Array = ['chat', 'crypto', 'teams', 'people'] @@ -200,18 +192,12 @@ export const initChat2Callbacks = () => { onGetTeamsTeamIDToMembers: (teamID: T.Teams.TeamID) => { return storeRegistry.getState('teams').teamIDToMembers.get(teamID) }, - onGetUsersInfoMap: () => { - return storeRegistry.getState('users').infoMap - }, onTeamsGetMembers: async (teamID: T.Teams.TeamID) => { return storeRegistry.getState('teams').dispatch.getMembers(teamID) }, onTeamsUpdateTeamRetentionPolicy: (metas: ReadonlyArray) => { storeRegistry.getState('teams').dispatch.updateTeamRetentionPolicy(metas) }, - onUsersUpdates: (updates: ReadonlyArray<{name: string; info: Partial}>) => { - storeRegistry.getState('users').dispatch.updates(updates) - }, }, }, }) @@ -232,9 +218,6 @@ export const initTeamsCallbacks = () => { ) => { storeRegistry.getState('chat').dispatch.previewConversation(p) }, - onUsersUpdates: (updates: ReadonlyArray<{name: string; info: Partial}>) => { - storeRegistry.getState('users').dispatch.updates(updates) - }, }, }, }) @@ -270,21 +253,6 @@ export const initRecoverPasswordCallbacks = () => { }) } -export const initTracker2Callbacks = () => { - const currentState = useTrackerState.getState() - useTrackerState.setState({ - dispatch: { - ...currentState.dispatch, - defer: { - ...currentState.dispatch.defer, - onUsersUpdates: (updates: ReadonlyArray<{name: string; info: Partial}>) => { - useUsersState.getState().dispatch.updates(updates) - }, - }, - }, - }) -} - export const initSharedSubscriptions = () => { // HMR cleanup: unsubscribe old store subscriptions before re-subscribing for (const unsub of _sharedUnsubs) unsub() @@ -559,7 +527,6 @@ export const initSharedSubscriptions = () => { initTeamsCallbacks() initPushCallbacks() initRecoverPasswordCallbacks() - initTracker2Callbacks() } // This is to defer loading stores we don't need immediately. diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 385168be7e0b..61babecc4454 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -32,6 +32,7 @@ import {storeRegistry} from '@/stores/store-registry' import {uint8ArrayToString} from '@/util/uint8array' import {useConfigState} from '@/stores/config' import {useCurrentUserState} from '@/stores/current-user' +import {useUsersState} from '@/stores/users' import {useWaitingState} from '@/stores/waiting' const defaultTopReacjis = [ @@ -231,10 +232,8 @@ export type State = Store & { onGetTeamsTeamIDToMembers: ( teamID: T.Teams.TeamID ) => ReadonlyMap | undefined - onGetUsersInfoMap: () => ReadonlyMap onTeamsGetMembers: (teamID: T.Teams.TeamID) => Promise onTeamsUpdateTeamRetentionPolicy: (metas: ReadonlyArray) => void - onUsersUpdates: (updates: ReadonlyArray<{name: string; info: Partial}>) => void } createConversation: (participants: ReadonlyArray, highlightMessageID?: T.Chat.MessageID) => void ensureWidgetMetas: () => void @@ -393,18 +392,12 @@ export const useChatState = Z.createZustand('chat', (set, get) => { onGetTeamsTeamIDToMembers: (_teamID: T.Teams.TeamID) => { throw new Error('onGetTeamsTeamIDToMembers not properly initialized') }, - onGetUsersInfoMap: () => { - throw new Error('onGetUsersInfoMap not properly initialized') - }, onTeamsGetMembers: (_teamID: T.Teams.TeamID) => { throw new Error('onTeamsGetMembers not properly initialized') }, onTeamsUpdateTeamRetentionPolicy: (_metas: ReadonlyArray) => { throw new Error('onTeamsUpdateTeamRetentionPolicy not properly initialized') }, - onUsersUpdates: (_updates: ReadonlyArray<{name: string; info: Partial}>) => { - throw new Error('onUsersUpdates not properly initialized') - }, }, ensureWidgetMetas: () => { const {inboxLayout} = get() @@ -748,7 +741,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { const usernames = update.CanonicalName.split(',') const broken = (update.breaks.breaks || []).map(b => b.user.username) const updates = usernames.map(name => ({info: {broken: broken.includes(name)}, name})) - get().dispatch.defer.onUsersUpdates(updates) + useUsersState.getState().dispatch.updates(updates) break } case 'chat.1.chatUi.chatInboxUnverified': @@ -969,7 +962,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { }, onGetInboxConvsUnboxed: action => { // TODO not reactive - const infoMap = get().dispatch.defer.onGetUsersInfoMap() + const {infoMap} = useUsersState.getState() const {convs} = action.payload.params const inboxUIItems = JSON.parse(convs) as Array const metas: Array = [] @@ -995,7 +988,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { }) }) if (Object.keys(usernameToFullname).length > 0) { - get().dispatch.defer.onUsersUpdates( + useUsersState.getState().dispatch.updates( Object.keys(usernameToFullname).map(name => ({ info: {fullname: usernameToFullname[name]}, name, @@ -1029,7 +1022,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { return map }, {}) - get().dispatch.defer.onUsersUpdates( + useUsersState.getState().dispatch.updates( Object.keys(usernameToFullname).map(name => ({ info: {fullname: usernameToFullname[name]}, name, diff --git a/shared/stores/team-building.tsx b/shared/stores/team-building.tsx index 732ff7c81855..637af1c68a56 100644 --- a/shared/stores/team-building.tsx +++ b/shared/stores/team-building.tsx @@ -11,6 +11,7 @@ import {validateEmailAddress} from '@/util/email-address' import {registerDebugClear} from '@/util/debug' import {searchWaitingKey} from '@/constants/strings' import {navigateUp, getModalStack} from '@/constants/router' +import {useUsersState} from '@/stores/users' export {allServices, selfToUser} from '@/constants/team-building' export {searchWaitingKey} from '@/constants/strings' @@ -51,8 +52,6 @@ export type State = Store & { onGetSettingsContactsImportEnabled: () => boolean | undefined onGetSettingsContactsUserCountryCode: () => string | undefined onShowUserProfile: (username: string) => void - onUsersGetBlockState: (usernames: ReadonlyArray) => void - onUsersUpdates: (infos: ReadonlyArray<{name: string; info: Partial}>) => void } fetchUserRecs: () => void finishTeamBuilding: () => void @@ -316,12 +315,6 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { onShowUserProfile: (_username: string) => { throw new Error('onShowUserProfile not properly initialized') }, - onUsersGetBlockState: (_usernames: ReadonlyArray) => { - throw new Error('onUsersGetBlockState not properly initialized') - }, - onUsersUpdates: (_infos: ReadonlyArray<{name: string; info: Partial}>) => { - throw new Error('onUsersUpdates not properly initialized') - }, }, fetchUserRecs: () => { const includeContacts = get().namespace === 'chat' @@ -458,9 +451,9 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { blocks.push(keybase) } } - get().dispatch.defer.onUsersUpdates(updates) + useUsersState.getState().dispatch.updates(updates) if (blocks.length) { - get().dispatch.defer.onUsersGetBlockState(blocks) + useUsersState.getState().dispatch.getBlockState(blocks) } } ignorePromise(f()) diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 2e70dd303772..400095a5bbaf 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -23,6 +23,7 @@ import {storeRegistry} from '@/stores/store-registry' import {useConfigState} from '@/stores/config' import {type useChatState} from '@/stores/chat' import {useCurrentUserState} from '@/stores/current-user' +import {useUsersState} from '@/stores/users' import * as Util from '@/constants/teams' import {getTab} from '@/constants/router' @@ -834,7 +835,6 @@ export type State = Store & { onChatPreviewConversation?: ( p: Parameters['dispatch']['previewConversation']>[0] ) => void - onUsersUpdates?: (updates: ReadonlyArray<{name: string; info: Partial}>) => void } addMembersWizardPushMembers: (members: Array) => void addMembersWizardRemoveMember: (assertion: string) => void @@ -1313,9 +1313,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { }) => { throw new Error('onChatPreviewConversation not implemented') }, - onUsersUpdates: (_updates: ReadonlyArray<{name: string; info: Partial}>) => { - throw new Error('onUsersUpdates not implemented') - }, }, deleteChannelConfirmed: (teamID, conversationIDKey) => { const f = async () => { @@ -1494,7 +1491,7 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { set(s => { s.teamIDToMembers.set(teamID, members) }) - get().dispatch.defer.onUsersUpdates?.( + useUsersState.getState().dispatch.updates( [...members.values()].map(m => ({ info: {fullname: m.fullName}, name: m.username, diff --git a/shared/stores/tracker.tsx b/shared/stores/tracker.tsx index 9b37b207db4e..7494d6e98b6d 100644 --- a/shared/stores/tracker.tsx +++ b/shared/stores/tracker.tsx @@ -8,6 +8,7 @@ import {RPCError} from '@/util/errors' import {mapGetEnsureValue} from '@/util/map' import {navigateAppend, navigateUp} from '@/constants/router' import {useCurrentUserState} from '@/stores/current-user' +import {useUsersState} from '@/stores/users' export const noDetails: T.Tracker.Details = { assertions: new Map(), @@ -163,9 +164,7 @@ const initialStore: Store = { export type State = Store & { dispatch: { - defer: { - onUsersUpdates?: (updates: ReadonlyArray<{name: string; info: Partial}>) => void - } + defer: {} changeFollow: (guiID: string, follow: boolean) => void closeTracker: (guiID: string) => void getProofSuggestions: () => void @@ -231,11 +230,7 @@ export const useTrackerState = Z.createZustand('tracker', (set, get) => { s.showTrackerSet.delete(username) }) }, - defer: { - onUsersUpdates: () => { - throw new Error('onUsersUpdates not implemented') - }, - }, + defer: {}, getProofSuggestions: () => { const f = async () => { try { @@ -327,9 +322,9 @@ export const useTrackerState = Z.createZustand('tracker', (set, get) => { d.followersCount = d.followers.size }) if (fs.users) { - get().dispatch.defer.onUsersUpdates?.( - fs.users.map(u => ({info: {fullname: u.fullName}, name: u.username})) - ) + useUsersState + .getState() + .dispatch.updates(fs.users.map(u => ({info: {fullname: u.fullName}, name: u.username}))) } } catch (error) { if (error instanceof RPCError) { @@ -353,9 +348,9 @@ export const useTrackerState = Z.createZustand('tracker', (set, get) => { d.followingCount = d.following.size }) if (fs.users) { - get().dispatch.defer.onUsersUpdates?.( - fs.users.map(u => ({info: {fullname: u.fullName}, name: u.username})) - ) + useUsersState + .getState() + .dispatch.updates(fs.users.map(u => ({info: {fullname: u.fullName}, name: u.username}))) } } catch (error) { if (error instanceof RPCError) { @@ -447,7 +442,7 @@ export const useTrackerState = Z.createZustand('tracker', (set, get) => { ) d.hidFromFollowers = hidFromFollowers }) - username && get().dispatch.defer.onUsersUpdates?.([{info: {fullname: card.fullName}, name: username}]) + username && useUsersState.getState().dispatch.updates([{info: {fullname: card.fullName}, name: username}]) }, notifyReset: guiID => { set(s => { From ab6e33248acb46ed4f43eed65b17f7cca243137a Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 15 Apr 2026 11:51:04 -0400 Subject: [PATCH 2/4] WIP --- shared/constants/init/shared.tsx | 21 --------------------- shared/stores/chat.tsx | 8 ++------ shared/stores/push.d.ts | 3 --- shared/stores/push.desktop.tsx | 5 ----- shared/stores/push.native.tsx | 8 ++------ shared/stores/tests/push.desktop.test.ts | 3 +-- 6 files changed, 5 insertions(+), 43 deletions(-) diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index f901f83d679f..22d37d51f16a 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -47,7 +47,6 @@ import {useDarkModeState} from '@/stores/darkmode' import {useFollowerState} from '@/stores/followers' import {useModalHeaderState} from '@/stores/modal-header' import {useProvisionState} from '@/stores/provision' -import {usePushState} from '@/stores/push' import {useSettingsContactsState} from '@/stores/settings-contacts' import {useState as useRecoverPasswordState} from '@/stores/recover-password' import {useTeamsState} from '@/stores/teams' @@ -185,10 +184,6 @@ export const initChat2Callbacks = () => { dispatch: { ...currentState.dispatch, defer: { - onGetDaemonState: () => { - const daemonState = storeRegistry.getState('daemon') - return {dispatch: daemonState.dispatch, handshakeVersion: daemonState.handshakeVersion} - }, onGetTeamsTeamIDToMembers: (teamID: T.Teams.TeamID) => { return storeRegistry.getState('teams').teamIDToMembers.get(teamID) }, @@ -223,21 +218,6 @@ export const initTeamsCallbacks = () => { }) } -export const initPushCallbacks = () => { - const currentState = usePushState.getState() - usePushState.setState({ - dispatch: { - ...currentState.dispatch, - defer: { - ...currentState.dispatch.defer, - onGetDaemonHandshakeState: () => { - return useDaemonState.getState().handshakeState - }, - }, - }, - }) -} - export const initRecoverPasswordCallbacks = () => { const currentState = useRecoverPasswordState.getState() useRecoverPasswordState.setState({ @@ -525,7 +505,6 @@ export const initSharedSubscriptions = () => { initChat2Callbacks() initTeamBuildingCallbacks() initTeamsCallbacks() - initPushCallbacks() initRecoverPasswordCallbacks() } diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 61babecc4454..13bf1c727073 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -9,7 +9,6 @@ import * as TeamConstants from '@/constants/teams' import * as Z from '@/util/zustand' import isEqual from 'lodash/isEqual' import logger from '@/logger' -import type {State as DaemonState} from '@/stores/daemon' import type * as Router2 from '@/constants/router' import {ProviderScreen} from '@/stores/convostate' import type {GetOptionsRet, RouteDef} from '@/constants/types/router' @@ -32,6 +31,7 @@ import {storeRegistry} from '@/stores/store-registry' import {uint8ArrayToString} from '@/util/uint8array' import {useConfigState} from '@/stores/config' import {useCurrentUserState} from '@/stores/current-user' +import {useDaemonState} from '@/stores/daemon' import {useUsersState} from '@/stores/users' import {useWaitingState} from '@/stores/waiting' @@ -228,7 +228,6 @@ export type State = Store & { badgesUpdated: (badgeState?: T.RPCGen.BadgeState) => void clearMetas: () => void defer: { - onGetDaemonState: () => Pick onGetTeamsTeamIDToMembers: ( teamID: T.Teams.TeamID ) => ReadonlyMap | undefined @@ -386,9 +385,6 @@ export const useChatState = Z.createZustand('chat', (set, get) => { ignorePromise(f()) }, defer: { - onGetDaemonState: () => { - throw new Error('onGetDaemonState not properly initialized') - }, onGetTeamsTeamIDToMembers: (_teamID: T.Teams.TeamID) => { throw new Error('onGetTeamsTeamIDToMembers not properly initialized') }, @@ -446,7 +442,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { if (get().staticConfig) { return } - const {handshakeVersion, dispatch} = get().dispatch.defer.onGetDaemonState() + const {handshakeVersion, dispatch} = useDaemonState.getState() const f = async () => { const name = 'chat.loadStatic' dispatch.wait(name, handshakeVersion, true) diff --git a/shared/stores/push.d.ts b/shared/stores/push.d.ts index a6ed4fb81a35..cd2fe4c9be72 100644 --- a/shared/stores/push.d.ts +++ b/shared/stores/push.d.ts @@ -11,9 +11,6 @@ type Store = T.Immutable<{ export type State = Store & { dispatch: { - defer: { - onGetDaemonHandshakeState?: () => T.Config.DaemonHandshakeState - } checkPermissions: () => Promise clearPendingPushNotification: () => void deleteToken: (version: number) => void diff --git a/shared/stores/push.desktop.tsx b/shared/stores/push.desktop.tsx index dfd7452297d1..274991fc6d4e 100644 --- a/shared/stores/push.desktop.tsx +++ b/shared/stores/push.desktop.tsx @@ -16,11 +16,6 @@ export const usePushState = Z.createZustand('push', () => { return Promise.resolve(false) }, clearPendingPushNotification: () => {}, - defer: { - onGetDaemonHandshakeState: () => { - return 'done' - }, - }, deleteToken: () => {}, handlePush: () => {}, initialPermissionsCheck: () => {}, diff --git a/shared/stores/push.native.tsx b/shared/stores/push.native.tsx index abbc3173465b..abeb68e41a36 100644 --- a/shared/stores/push.native.tsx +++ b/shared/stores/push.native.tsx @@ -3,6 +3,7 @@ import {ignorePromise, neverThrowPromiseFunc, timeoutPromise} from '@/constants/ import {emitDeepLink} from '@/router-v2/linking' import {useConfigState} from '@/stores/config' import {useCurrentUserState} from '@/stores/current-user' +import {useDaemonState} from '@/stores/daemon' import {useLogoutState} from '@/stores/logout' import {useWaitingState} from '@/stores/waiting' import * as Z from '@/util/zustand' @@ -114,11 +115,6 @@ export const usePushState = Z.createZustand('push', (set, get) => { s.pendingPushNotification = undefined }) }, - defer: { - onGetDaemonHandshakeState: () => { - throw new Error('onGetDaemonHandshakeState not implemented') - }, - }, deleteToken: version => { const f = async () => { const waitKey = 'push:deleteToken' @@ -328,7 +324,7 @@ export const usePushState = Z.createZustand('push', (set, get) => { if ( p.show && useConfigState.getState().loggedIn && - get().dispatch.defer.onGetDaemonHandshakeState?.() === 'done' && + useDaemonState.getState().handshakeState === 'done' && !get().justSignedUp && !get().hasPermissions ) { diff --git a/shared/stores/tests/push.desktop.test.ts b/shared/stores/tests/push.desktop.test.ts index a199c06a888a..5a0c2882629d 100644 --- a/shared/stores/tests/push.desktop.test.ts +++ b/shared/stores/tests/push.desktop.test.ts @@ -6,11 +6,10 @@ afterEach(() => { resetAllStores() }) -test('desktop push store reports the desktop handshake state and resettable defaults', async () => { +test('desktop push store reports resettable defaults', async () => { const {dispatch} = usePushState.getState() await expect(dispatch.checkPermissions()).resolves.toBe(false) - expect(dispatch.defer.onGetDaemonHandshakeState?.()).toBe('done') dispatch.clearPendingPushNotification() dispatch.deleteToken(1) From 3aa09aa7ecc6de6ab22164ef77adb8b2cc666c6b Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 15 Apr 2026 11:56:02 -0400 Subject: [PATCH 3/4] WIP --- shared/constants/init/shared.tsx | 6 ------ shared/stores/team-building.tsx | 14 +++----------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 22d37d51f16a..b1f39c839e86 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -121,12 +121,6 @@ export const initTeamBuildingCallbacks = () => { onAddMembersWizardPushMembers: (members: Array) => { useTeamsState.getState().dispatch.addMembersWizardPushMembers(members) }, - onGetSettingsContactsImportEnabled: () => { - return useSettingsContactsState.getState().importEnabled - }, - onGetSettingsContactsUserCountryCode: () => { - return useSettingsContactsState.getState().userCountryCode - }, onShowUserProfile: (username: string) => { navToProfile(username) }, diff --git a/shared/stores/team-building.tsx b/shared/stores/team-building.tsx index 637af1c68a56..5723929d6cae 100644 --- a/shared/stores/team-building.tsx +++ b/shared/stores/team-building.tsx @@ -11,6 +11,7 @@ import {validateEmailAddress} from '@/util/email-address' import {registerDebugClear} from '@/util/debug' import {searchWaitingKey} from '@/constants/strings' import {navigateUp, getModalStack} from '@/constants/router' +import {useSettingsContactsState} from '@/stores/settings-contacts' import {useUsersState} from '@/stores/users' export {allServices, selfToUser} from '@/constants/team-building' export {searchWaitingKey} from '@/constants/strings' @@ -49,8 +50,6 @@ export type State = Store & { onAddMembersWizardPushMembers: (members: Array) => void onFinishedTeamBuildingChat: (users: ReadonlySet) => void onFinishedTeamBuildingCrypto: (users: ReadonlySet) => void - onGetSettingsContactsImportEnabled: () => boolean | undefined - onGetSettingsContactsUserCountryCode: () => string | undefined onShowUserProfile: (username: string) => void } fetchUserRecs: () => void @@ -306,12 +305,6 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { onFinishedTeamBuildingCrypto: (_users: ReadonlySet) => { throw new Error('onFinishedTeamBuildingCrypto not properly initialized') }, - onGetSettingsContactsImportEnabled: () => { - throw new Error('onGetSettingsContactsImportEnabled not properly initialized') - }, - onGetSettingsContactsUserCountryCode: () => { - throw new Error('onGetSettingsContactsUserCountryCode not properly initialized') - }, onShowUserProfile: (_username: string) => { throw new Error('onShowUserProfile not properly initialized') }, @@ -330,8 +323,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const contactRes = _contactRes || [] const contacts = contactRes.map(contactToUser) let suggestions = suggestionRes.map(interestingPersonToUser) - const expectingContacts = - get().dispatch.defer.onGetSettingsContactsImportEnabled() && includeContacts + const expectingContacts = useSettingsContactsState.getState().importEnabled && includeContacts if (expectingContacts) { suggestions = suggestions.slice(0, 10) } @@ -432,7 +424,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { let users: typeof _users if (selectedService === 'keybase') { // If we are on Keybase tab, do additional search if query is phone/email. - const userRegion = get().dispatch.defer.onGetSettingsContactsUserCountryCode() + const {userCountryCode: userRegion} = useSettingsContactsState.getState() users = await specialContactSearch(_users, searchQuery, userRegion) } else { users = _users From ee2cf94566b5cd0d1fd7a012a7fb746bdec916fe Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 15 Apr 2026 12:05:47 -0400 Subject: [PATCH 4/4] WIP --- skill/zustand-store-pruning/SKILL.md | 42 +++++++++++++++++++ .../references/keybase-examples.md | 31 ++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/skill/zustand-store-pruning/SKILL.md b/skill/zustand-store-pruning/SKILL.md index a3ba27b36b5c..b3f6797cc3c1 100644 --- a/skill/zustand-store-pruning/SKILL.md +++ b/skill/zustand-store-pruning/SKILL.md @@ -82,6 +82,33 @@ Prefer reloading in components instead of keeping a store cache when: - The cache only saves a small RPC but forces unrelated screens to coordinate through global state - The notification path only exists to keep that convenience cache warm +Prefer direct store imports instead of `shared/constants/init/shared.tsx` callback plumbing when: + +- A store's `dispatch.defer.*` callback exists only to forward to another Zustand store +- The target store is leaf-like and does not import the calling store back +- The callback does not need platform-specific override behavior +- The callback does not exist to break a real import cycle + +Keep init-time callback plumbing when: + +- The direct import would introduce a store cycle or make one more likely +- The callback is intentionally abstracting a platform split or runtime override +- The callback bundles several stores behind one bootstrap seam that still matters + +For Keybase repo cleanup, this usually means: + +- If `chat`, `teams`, `tracker`, `team-building`, `push`, or similar stores are only calling into leaf stores like `users`, `daemon`, or `settings-contacts`, prefer `useLeafStore.getState()` directly +- Treat `shared/constants/init/shared.tsx` as a smell when it is only wiring one store method straight into another +- After replacing the direct call sites, delete the matching `defer` field types, default throwers, init wiring, dead imports, and any now-empty init helper + +Concerns to check before making this change: + +- Search both directions with `rg` to confirm the target store does not already import the caller +- Check `store-registry.tsx`, dynamic `require(...)`, and platform-split files before assuming a store is leaf-safe +- Preserve bootstrap-only behavior that is still real; do not remove an init helper if it still wires unrelated callbacks +- Update tests and desktop/native stubs that may still reference the old `defer` field +- Keep the remaining `defer` surface coherent; if a store's `defer` object becomes empty, remove the field rather than leaving dead scaffolding behind + For listener-driven multi-step flows, separate callback plumbing from UI state: - If `incomingCallMap` or `customResponseIncomingCallMap` only need to keep live response handlers across navigation, move banners, form state, and selections out of the feature store first @@ -116,6 +143,13 @@ For each field and action, label it: Do this before writing code. If several fields move together, migrate that whole screen flow in one pass. +Also label cross-store callback seams: + +- `keep-init-plumbing` +- `replace-direct-import` + +Use `replace-direct-import` when a `dispatch.defer.*` field only forwards to a leaf-like store and there is no import-cycle risk. + ### 3. Move screen-owned RPCs into components Prefer `C.useRPC` when the RPC belongs to the current screen: @@ -170,6 +204,14 @@ After consumers move off the store: - Keep reset behavior coherent for whatever remains - Preserve public store names unless there is a strong reason to rename them +After removing init callback plumbing: + +- Delete the matching `dispatch.defer` type entries +- Delete the matching default throw implementations +- Delete `shared/constants/init/shared.tsx` wiring for those callbacks +- Delete now-empty init helpers and their startup calls +- Delete stale imports left behind in both the store and `shared.tsx` + If nothing meaningful remains after moving screen-owned data out, delete the store entirely instead of leaving a one-field convenience cache behind. ## Commit Shape diff --git a/skill/zustand-store-pruning/references/keybase-examples.md b/skill/zustand-store-pruning/references/keybase-examples.md index 0288df0b87c5..8fd3a8402b66 100644 --- a/skill/zustand-store-pruning/references/keybase-examples.md +++ b/skill/zustand-store-pruning/references/keybase-examples.md @@ -98,6 +98,37 @@ These stores are not automatic no-touch zones, but they need a stronger reason b They contain global caches, notification-driven state, navigation coordination, or app/session state that does not belong to a single screen. +## Replacing `init/shared.tsx` Plumbing + +Common Keybase cleanup pattern: + +- A store defines `dispatch.defer.onSomething` +- `shared/constants/init/shared.tsx` fills that callback with a direct call into another store +- The callback exists only to bridge one Zustand store to another + +Good direct-import targets: + +- `users` +- `daemon` +- `settings-contacts` + +These are good targets when they are leaf-like for the call path you are changing. + +Recent examples: + +- `team-building` now imports `useUsersState` and `useSettingsContactsState` directly instead of receiving `onUsersUpdates`, `onUsersGetBlockState`, `onGetSettingsContactsImportEnabled`, and `onGetSettingsContactsUserCountryCode` from `shared/constants/init/shared.tsx` +- `chat` now imports `useUsersState` and `useDaemonState` directly for user cache updates and static-config handshake coordination instead of using `onUsersUpdates`, `onGetUsersInfoMap`, and `onGetDaemonState` +- `teams` and `tracker` now call `useUsersState` directly instead of routing through `onUsersUpdates` +- `push.native` now reads `useDaemonState.getState().handshakeState` directly instead of using `onGetDaemonHandshakeState` + +Checklist for this pattern: + +- Confirm the target store does not import the caller back +- Check platform files and dynamic `require(...)` before assuming no cycle +- Replace all live call sites before deleting the `defer` field +- Remove the matching type entries, default throwers, init wiring, no-op init helpers, and stale tests +- Keep `shared/constants/init/shared.tsx` for real bootstrap coordination, not simple store-to-store forwarding + ## Route Param Patterns In This Repo Common navigation shape: