Repository navigation
Frontend State Management
Canonical source:
docs/05-frontend/state-management-guide.md— this page is rendered from it on every publish; edit the source.
How ClawAI manages server state, client state, and component state.
ClawAI uses three distinct state management approaches, each for a specific category of state:
| Category | Tool | Purpose | Persistence |
|---|---|---|---|
| Server State | TanStack Query | Data from the backend API | Cache with TTL |
| Client State | Zustand | App-level client-only state | localStorage |
| Component State | React Hooks | Ephemeral UI state (forms, toggles) | None (in-memory) |
Decision rule: If the data comes from the backend, use TanStack Query. If it is app-wide client state, use Zustand. If it is local to a single component, use React hooks.
TanStack Query is configured in src/app/providers.tsx:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000, // Data considered fresh for 5 seconds
refetchInterval: 10 * 1000, // Refetch every 10 seconds
refetchOnWindowFocus: true, // Refetch when user returns to tab
refetchOnReconnect: true, // Refetch when network reconnects
retry: 1, // Retry failed requests once
},
},
});The provider hierarchy is:
LocaleProvider > ThemeProvider > QueryClientProvider > children + Toaster
All query keys are centralized in src/repositories/shared/query-keys.ts:
export const queryKeys = {
auth: { me: ['auth', 'me'] },
threads: { all, lists, list, detail, messages, messagesInfinite, listInfinite },
connectors: { all, lists, list, detail, models },
models: { all },
routing: { config, policies: { all, lists, list }, decisions: { all, byThread } },
localModels: { all, lists },
runtimes: { all },
clientLogs: { all, lists, list, stats },
serverLogs: { all, lists, list, stats },
audits: { all, lists, list, stats },
usage: { all, lists, list, summary, cost, latency },
admin: { users },
memory: { all, lists, list, detail },
contextPacks: { all, lists, detail },
files: { all, lists, list, detail, chunks },
health: { all, aggregated },
dashboard: { all, stats },
catalog: { all, lists, list, detail },
pullJobs: { all },
};-
Hierarchical:
queryKeys.threads.allinvalidates everything thread-related -
Granular:
queryKeys.threads.detail(id)invalidates only one thread -
Filters as keys:
queryKeys.threads.list(filters)-- different filters = different cache entries - Infinite queries: Separate keys for paginated vs infinite scroll variants
All GET requests are wrapped in custom hooks:
// src/hooks/chat/use-threads.ts
export function useThreads(filters: ThreadFilters) {
return useQuery({
queryKey: queryKeys.threads.list(filters),
queryFn: () => chatRepository.listThreads(filters),
});
}Rules:
- Never call
useQuerydirectly in a TSX file - Always wrap in a custom hook in
src/hooks/<domain>/ - Always use the query key factory
All mutations invalidate relevant caches on success:
// src/hooks/chat/use-create-thread.ts
export function useCreateThread() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (payload: CreateThreadPayload) =>
chatRepository.createThread(payload),
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: queryKeys.threads.lists(),
});
},
onError: (error: Error) => {
showToast.apiError(error, t('chat.createFailed'));
},
});
return { createThread: mutation.mutate, isPending: mutation.isPending };
}Rules:
- Always invalidate affected queries in
onSuccess - Use
voidprefix when callinginvalidateQueries(it returns a Promise) - Show user feedback via
showToastinonError - Prefer
invalidateQueriesover manualrefetch
For virtualized scrolling (thread list, message list):
export function useVirtualizedThreads(filters: ThreadFilters) {
return useInfiniteQuery({
queryKey: queryKeys.threads.listInfinite(filters),
queryFn: ({ pageParam }) =>
chatRepository.listThreads({ ...filters, cursor: pageParam }),
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
initialPageParam: undefined,
});
}For real-time data updates (e.g., waiting for AI responses):
useQuery({
queryKey: queryKeys.threads.messages(threadId),
queryFn: () => chatRepository.listMessages(threadId),
refetchInterval: isWaitingForResponse ? 1000 : 10000, // Poll every 1s while waiting
});Zustand is used for minimal client-only state. Three stores exist:
Stores authentication tokens and user profile. Persisted to localStorage under claw-auth-storage.
export const useAuthStore = create<AuthStoreState & AuthStoreActions>()(
persist(
(set) => ({
accessToken: null,
refreshToken: null,
user: null,
isAuthenticated: false,
setAuth: ({ accessToken, refreshToken, user }) =>
set({ accessToken, refreshToken, user, isAuthenticated: true }),
setUser: (user) => set({ user }),
setTokens: ({ accessToken, refreshToken }) =>
set({ accessToken, refreshToken }),
clearAuth: () => set(AUTH_INITIAL_STATE),
}),
{
name: 'claw-auth-storage',
partialize: (state) => ({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
user: state.user,
isAuthenticated: state.isAuthenticated,
}),
},
),
);Usage:
const user = useAuthStore((state) => state.user);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const clearAuth = useAuthStore((state) => state.clearAuth);Stores sidebar expanded/collapsed state. Persisted to localStorage.
Stores log page filter state (level, service, date range). Not persisted.
- Minimal usage -- only for true client-only state
- No server data -- never cache API responses in Zustand
-
Types in
src/types/store.types.ts-- not inline -
Initial state in
src/constants/-- not inline -
Selectors -- always use selector pattern to avoid unnecessary re-renders:
// CORRECT -- only re-renders when user changes const user = useAuthStore((state) => state.user); // WRONG -- re-renders on any store change const { user } = useAuthStore();
Used for ephemeral UI state that does not need to survive page navigation:
// Form state
const [search, setSearch] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
// Derived/computed state
const filteredItems = useMemo(
() => items.filter((item) => item.name.includes(search)),
[items, search],
);
// Callbacks
const handleSubmit = useCallback(() => {
createItem(formState);
setIsModalOpen(false);
}, [createItem, formState]);Rules:
- All React hooks must be inside controller hooks in
src/hooks/, never in TSX files - No prop drilling beyond 2 levels
- Use
useMemofor expensive computations - Use
useCallbackfor callbacks passed to child components
| Question | Answer | Use |
|---|---|---|
| Does it come from the backend API? | Yes | TanStack Query |
| Does it need to survive page navigation? | Yes | Zustand |
| Is it shared across multiple components? | Yes | Zustand or Context |
| Is it local to one component tree? | Yes | React hooks |
| Is it a form value? | Yes | React hooks |
| Is it derived from other state? | Yes | useMemo |
| Does it need to persist across sessions? | Yes | Zustand + persist |
Browser localStorage
|
v
Zustand (auth tokens, preferences)
|
v
HTTP Client (axios) <-- attaches Bearer token from Zustand
|
v
TanStack Query Cache <-- manages cache, refetch, invalidation
|
v
Controller Hook <-- composes query hooks + mutation hooks + local state
|
v
Page Component <-- receives all data via controller hook return value
|
v
Child Components <-- receive data via props only
ClawAI Wiki · source-controlled under wiki/ · generated/mirrored from the current repository knowledge layer.
ClawAI Wiki
Product
- Product-Vision
- Flagship-Features
- Business-Overview
- Requirements-Register
- Drift-Log
- Runtime-Managed-Connector-Providers
- Threads
- ADR-Index
Architecture
- System-at-a-Glance
- Architecture-Map
- System-Architecture
- Message-Flow
- End-to-End-Data-Flow
- Routing-Engine
- Event-Bus
- Security-Architecture
Platform
- Backend-Services
- Frontend-Architecture
- Shared-Packages
- Database-Reference
- API-Reference
- Provider-Catalog
- Memory-and-Context-Architecture
- File-Support-Architecture
- Research-and-Web-Crawling
Runtime & Ops
- Docker-Guide
- Nginx-Reference
- CI-CD-Pipeline
- Logging-and-Observability
- Operational-Runbooks
- Troubleshooting
Quality
Engineering OS
- AI-Native-Engineering-OS
- Generated-Manifests
- Rules-Catalog
- Skills-Catalog
- Context-Layer
- Engineering-Memory
- Tooling-and-Automation
- Scripts-and-Operations
- ESLint-and-Architecture-Enforcement
- Extended-Work-Skills
Agents
Reference