Skip to content

State management

Server data (lists, details, anything from your API) lives in TanStack Query, see API layer. The state library is for app state: the session, settings and preferences.

Each feature keeps its store private and exports hooks instead. For auth:

Hook Returns
useIsLoggedIn() boolean
useCurrentUser() the current User or null
useLogin() { login(credentials), isLoading }, and shows a success toast
useLogout() a logout() function
import { useIsLoggedIn, useLogout } from '@/features/auth';

Because UI only touches these hooks, the CLI can swap Zustand for Redux without changing a screen, and you can too.

Stores are created with createPersistedStore() from src/store/createPersistedStore.ts. It:

  • persists to encrypted MMKV through a Zustand storage adapter,
  • uses skipHydration: true, so stores can be created at import time, before storage is ready,
  • is rehydrated explicitly by rehydrateStores() once storage has opened.

Adding a store:

src/features/settings/store/useSettingsStore.ts
import { createPersistedStore } from '@/store/createPersistedStore';
interface SettingsState {
theme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark') => void;
}
export const useSettingsStore = createPersistedStore<SettingsState>(
'settings-storage',
(set) => ({
theme: 'light',
setTheme: (theme) => set({ theme }),
}),
);

Always read with an atomic selector, useSettingsStore((s) => s.theme). Calling the hook with no selector subscribes to the whole store and re-renders on every change, so ESLint flags it.

Then register the store, or it will never load its saved state:

src/store/index.ts
export async function rehydrateStores(): Promise<void> {
await useUserStore.persist.rehydrate();
await useSettingsStore.persist.rehydrate(); // ← add
}

Persisted state goes to MMKV, which is encrypted at rest. Raw passwords are never stored: the auth state keeps only the returned user profile, and tokens are stored separately in src/storage/token.ts. See Storage & security.