Appearance
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
| State | Owner | Current examples |
|---|---|---|
| Server and auth data | TanStack Query | Session, organizations, active organization, health check, event mutation |
| Route state | TanStack Router | Safe post-sign-in redirect, reset-password token |
| Form state | React Hook Form and Zod | Sign-in, workspace, account, and event forms |
| Provider-scoped derived state | React context backed by Query hooks | Active workspace and workspace list |
| Component interaction | Local React state | Mobile sheet, notification popover, dialogs, calendar |
| Theme preference | next-themes | System default and explicit light/dark selection |
| Remembered sign-in email | sessionStorage | Email 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:
authfromsrc/lib/api/setup.tsfor Better Auth sessions and organization operations.apifromsrc/lib/api/tuyau.tsfor 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
customCategorywhen the selected category is no longerother; - 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:
MobileHeaderControlscontrols whether the navigation sheet is open.use-notification-bell.tscontrols the notification popover.use-workspace-switcher.tscontrols 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.