- Server state is fetched and cached with React Query. It is never copied into a store.
- Client state that outlives a screen lives in a Zustand store. Examples are the session, the running workout and the color scheme.
React Query
The singleQueryClient is created in src/lib/api/queryClient.ts and provided in the root layout.
The cache is in memory only. There is no persister, so a cold start always fetches again.
authStore calls queryClient.clear() on sign in, studio switch and sign out.
Query keys
Each feature exports its keys from its hooks file.Refresh on foreground
refetchOnWindowFocus is off because React Native has no window focus. Instead (app)/_layout.tsx listens to AppState and, on active, invalidates homeQueryKey and calls authStore.refreshSession(). Individual hooks add their own listeners where needed. usePushPermission refetches on active so the banner clears after a trip to system settings.
Patterns in use
placeholderData: keepPreviousDatainuseHome, so changing the nutrition day hint does not flash a skeleton.- Optimistic updates with
onMutateandsetQueryData, then an invalidation inonSettled. See the worked example in Feature modules. - Side effects on fresh data inside the hook.
useHomecallssyncHomeWidgetswhenever data arrives.
Zustand stores
All stores use plaincreate. None use the persist middleware. Stores that survive a restart write to storage themselves.
Reading stores
Components subscribe with a selector:useAuthStore((s) => s.status). Code outside React reads with getState() and subscribes with subscribe(). (app)/_layout.tsx uses both to mirror the workout and cardio sessions into the Live Activity and the Android notification.
Thin selector hooks wrap the common reads: useColors(), useColorScheme(), useBrand(), useBrandPalette(), useHomeTheme(), useWorkoutSummary().
Persistence layers
There are three places on the device.SecureStore
src/lib/storage/secureStore.ts exports small typed wrappers. It is async, encrypted, and on iOS uses the after-first-unlock keychain class described in Authentication. On web it falls back to localStorage.
Two more keys are written with the synchronous SecureStore API from
src/lib/i18n/language.ts, because they are needed before the first render:
JSON files
src/lib/storage/jsonFileStore.ts provides readJsonSync, writeJsonSync and removeJsonSync. Files live in the app’s document directory through expo-file-system. On web they map to localStorage entries with the file name as the key. All three functions swallow errors and return null or nothing.
They are synchronous on purpose. A store can read its snapshot while it is being created, so the first render already has the restored state.
The workout snapshot write is throttled to one write per 400 ms, and flushed at once when the app leaves the foreground. Details are in Workout crash recovery.
Every snapshot carries a
version. Bump the constant when the stored shape changes so old files are discarded instead of being read into a new shape.
Shared widget directory
Home screen widgets and the Live Activity run in a separate iOS process.features/branding/widgetBrandLogo.ts copies the studio logo, or the bundled Perform mark, into widgetsDirectory from expo-widgets so SwiftUI can load it. See Live Activity and widgets.
What is never persisted
- The React Query cache.
- The preview token. In preview mode the stores skip all device writes by checking
IS_PREVIEW. - Module-level latches such as
pushRegisteredinsrc/lib/notifications.tsandcoldStartHandledinsrc/lib/notificationRouting.ts. They live for the JS process. Sign out does not reload the bundle, which is whyresetPushRegistration()exists.