Frontend
State Management

Frontend state management

How Eziseller's Next.js frontend organises client state across React Context, local component state, and cookie/localStorage persistence. Audience: new dev with React experience, no Eziseller context.

1. Overview

Eziseller uses React Context + useReducer/useState as its only client-side state container. There is no Redux, no Zustand, and no React Query / SWR in active use. zustand@5 is listed in package.json dependencies but is not imported anywhere in src/ — it is effectively dead weight pending a future refactor.

Domain state (orders, products, customers, etc.) is not cached globally. Each page/component fetches through axios wrappers in src/lib/api/* on mount and keeps the result in local useState. Cross-cutting concerns — auth, cart, theme, toasts, navigation loading — live in Context providers mounted in the app root.

Persistence is split between cookies (auth, via js-cookie) and localStorage (cart, theme). Server Components cannot read either, so all stateful UI is marked "use client".

2. Architecture

Providers wrap the tree once; components consume via typed useAuth(), useCart(), useTheme(), useToast() hooks. Domain fetches bypass Context entirely — state lives in the leaf component.

3. Contexts

ContextFilePurposeStorage
AuthContextsrc/contexts/AuthContext.tsxUser identity, login/signup/logout, profile + logo mutations, session-expired event handlingCookies
CartContextsrc/contexts/CartContext.tsxBuyer-facing catalog cart (items, quantity, total) for /catalog/[slug]/*localStorage
ThemeContextsrc/contexts/ThemeContext.tsxTheme mode (light/dark/system) and colour palettelocalStorage
ToastContextsrc/contexts/ToastContext.tsxImperative toast.success/error/... API; stores active toast listIn-memory only
NavigationLoadingContextsrc/contexts/NavigationLoadingContext.tsxGlobal route-transition spinner; 8s fallback auto-stop, 300ms minimum displayIn-memory only

AuthContext is the only one built on useReducer (action types: SET_LOADING, LOGIN_SUCCESS, LOGOUT, UPDATE_USER, AUTH_ERROR, CLEAR_ERROR). The rest use useState.

4. Zustand stores

None. zustand is declared in package.json but grep for from 'zustand' in src/ returns zero matches. Treat it as a future-use dependency; do not assume any global store exists.

5. Persistence strategy

Cookie keys (src/lib/auth.ts):

  • auth_token — JWT access token, 1 day
  • refresh_token — JWT refresh token, 7 days
  • user_data — JSON-serialised User, 7 days (acts as an SSR-safe warm cache before /api/auth/profile resolves)
  • admin_auth_token, admin_refresh_token, impersonating_user — saved aside during admin impersonation and restored on stopImpersonation()

localStorage keys:

  • catalog_cart — array of CartItem
  • themeMode'light' | 'dark' | 'system'
  • themeColors — JSON of ThemeColors
  • theme — legacy key used by src/hooks/useTheme.ts (parallel to themeMode, not unified)

sessionStorage.impersonate_data is used only by /admin/users/impersonate handoff.

6. Key files

7. Env vars & config

None specific to state management. Auth cookie domain and lifetimes are hard-coded in src/lib/auth.ts.

8. Gotchas & troubleshooting

  • Cart persists across storesCause: CartContext uses a single localStorage key catalog_cart with no store/slug scoping. Opening catalog A, adding items, then visiting catalog B shows A's items. → Fix planned: key the cart by store slug (catalog_cart:${slug}); see docs/kb/01-core/catalog/cart.md.
  • Stale useAuth() values in callbacksCause: AuthContext value is rebuilt each render; closures captured inside useEffect/useCallback with empty deps see the initial null user. → Fix: include user in the dep array, or read from a ref, or dispatch into the reducer rather than reading state.
  • 401 loops after refresh-token expiryCause: authApi interceptor retries once with _retry flag; if refresh itself returns 401 the interceptor clears cookies and hard-navigates to /login. Third-party axios instances that don't share this interceptor will loop. → Fix: always import shared clients from src/lib/api/*, never create a raw axios.create for authenticated calls.
  • SSR / localStorage hydration mismatchCause: CartContext and ThemeContext initialise with useState([]) / default theme, then overwrite from localStorage in a client useEffect. First paint shows empty cart / default theme before the effect runs. → Fix: gate cart/theme-dependent UI behind an isHydrated flag, or render a skeleton until the effect fires.
  • getToken() returns null on the serverCause: typeof window === 'undefined' guard. Any code path that runs during SSR (e.g. a Server Component importing src/lib/auth.ts indirectly) will see no token. → Fix: keep auth-aware code in "use client" modules; do not pass auth state through props from Server Components.
  • Three theme keys coexistthemeMode, themeColors (ThemeContext) and theme (src/hooks/useTheme.ts). They are not kept in sync. → Fix: treat useTheme.ts as legacy; prefer useTheme() from ThemeContext.
  • Impersonation leaks on logoutclearAuth() wipes admin cookies too, so a mid-impersonation logout loses the admin session. stopImpersonation() must be called first.
  • Refresh tokens are not server-tracked across restarts — the backend keeps them in an in-memory Set (see backend/routes/auth.ts). A backend redeploy invalidates every refresh token; the frontend surfaces this as a forced re-login via the session-expired event.

9. Extension points

Use a new Context when: state is genuinely cross-cutting (touches >3 unrelated routes), changes infrequently, and fits a provider shape. Mount the provider in src/app/layout.tsx (global) or a route-group layout (scoped, like CartProvider under /catalog).

Use component-local useState + an src/lib/api/* call when: data is page-scoped (order list, product detail, dashboard stats). This is the dominant pattern — do not promote it to Context just to avoid prop drilling one level.

Consider Zustand when: you need a store that's consumed by many unrelated components with frequent updates and selector-level subscriptions (e.g., a future multi-tab inbox). The dependency is already installed; add the store under src/stores/ and document it here. Do not introduce Redux.

Do not add React Query / SWR ad-hoc — it would fragment the data-fetching story. Either migrate wholesale or stay on the current axios + useState pattern.

10. Related docs

  • authentication.md — JWT flow, refresh tokens, session-expired event
  • cart.md — cart item shape and the cross-store contamination bug
  • maintenance-mode.md — middleware-level gating that bypasses Context entirely