# How Zustand and TanStack Query Integrate in the Open Notebook Next.js Frontend

> Learn how Zustand and TanStack Query integrate in the Open Notebook Next.js frontend to manage UI state and server data separately, improving performance and developer experience.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-15

---

**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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/sidebar-store.ts), [`auth-store.ts`](https://github.com/lfnovo/open-notebook/blob/main/auth-store.ts), and [`navigation-store.ts`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/SourceDetailContent.tsx) component demonstrates this integration by reading theme preferences from Zustand while fetching source details via TanStack Query.

```tsx
// 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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/stores/theme-store.ts) |
| Sidebar state management | [`frontend/src/lib/stores/sidebar-store.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/stores/sidebar-store.ts) |
| Authentication state | [`frontend/src/lib/stores/auth-store.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/stores/auth-store.ts) |
| Source detail integration | [`frontend/src/components/source/SourceDetailContent.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/components/source/SourceDetailContent.tsx) |
| Podcast generation dialog | [`frontend/src/components/podcasts/GeneratePodcastDialog.tsx`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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`](https://github.com/lfnovo/open-notebook/blob/main/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.