# How Frontend-Backend Communication Works Using Zustand and TanStack Query in Open Notebook

> Discover how Open Notebook syncs its React frontend with a FastAPI backend using Zustand for UI state and TanStack Query for server state, ensuring automatic cache invalidation.

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

---

**Open Notebook uses Zustand for synchronous UI state and TanStack Query for asynchronous server state, with automatic cache invalidation keeping the React frontend synchronized with the FastAPI backend.**

The `lfnovo/open-notebook` repository implements a modern React architecture that cleanly separates local interface concerns from remote data management. The frontend leverages **Zustand** to handle persistent UI preferences like themes and navigation states, while **TanStack Query** manages all HTTP communication with the backend API. This dual-store pattern eliminates prop drilling and ensures components re-render automatically when either local settings change or server data updates.

## Architecture Overview

Open Notebook’s frontend architecture divides state into two distinct layers:

1. **Zustand (Client State)** – Handles synchronous, browser-specific UI concerns such as theme selection, sidebar collapse states, and navigation shortcuts. These stores persist to `localStorage` using the `persist` middleware.

2. **TanStack Query (Server State)** – Manages asynchronous data fetching, caching, and background synchronization with the FastAPI backend. It provides declarative hooks for GET, POST, PUT, and DELETE operations.

Both systems are initialized at the root of the application through provider components (`QueryProvider`, `ThemeProvider`) that wrap the React tree, making cached data and UI preferences available throughout the component hierarchy.

## Setting Up the Query Client

### QueryClient Configuration

The global query client is instantiated in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts) with standardized cache policies and retry logic:

```typescript
// src/lib/api/query-client.ts
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 10 * 60 * 1000,   // 10 minutes
      retry: 2,
      refetchOnWindowFocus: false,
    },
    mutations: { retry: 1 },
  },
})

```

This configuration ensures that data remains fresh for five minutes before background refetching and garbage collects unused cache entries after ten minutes.

### Centralized Query Keys

To prevent cache invalidation errors, the file exports a `QUERY_KEYS` object that standardizes key construction across the application:

```typescript
export const QUERY_KEYS = {
  notebooks: ['notebooks'] as const,
  notebook: (id: string) => ['notebooks', id] as const,
  // … other keys
}

```

The `QueryProvider` component in [`frontend/src/components/providers/QueryProvider.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/components/providers/QueryProvider.tsx) then supplies this client to the React tree:

```tsx
// src/components/providers/QueryProvider.tsx
'use client'

import { QueryClientProvider } from '@tanstack/react-query'
import { queryClient } from '@/lib/api/query-client'

export function QueryProvider({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  )
}

```

## Fetching and Mutating Server Data

### Using useQuery for Data Fetching

Domain-specific hooks wrap TanStack Query’s `useQuery` to fetch data from the FastAPI backend. For example, `useNotebooks` in [`frontend/src/lib/hooks/use-notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/hooks/use-notebooks.ts) retrieves notebook lists:

```tsx
// src/lib/hooks/use-notebooks.ts
import { useQuery } from '@tanstack/react-query'
import { notebooksApi } from '@/lib/api/notebooks'
import { QUERY_KEYS } from '@/lib/api/query-client'

export function useNotebooks(archived?: boolean) {
  return useQuery({
    queryKey: [...QUERY_KEYS.notebooks, { archived }],
    queryFn: () => notebooksApi.list({ archived, order_by: 'updated desc' }),
  })
}

```

This hook returns `{ data, isLoading, error }` and automatically subscribes the component to the query cache. Multiple components calling `useNotebooks()` share the same cached data, eliminating redundant HTTP requests.

### Using useMutation for Updates

Mutations handle create, update, and delete operations. The `useCreateNotebook` hook demonstrates how to invalidate related queries after a successful mutation:

```tsx
export function useCreateNotebook() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: (data) => notebooksApi.create(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.notebooks })
    },
  })
}

```

When `invalidateQueries` executes, TanStack Query automatically refetches the notebook list, ensuring all components displaying notebooks receive the updated dataset without manual refresh.

## Managing Local UI State with Zustand

### Persistent Theme Store Example

Zustand stores manage UI state that never needs to reach the server. 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) demonstrates the `persist` middleware for cross-session storage:

```typescript
// src/lib/stores/theme-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

export type Theme = 'light' | 'dark' | 'system'

interface ThemeState {
  theme: Theme
  setTheme: (theme: Theme) => void
}

export const useThemeStore = create<ThemeState>()(
  persist(
    (set, get) => ({
      theme: 'system',
      setTheme: (theme) => {
        set({ theme })
        // Immediately apply to document
        if (typeof window !== 'undefined') {
          const root = window.document.documentElement
          const effective = theme === 'system' ? get().getSystemTheme() : theme
          root.classList.remove('light', 'dark')
          root.classList.add(effective)
          root.setAttribute('data-theme', effective)
        }
      },
      // … helpers omitted for brevity
    }),
    { name: 'theme-storage', partialize: (s) => ({ theme: s.theme }) }
  )
)

```

Components consume this state via a simple hook, receiving instantaneous updates without asynchronous overhead:

```tsx
import { useTheme } from '@/lib/stores/theme-store'

function ThemeToggle() {
  const { theme, setTheme, isDark } = useTheme()
  return (
    <button onClick={() => setTheme(isDark ? 'light' : 'dark')}>
      Switch to {isDark ? 'light' : 'dark'} mode
    </button>
  )
}

```

### Other UI State Stores

The repository contains additional Zustand stores located in `frontend/src/lib/stores/`:

- **navigation-store.ts** – Tracks "return-to" navigation states and history shortcuts
- **sidebar-store.ts** – Manages sidebar collapse/expand state
- **auth-store.ts** – Stores authentication flags and session tokens

Each uses the `persist` middleware to survive page reloads, ensuring user preferences remain intact across sessions.

## Integrating Both State Systems

Real components combine both libraries. The `NotebookList` component in `frontend/src/app/(dashboard)/notebooks/components/NotebookList.tsx` demonstrates this integration:

```tsx
// src/app/(dashboard)/notebooks/components/NotebookList.tsx
import { useNotebooks } from '@/lib/hooks/use-notebooks'
import { useTheme } from '@/lib/stores/theme-store'

export default function NotebookList() {
  const { data: notebooks, isLoading } = useNotebooks()
  const { theme, toggleTheme } = useTheme()

  if (isLoading) return <div>Loading…</div>

  return (
    <section className={theme}>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <ul>
        {notebooks?.map((nb) => (
          <li key={nb.id}>{nb.title}</li>
        ))}
      </ul>
    </section>
  )
}

```

**Execution flow:**
1. `useNotebooks` triggers a GET request to `/notebooks` via `notebooksApi.list()` and caches the result under `QUERY_KEYS.notebooks`
2. `useTheme` reads the current theme from the Zustand store synchronously
3. When the user toggles the theme, the Zustand store updates instantly—no network request occurs
4. When a mutation elsewhere creates a notebook, `invalidateQueries` triggers a background refetch, and `NotebookList` re-renders with fresh data

## Summary

- **Zustand** handles synchronous UI state (themes, navigation, sidebar) and persists it to `localStorage` via the `persist` middleware
- **TanStack Query** manages asynchronous server state with automatic caching, background refetching, and query deduplication
- **Cache invalidation** occurs through `queryClient.invalidateQueries()` in mutation `onSuccess` callbacks, ensuring UI consistency
- **Centralized query keys** in [`query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/query-client.ts) prevent typos and ensure consistent cache management across the application
- **Provider composition** in the root layout wraps the app with both `QueryClientProvider` and Zustand context providers

## Frequently Asked Questions

### How does TanStack Query know when to refetch data?

TanStack Query refetches data when query keys are invalidated using `queryClient.invalidateQueries()`. In Open Notebook, mutation hooks call this method in their `onSuccess` callbacks, automatically triggering refetches for any component subscribed to those query keys. The library also supports background refetching when the window regains focus or network reconnects, though `refetchOnWindowFocus` is disabled in this codebase.

### Why use Zustand instead of React Context for UI state?

Zustand provides better performance for high-frequency updates and eliminates the boilerplate associated with Context providers. The `persist` middleware also handles `localStorage` serialization automatically, whereas React Context requires manual effect hooks to achieve persistence. Additionally, Zustand's atomic selectors prevent unnecessary re-renders when unrelated store properties change.

### Where is the API client configured for TanStack Query?

The API client logic resides in `frontend/src/lib/api/` files such as [`notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/notebooks.ts). These files export thin Axios wrappers (named like `notebooksApi`) that perform the actual HTTP requests. The TanStack Query hooks import these wrappers and pass them to `queryFn` parameters, keeping data fetching logic separate from caching and state management concerns.

### Can Zustand and TanStack Query share state directly?

No, the two libraries maintain separate state containers. However, they interoperate through React's component lifecycle: components read Zustand values for immediate UI rendering (like themes) while TanStack Query manages asynchronous data. When a mutation succeeds, TanStack Query invalidates its cache, causing components to re-render with fresh server data while Zustand-managed UI preferences (like scroll position) remain unchanged.