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 theme value immediately for UI rendering without loading states.
  • TanStack Query supplies source data with isLoading indicators for skeleton screens.
  • After mutating data, queryClient.invalidateQueries forces 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: useSidebarStore determines sidebar layout instantly while useQuery(['notebooks']) populates the content area asynchronously.
  • Optimistic UI: useAuthStore toggles local loading indicators while useMutation processes authentication requests, with onSuccess callbacks invalidating the ['auth'] query key.
  • Global UI Toggles: Theme changes via useThemeStore write immediately to localStorage through Zustand's persist middleware, requiring no server coordination or query invalidation.
  • Data-Driven Invalidation: After mutating notes in NoteEditorDialog.tsx, the component calls queryClient.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 to localStorage.
  • TanStack Query handles all asynchronous server data fetching, caching, and invalidation for notebooks, sources, and notes via useQuery and useMutation hooks.
  • Components such as SourceDetailContent.tsx consume 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →