How Zustand and TanStack Query Integrate in the Open Notebook Next.js Frontend
The Open Notebook Next.js frontend uses Zustand for synchronous UI state (themes, sidebars, and authentication) and TanStack Query for asynchronous server data (notebooks, sources, and notes), with components consuming both libraries to separate immediate user interactions from remote data fetching and caching.
The lfnovo/open-notebook repository implements a clean state management architecture in its Next.js frontend. By leveraging Zustand for client-side UI state and TanStack Query for server-side data, the codebase maintains clear separation between transient interface elements and persistent backend content.
Architectural Separation of Concerns
The frontend distinctly partitions state management responsibilities based on data origin and lifecycle characteristics.
Zustand for Client-Side UI State
Zustand manages ephemeral UI state that requires immediate, synchronous updates without network latency. In frontend/src/lib/stores/theme-store.ts, the store handles theme preferences with the persist middleware to survive page reloads via localStorage. Similarly, sidebar-store.ts, auth-store.ts, and navigation-store.ts manage sidebar visibility, authentication status, and navigation history entirely within the browser.
These stores expose simple hooks like useThemeStore that components import directly, providing instantaneous access to values such as the current theme mode or sidebar collapse state.
TanStack Query for Server Data Management
TanStack Query (@tanstack/react-query) handles all asynchronous data fetching from the FastAPI backend. It manages notebooks, sources, notes, and podcasts through cached query keys, automatic background refetching, and stale-while-revalidate semantics. The library coordinates request lifecycles via the QueryClient, handling loading states, error retries, and optimistic updates without requiring manual state management.
Components interact with server data through hooks like useQuery for reads and useMutation for writes, while the queryClient provides imperative cache invalidation methods.
Integration Patterns in Components
Components combine both libraries to handle UI feedback and data operations simultaneously. The SourceDetailContent.tsx component demonstrates this integration by reading theme preferences from Zustand while fetching source details via TanStack Query.
// frontend/src/components/source/SourceDetailContent.tsx
import { useQueryClient } from '@tanstack/react-query';
import { useThemeStore } from '@/lib/stores/theme-store';
import { useSource } from '@/lib/api/source';
export function SourceDetailContent({ sourceId }: { sourceId: string }) {
// 1️⃣ UI state from Zustand (instant, synchronous)
const theme = useThemeStore(state => state.theme);
// 2️⃣ Remote data via TanStack Query (cached, asynchronous)
const { data: source, isLoading } = useSource(sourceId);
// 3️⃣ QueryClient for cache invalidation
const queryClient = useQueryClient();
const updateSource = async (updates) => {
await api.updateSource(sourceId, updates);
// Invalidate the specific query to trigger refetch
queryClient.invalidateQueries(['source', sourceId]);
};
return (
// Render logic uses `theme` for styling and `source` for content
);
}
Key integration points:
- Zustand provides the
themevalue immediately for UI rendering without loading states. - TanStack Query supplies
sourcedata withisLoadingindicators for skeleton screens. - After mutating data,
queryClient.invalidateQueriesforces a refresh of the specific query key, ensuring the UI reflects the latest server state.
Common Patterns Across the Codebase
The repository employs consistent patterns when combining these libraries:
- Initial Page Load:
useSidebarStoredetermines sidebar layout instantly whileuseQuery(['notebooks'])populates the content area asynchronously. - Optimistic UI:
useAuthStoretoggles local loading indicators whileuseMutationprocesses authentication requests, withonSuccesscallbacks invalidating the['auth']query key. - Global UI Toggles: Theme changes via
useThemeStorewrite immediately tolocalStoragethrough Zustand'spersistmiddleware, requiring no server coordination or query invalidation. - Data-Driven Invalidation: After mutating notes in
NoteEditorDialog.tsx, the component callsqueryClient.invalidateQueries(['notes', notebookId])to refresh the notebook's content without manual state updates.
Where to Find the Implementation
| Responsibility | File Path |
|---|---|
| Theme store with persistence | frontend/src/lib/stores/theme-store.ts |
| Sidebar state management | frontend/src/lib/stores/sidebar-store.ts |
| Authentication state | frontend/src/lib/stores/auth-store.ts |
| Source detail integration | frontend/src/components/source/SourceDetailContent.tsx |
| Podcast generation dialog | frontend/src/components/podcasts/GeneratePodcastDialog.tsx |
| Note editor with mutations | frontend/src/app/(dashboard)/notebooks/components/NoteEditorDialog.tsx |
Summary
- Zustand manages synchronous UI state including themes, sidebars, and navigation in
frontend/src/lib/stores/, persisting values like theme preferences tolocalStorage. - TanStack Query handles all asynchronous server data fetching, caching, and invalidation for notebooks, sources, and notes via
useQueryanduseMutationhooks. - Components such as
SourceDetailContent.tsxconsume both libraries simultaneously, using Zustand for instantaneous UI feedback and TanStack Query for robust server data synchronization. - Mutations trigger cache invalidation through
queryClient.invalidateQueries, ensuring stale data is automatically refreshed without manual state management.
Frequently Asked Questions
Why does the codebase use both Zustand and TanStack Query instead of just one state management library?
Zustand excels at managing simple, synchronous UI state that must update instantly without network latency, such as toggling dark mode or collapsing a sidebar. TanStack Query is optimized for asynchronous server data with built-in caching, automatic refetching, and request deduplication. Using both allows each library to handle the specific concerns it solves best, preventing the UI blocking that would occur if remote data fetching controlled local interface state.
How does the theme store persist user preferences across browser sessions?
The theme store in frontend/src/lib/stores/theme-store.ts uses Zustand's persist middleware to serialize the current theme value to localStorage whenever it changes. On subsequent page loads, the middleware rehydrates the store from localStorage before the component renders, ensuring the user's preference applies immediately without flashing the default theme.
What triggers a refetch of server data after a mutation completes?
After successful mutations, components call queryClient.invalidateQueries(['queryKey']) with the specific query key used for that data. This marks the cached entry as stale, causing TanStack Query to automatically refetch the data in the background. For example, updating a source invalidates ['source', sourceId], ensuring the SourceDetailContent component displays the fresh data.
Can Zustand stores access TanStack Query data directly?
No, the architecture maintains a strict separation where Zustand stores do not import or access TanStack Query hooks. UI state in Zustand remains independent of server data, and components serve as the integration layer by reading from both sources. This prevents circular dependencies and ensures Zustand stores remain lightweight and synchronous while TanStack Query handles all network complexity.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →