Skip to Content
ContributeFrontendState Management

State Management

This document explains the state management approach used in the Rhesis frontend application.

State Management Architecture

The frontend uses:

  1. React Context: For global/ambient state shared across components
  2. Local Component State: For component-specific state
  3. Server-side fetching: For entity-detail pages, via a server-only API client factory
  4. Client-side fetching: For interactive list/filter views, via useState/useEffect and the BFF-proxied API client
  5. @tanstack/react-query: For mutations in some feature hooks

There is no Redux/Zustand store and no "use server" Server Actions in this codebase — mutations go through client-side calls to typed API client classes.

React Context

Global state lives in src/contexts/: ActiveProjectContext, FeaturesContext, NavigationItemsContext, OnboardingContext, OrganizationContext, OrgSettingsContext, PermissionsContext, WebSocketContext.

Theme Context

src/components/providers/ThemeProvider.tsx wraps MUI’s own ThemeProvider and exposes a ColorModeContext (not a generic useTheme() hook):

src/components/providers/ThemeProvider.tsx
export const ColorModeContext = React.createContext({
toggleColorMode: () => {},
mode: 'light' as 'light' | 'dark',
});

export default function ThemeContextProvider({ children }: { children: React.ReactNode }) {
const [mode, setMode] = React.useState<'light' | 'dark'>('light');
// ...reads localStorage + a "theme-mode" cookie on mount, falls back to
// prefers-color-scheme, and sets document.documentElement's data-theme-mode attribute
const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]);

return (
    <ColorModeContext.Provider value={{ mode, toggleColorMode: /* ... */ () => {} }}>
      <MuiThemeProvider theme={theme}>{children}</MuiThemeProvider>
    </ColorModeContext.Provider>
);
}

Consumers read it directly:

Consumer
import { useContext } from 'react';
import { ColorModeContext } from '@/components/providers/ThemeProvider';

const { mode, toggleColorMode } = useContext(ColorModeContext);

The cookie (not just localStorage) lets the server read the theme mode too. Mode is binary ('light' | 'dark') — there’s no separate 'system' value, though the initial mode falls back to the OS preference via prefers-color-scheme when nothing is stored yet.

Server-Side Fetching (Detail Pages)

Entity-detail pages fetch server-side using createServerApiFactory(), which attaches the session token — this only runs in Server Components, Route Handlers, and other server-only code:

app/(protected)/projects/[identifier]/page.tsx
import { createServerApiFactory } from '@/utils/api-client/server-factory';

export default async function ProjectPage({
params,
}: {
params: Promise<{ identifier: string }>;
}) {
const { identifier } = await params;
const apiFactory = await createServerApiFactory();
const project = await apiFactory.getProjectsClient().getProject(identifier);

return <ProjectDetails project={project} />;
}

Client-Side Fetching (List/Filter Views)

List pages are typically thin Server Components that render a client wrapper, which does its own fetching so pagination/filtering don’t require a full page reload:

app/(protected)/projects/page.tsx
// Server Component: only an auth check, no data fetching
export default async function ProjectsPage() {
await auth();
return <ProjectsClientWrapper />;
}
ProjectsClientWrapper.tsx
'use client';

import { useState, useEffect, useCallback } from 'react';
import { ApiClientFactory } from '@/utils/api-client/client-factory';

export default function ProjectsClientWrapper() {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(25);
const [projects, setProjects] = useState([]);

const fetchProjects = useCallback(async () => {
    // No token passed — the BFF proxy injects auth server-side.
    const factory = new ApiClientFactory();
    const { data } = await factory.getProjectsClient().getProjects({ page, pageSize: rowsPerPage });
    setProjects(data);
}, [page, rowsPerPage]);

useEffect(() => { fetchProjects(); }, [fetchProjects]);

return <BaseDataGrid rows={projects} columns={columns} /* ...pagination props */ />;
}

Never pass a session token from client code. ApiClientFactory must be instantiated with no arguments in client components/hooks — buildAuthHeaders() only attaches Authorization when running server-side, so a client-side token is silently dropped. Gate on auth state with isAuthenticated(status) / useIsAuthenticated(), not on token presence. See the BFF Auth Pattern in apps/frontend/AGENTS.md.

Pagination for grid views is generally driven by local component state feeding MUI X Data Grid, not URL search params — though useSearchParams/usePathname are used elsewhere for shareable filter state.

Local Component State

For component-specific state, use React’s built-in hooks (useState, useReducer) — no additional library is needed.

Mutations with React Query

Some feature hooks wrap mutations with @tanstack/react-query:

src/hooks/useTasks.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ApiClientFactory } from '@/utils/api-client/client-factory';

export function useTasks() {
const queryClient = useQueryClient();
const createTask = useMutation({
    mutationFn: (data) => new ApiClientFactory().getTasksClient().createTask(data),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }),
});
// ...updateTask, deleteTask, getTask follow the same pattern
return { createTask: createTask.mutateAsync /* , updateTask, deleteTask, getTask */ };
}

Note this hook exposes mutation functions, not a cached task list — callers that need a list fetch it separately.

State Management Best Practices

  1. Minimize client-side state: prefer server-side fetching for detail/read-only pages
  2. Never prop-drill a session token into client components — always go through the BFF (ApiClientFactory() with no args)
  3. Context for global state: use React Context for theme, active project, permissions, and similar cross-cutting concerns
  4. Colocate local state: keep component state as close as possible to where it’s used
  5. Gate on auth status, not token presence — isAuthenticated(status) / useIsAuthenticated()