State Management
Relevant source files
The following files were used as context for generating this wiki page:The Umami application employs a dual-strategy approach for state management, clearly separating global UI state from server state. This separation is achieved by leveraging two key libraries: Zustand for managing client-side state and TanStack React Query for handling data fetching, caching, and synchronization with the server.
This architecture provides a clean separation of concerns. Zustand offers a minimalistic API for managing shared state across components, such as user preferences and application settings. React Query excels at managing the lifecycle of server data, simplifying asynchronous operations and improving UI responsiveness through caching and automatic refetching.
Core Technologies
The state management architecture is built upon the following libraries, as defined in the project's dependencies.
| Library | Version | Purpose |
|---|---|---|
zustand | ^5.0.9 | Global client-side state management. |
@tanstack/react-query | ^5.90.11 | Server state management, caching, and fetching. |
Sources: package.json:80-81, 110
Global Application State (Zustand)
Global UI and application-level state that needs to be shared across the entire component tree is managed by Zustand. The primary store for this is the app store.
Sources: src/store/app.ts
The app Store
The app store, defined in src/store/app.ts, is responsible for holding application-wide state that is not directly tied to server data. This includes user preferences like theme and language, session information like the current user and share token, and UI state like the selected date range.
Initial State
The store is initialized with values derived from localStorage, environment variables, or predefined constants. This allows user preferences to persist across sessions.
The following diagram illustrates the logic for determining the initial value for a state property, using locale as an example.
State Properties and Setters
The app store exposes its state through the useApp hook and provides specific setter functions to modify the state.
| Property | Initial Value Logic | Setter Function | Description |
|---|---|---|---|
locale | getItem(LOCALE_CONFIG) or process.env.defaultLocale or DEFAULT_LOCALE | setLocale(locale) | The application's current language locale. |
theme | getItem(THEME_CONFIG) or DEFAULT_THEME | (Not exposed directly) | The current UI theme (e.g., 'light', 'dark'). |
timezone | getItem(TIMEZONE_CONFIG) or getTimezone() | setTimezone(timezone) | The user's selected or detected timezone. |
dateRangeValue | getItem(DATE_RANGE_CONFIG) or DEFAULT_DATE_RANGE_VALUE | setDateRangeValue(value) | The default date range selected in the UI. |
shareToken | null | setShareToken(token) | The active share token for viewing shared pages. |
user | null | setUser(user) | The currently authenticated user object. |
config | null | setConfig(config) | Application configuration data. |
Sources: src/store/app.ts:12-42, src/lib/constants.ts:2-8
Server State and Data Fetching (React Query)
Server state, which includes all data fetched from the backend API, is managed by TanStack React Query. The application abstracts data-fetching logic into custom hooks, promoting reusability and a clean component architecture. This pattern is evident throughout the src/components/hooks/queries directory.
Sources: src/components/hooks/index.ts:8-57, package.json:81
Custom Query Hooks
A standard pattern in the codebase is to create a dedicated hook for each API endpoint. The useWebsiteQuery hook is a representative example of this pattern.
useWebsiteQuery
This hook is responsible for fetching data for a single website.
- File:
src/components/hooks/queries/useWebsiteQuery.ts - Query Key:
['website', websiteId] - API Endpoint:
/api/websites/${websiteId} - Functionality: It uses the
useApihook (an abstraction overfetch) to retrieve website details. ThewebsiteIdis used to form a unique query key, enabling React Query to cache the data effectively. The hook is enabled only when awebsiteIdis provided.
The following sequence diagram shows the data flow when a component uses useWebsiteQuery.
This pattern of creating specific hooks for queries (useUsersQuery, useTeamsQuery, useWebsiteStatsQuery, etc.) is used extensively, providing a consistent and robust way to interact with server data.
Sources: src/components/hooks/index.ts:8-57
State Providers and Initialization
The root of the application's React tree is wrapped with providers that make the state management systems available to all child components. This setup is configured in Providers.tsx.
Sources: src/app/Providers.tsx, src/app/layout.tsx:38-40
Providers.tsx
This component is responsible for setting up the context for React Query.
QueryClientCreation: A new instance ofQueryClientis created to manage the query cache.QueryClientProvider: The entire application is wrapped inQueryClientProvider, passing thequeryClientinstance to it. This makes React Query's hooks and cache accessible throughout the component tree.
// src/app/Providers.tsx:7-14
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useMemo } from 'react';
export function Providers({ children }) {
const queryClient = useMemo(() => new QueryClient(), []);
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
Sources: src/app/Providers.tsx:7-14
The Providers component is then used in the root layout to wrap the main application content, ensuring state management is available everywhere.
Sources: src/app/layout.tsx:39
Conclusion
The state management in Umami is thoughtfully designed with a clear separation between client and server concerns. Zustand provides a lightweight and straightforward solution for global UI state, such as themes and user settings, with persistent state loaded from localStorage. For all asynchronous data operations, React Query is used via a consistent pattern of custom hooks, which simplifies data fetching, caching, and synchronization, leading to a more resilient and performant user experience. This dual-library strategy leverages the strengths of each tool to create a scalable and maintainable state architecture.