Skip to content

State Management

State stays with the narrowest part of the webapp that can manage it correctly. The current app uses TanStack Query for server state, TanStack Router for URL state, React Hook Form with Zod for forms, React context for provider-scoped views of cached data, local React state for interactions, and next-themes for the color-mode preference.

Ownership Map

StateOwnerCurrent examples
Server and auth dataTanStack QuerySession, organizations, active organization, health check, event mutation
Route stateTanStack RouterSafe post-sign-in redirect, reset-password token
Form stateReact Hook Form and ZodSign-in, workspace, account, and event forms
Provider-scoped derived stateReact context backed by Query hooksActive workspace and workspace list
Component interactionLocal React stateMobile sheet, notification popover, dialogs, calendar
Theme preferencenext-themesSystem default and explicit light/dark selection
Remembered sign-in emailsessionStorageEmail and checkbox state within the browser tab

Zustand is installed but the current webapp has no Zustand store. Don't move server, auth, route, form, theme, or local interaction state into a global store. Add one only when a concrete client-only state requirement has several distant consumers and must outlive their component lifecycles.

Server State

src/integrations/tanstack-query/ creates the shared Query Client and provider. src/main.tsx also places that client in router context, so routes and components use the same cache.

Use the configured clients:

  • auth from src/lib/api/setup.ts for Better Auth sessions and organization operations.
  • api from src/lib/api/tuyau.ts for other server routes.

Shared auth-client factories define session and organization query options and keys. Webapp hooks in src/hooks/auth/ wrap them with navigation, toast, and error behavior. Tuyau supplies generated query and mutation options for routes such as health checks and events.

Workspace State

The workspace route loads organizations before rendering. WorkspaceProvider then reads useActiveOrganization() and useOrganizations() from the Query cache and exposes a provider-scoped view:

tsx
const contextValue = {
  activeWorkspace,
  workspaceId: activeWorkspace?.id,
  workspaceName: activeWorkspace?.name,
  workspaces: workspaces || [],
  isLoading,
  hasWorkspace: !!activeWorkspace,
}

The context does not replace the Query cache or duplicate organization data in another store. It gives workspace descendants one typed access point and ensures useWorkspace() fails clearly when used outside its provider.

Event Creation

useCreateEvent() wraps api.events.store.mutationOptions(). It adds the active organization ID, success feedback, structured logging, and API error parsing. useCreateEventForm() maps field errors back into React Hook Form.

This split keeps request state in TanStack Query, form errors in the form, and interaction state in the component hook. The app does not perform an optimistic event-list update because there is no event-list endpoint to invalidate.

Route State

Use TanStack Router search parameters when state affects a URL or must survive navigation. Current uses are security-sensitive rather than generic filters:

tsx
export const Route = createFileRoute('/(public)/sign-in')({
  validateSearch: z.object({
    redirect: z
      .string()
      .regex(/^\/(?![\\/])/)
      .catch('/dashboard'),
  }),
  component: SignInPage,
})

The authenticated guard records the requested internal URL. The sign-in route validates and reads it, then restores it after authentication. The reset-password route validates and reads its token in the same route-owned way. See Routing for route boundaries and loaders.

Form State

Forms use schemas from src/schemas/ with zodResolver. The form owns field values, touched and validation state, submission wiring, and server field errors.

The event form is a representative composition. useCreateEventForm():

  • initializes nested event, address, guest-count, and budget values;
  • watches required fields to determine whether submission is available;
  • clears customCategory when the selected category is no longer other;
  • keeps calendar visibility in local state;
  • converts the selected date to an ISO string and normalizes an empty address before mutation;
  • maps API field errors back to their form controls.

Sign-in follows the same ownership rule. SignInForm owns credential fields and submits through useAuth(). When the user selects remember me, the auth hook stores only the email in sessionStorage; the form restores it on a later visit in the same tab. The password and Better Auth session are not stored there.

Local Interaction State

Keep transient interaction state beside the component that renders it. Current examples include:

  • MobileHeaderControls controls whether the navigation sheet is open.
  • use-notification-bell.ts controls the notification popover.
  • use-workspace-switcher.ts controls the switcher, create, and settings dialogs.
  • useCreateEventForm() controls the calendar popover.

Promote local state only when another owner genuinely needs to coordinate it.

Theme State

src/main.tsx configures next-themes with defaultTheme="system", class-based activation, and the theme storage key. With no saved preference, the resolved theme follows the operating system. An explicit header selection stores light or dark in localStorage.

Before React loads, the inline script in index.html initializes the same contract. It reads the theme key, removes values other than light or dark, and falls back to prefers-color-scheme when storage is empty or unavailable. It then applies the initial .dark class and browser color-scheme to the document root. This keeps the first paint aligned with the resolved theme instead of waiting for next-themes to mount.

Keep the storage key, accepted values, root class, and system fallback synchronized between index.html and the ThemeProvider in src/main.tsx. The inline script must remain small and run before the application entrypoint so it can prevent a startup theme flash.

src/routes/__root.tsx reads resolvedTheme and passes a concrete theme to the shared toaster. The UI package supplies light and dark tokens but does not control this app state. Styling details live in UI Components Package.

Request Errors

Use src/utils/api-errors.ts and src/utils/form-errors.ts to parse server responses, separate field errors, and map them to forms or toasts. Let one layer show feedback for a request. App mutation hooks normally handle single-action feedback; a component coordinating several steps should suppress inner feedback and handle the combined failure once.

Testing State Behavior

Tests sit beside the hooks and components they cover. Existing tests verify auth cache behavior, remembered email handling, organization wrappers, workspace forms and switching, event form mapping, notification state, and route-related component behavior. Build test providers with a fresh Query Client so cached data and mutation state do not leak between tests.

Built with ❤️ by the Jubiloop team