# How the Open Notebook Frontend Interacts with the API Using React Query and Zustand

> Discover how the Open Notebook frontend powers API interactions with React Query for data management and Zustand for UI state. Explore efficient frontend architecture.

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

---

**The Open Notebook frontend leverages React Query to manage all server-side data fetching, caching, and synchronization via a centralized API wrapper, while Zustand handles ephemeral UI state like themes and sidebar toggles, ensuring a clean separation between persistent server data and local client interactions.**

The `lfnovo/open-notebook` repository implements a modern React architecture that separates server state from client UI concerns. Understanding how the Open Notebook frontend interacts with the API using React Query and Zustand reveals a pattern that maximizes performance through automatic caching while keeping the codebase maintainable. This approach allows the application to handle complex data relationships—such as notebooks, notes, and chat sessions—while maintaining responsive local interactions.

## React Query for Server State Management

All asynchronous communication with the FastAPI backend flows through React Query. The library handles data fetching, background refetching, and cache invalidation via a centralized configuration.

### The API Wrapper Layer

Domain-specific API wrappers live under `frontend/src/lib/api/` and provide a thin abstraction over an Axios-based `apiClient`. For example, `notebooksApi` exposes typed methods for CRUD operations. These wrappers are consumed by React Query hooks rather than called directly from components, ensuring consistent error handling and request configuration.

### Query Keys and Cache Configuration

Cache consistency relies on centralized query keys defined in [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts). The `QUERY_KEYS` object provides a single source of truth for all cache identifiers.

```ts
// frontend/src/lib/api/query-client.ts
export const QUERY_KEYS = {
  notebooks: ['notebooks'] as const,
  notebook: (id: string) => ['notebooks', id] as const,
  // …other keys
}

```

These keys are used across all `useQuery` and `useMutation` calls to enforce cache boundaries and enable targeted invalidation.

### Data Fetching with useQuery

Data retrieval hooks combine the API wrapper with `useQuery` to provide loading states and cached results. The `useNotebooks` hook in [`frontend/src/lib/hooks/use-notebooks.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/hooks/use-notebooks.ts) demonstrates this pattern:

```tsx
// frontend/src/lib/hooks/use-notebooks.ts
export function useNotebooks(archived?: boolean) {
  return useQuery({
    queryKey: [...QUERY_KEYS.notebooks, { archived }],
    queryFn: () => notebooksApi.list({ archived, order_by: 'updated desc' }),
  })
}

```

Components consume these hooks directly. For instance, the notebook list page at `frontend/src/app/(dashboard)/notebooks/page.tsx` calls `const { data: notebooks, isLoading } = useNotebooks()` to render data without managing fetch logic.

### Mutations and Cache Invalidation

Server-side modifications use `useMutation` with explicit cache invalidation. After a successful mutation, the query client invalidates related keys to trigger automatic refetches. The `useCreateNotebook` hook implements this pattern:

```tsx
// frontend/src/lib/hooks/use-notebooks.ts
export function useCreateNotebook() {
  const queryClient = useQueryClient()
  const { toast } = useToast()
  const { t } = useTranslation()

  return useMutation({
    mutationFn: (data: CreateNotebookRequest) => notebooksApi.create(data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: QUERY_KEYS.notebooks })
      toast({ title: t('common.success'), description: t('notebooks.createSuccess') })
    },
  })
}

```

This ensures the notebook list automatically refreshes after creation, keeping the UI synchronized with the server state.

## Zustand for Client-Side UI State

While React Query manages server data, **Zustand** stores handle transient UI state that never requires network persistence.

### Theme and Sidebar Stores

Stores are defined in `frontend/src/lib/stores/` and created using Zustand’s `create<T>()` function. Each store exports a hook for direct component consumption.

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) manages light/dark/system preferences:

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

export const useThemeStore = create<ThemeState>()((set) => ({
  theme: 'system',
  setTheme: (theme) => set({ theme }),
}))

```

Similarly, [`frontend/src/lib/stores/sidebar-store.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/stores/sidebar-store.ts) controls the dashboard sidebar’s open/closed state without triggering API calls.

### Accessing Store Data in Components

Components import store hooks directly and update UI instantly. For example, accessing the theme requires no network round-trip:

```tsx
const { theme, setTheme } = useThemeStore()

```

This pattern keeps the UI responsive, as interactions like toggling a sidebar or switching themes update local state immediately without loading indicators or cache misses.

## Architecture and Provider Setup

The application wires these systems together at the root level to ensure global availability.

### QueryClientProvider Configuration

The `QueryProvider` component in [`frontend/src/components/providers/QueryProvider.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/components/providers/QueryProvider.tsx) wraps the application in a `QueryClientProvider`, instantiating a shared `QueryClient` from [`query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/query-client.ts):

```tsx
// frontend/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>
}

```

This singleton client ensures all hooks share the same cache instance.

### Composing Providers in the Root Layout

The root layout—typically [`frontend/src/app/layout.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/app/layout.tsx) or [`AppShell.tsx`](https://github.com/lfnovo/open-notebook/blob/main/AppShell.tsx)—composes multiple providers. It nests `QueryProvider` alongside Zustand-based providers like `ThemeProvider` and `ModalProvider`. This hierarchy guarantees that React Query’s cache and Zustand’s stores are both available throughout the component tree before any data fetching or UI rendering occurs.

## Summary

- **React Query** manages all server-state interactions in `lfnovo/open-notebook`, handling data fetching, caching, and synchronization via domain-specific hooks in `frontend/src/lib/hooks/`.
- **Zustand** stores in `frontend/src/lib/stores/` control pure UI state such as themes and sidebar visibility, updating instantly without network requests.
- **Cache invalidation** occurs automatically through `queryClient.invalidateQueries` in mutation success callbacks, ensuring UI consistency after data modifications.
- **Provider composition** in the root layout makes both the React Query cache and Zustand stores globally accessible to the application.

## Frequently Asked Questions

### How does Open Notebook handle cache invalidation when creating a new notebook?

When a notebook is created, the `useCreateNotebook` hook calls `queryClient.invalidateQueries({ queryKey: QUERY_KEYS.notebooks })` in its `onSuccess` callback. This marks the notebook list cache as stale, triggering React Query to automatically refetch the data from the API and update the UI without manual state management.

### Why does the frontend use Zustand instead of React Query for theme state?

Zustand manages client-only UI concerns like theme selection because this data never needs to persist to the server or be cached across sessions. React Query is optimized for server-state synchronization, which would be unnecessary overhead for ephemeral interactions like toggling between light and dark modes.

### Where is the QueryClient configured in the Open Notebook frontend?

The `QueryClient` is configured and exported from [`frontend/src/lib/api/query-client.ts`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/lib/api/query-client.ts), which also defines the centralized `QUERY_KEYS` object. The client is then provided to the application via `QueryClientProvider` in [`frontend/src/components/providers/QueryProvider.tsx`](https://github.com/lfnovo/open-notebook/blob/main/frontend/src/components/providers/QueryProvider.tsx), ensuring a single cache instance is shared across all components.

### How does the API wrapper interact with the backend in Open Notebook?

The API wrappers located in `frontend/src/lib/api/` (such as `notebooksApi`) use an Axios-based `apiClient` to communicate with the FastAPI backend. These wrappers expose typed methods for CRUD operations, which are consumed exclusively by React Query hooks to ensure consistent error handling and loading state management throughout the application.