# When to Use Zustand vs TanStack Query for State Management in OpenWork

> Decide between Zustand and TanStack Query for OpenWork state management. Use Zustand for client UI state and TanStack Query for server state with caching and sync.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: best-practices
- Published: 2026-08-04

---

**Use Zustand for synchronous, client-only UI state like sidebar toggles and persisted layout preferences, and use TanStack Query for asynchronous server state such as session transcripts and remote resources that require caching and background synchronization.**

The OpenWork codebase maintains a strict architectural separation between local interface state and remote data fetching. Understanding when to apply **Zustand versus TanStack Query for state management in OpenWork** ensures optimal performance, instantaneous UI updates, and robust server data consistency. This guide maps the implementation patterns found in the source code to practical decision criteria.

## The Architectural Split in OpenWork

The codebase categorizes state into two distinct types, assigning specific libraries based on the state’s origin and lifecycle requirements.

### Zustand for Local UI State

**Zustand** manages synchronous state that lives entirely in the browser without network dependencies. In [`apps/app/src/react-app/shell/ui-state-store.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/shell/ui-state-store.ts), the store handles sidebar visibility, panel selections, and UI layout settings. Because this state requires no server round-trip, updates remain instantaneous and the footprint stays minimal.

### TanStack Query for Server State

**TanStack Query** (via `@tanstack/react-query`) handles asynchronous data fetched from the backend. The [`apps/app/src/react-app/infra/query-client.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/infra/query-client.ts) file configures a shared `QueryClient` that implements caching, garbage collection through `gcTime`, and stale-while-revalidate semantics for resources like session transcripts and permission lists.

## When to Choose Zustand

Select Zustand when your state meets these specific criteria:

- **Purely client-side** with no remote data dependencies.
- **Instantaneous writes** required without network latency or loading states.
- **Local persistence** needed across page reloads via `localStorage` or similar mechanisms.

The implementation in [`ui-state-store.ts`](https://github.com/different-ai/openwork/blob/main/ui-state-store.ts) demonstrates this pattern using Zustand’s `create` function combined with persistence middleware. Domain-specific stores like [`apps/app/src/react-app/domains/session/notification-store.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/domains/session/notification-store.ts) extend this pattern for localized feature state.

```typescript
import { create } from "zustand";

type UiState = {
  sidebarOpen: boolean;
  setSidebarOpen: (open: boolean) => void;
  toggleSidebar: () => void;
};

export const useUiStore = create<UiState>((set) => ({
  sidebarOpen: true,
  setSidebarOpen: (open) => set({ sidebarOpen: open }),
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));

```

## When to Choose TanStack Query

Adopt TanStack Query for state representing remote resources or asynchronous operations:

- **Server data** requiring intelligent caching, background refetching, and automatic retries.
- **Query-key based identity** for granular invalidation and subscription management.
- **Garbage collection** of unused queries to prevent memory leaks (configured via `gcTime`).

The `getReactQueryClient()` function in [`query-client.ts`](https://github.com/different-ai/openwork/blob/main/query-client.ts) establishes these behaviors with tuned `gcTime` settings for specific query families like `["react-session-transcript"]`.

```typescript
import { useQuery } from "@tanstack/react-query";
import { getReactQueryClient } from "@/infra/query-client";

export function useSessionTranscript(sessionId: string) {
  const client = getReactQueryClient();
  return useQuery(
    ["react-session-transcript", sessionId],
    async () => {
      const resp = await fetch(`/api/sessions/${sessionId}/transcript`);
      return resp.json();
    },
    { client }
  );
}

```

## Combining Both Libraries in Components

OpenWork frequently composes both libraries within single components to maximize responsiveness while maintaining data consistency. UI components read local flags from Zustand while fetching remote data via TanStack Query hooks.

```tsx
import { useUiStore } from "@/react-app/shell/ui-state-store";
import { useSessionTranscript } from "@/react-app/queries/session";

export function Sidebar() {
  const { sidebarOpen, toggleSidebar } = useUiStore();
  const { data, isLoading } = useSessionTranscript("current");

  return (
    <aside className={sidebarOpen ? "open" : "closed"}>
      <button onClick={toggleSidebar}>Toggle</button>
      {isLoading ? "Loading…" : <pre>{JSON.stringify(data, null, 2)}</pre>}
    </aside>
  );
}

```

In this pattern, the sidebar’s open/close flag lives in **Zustand** for immediate feedback, while the transcript data is fetched and cached by **TanStack Query**.

## Key Implementation Files

| File | Role |
|------|------|
| [`apps/app/src/react-app/shell/ui-state-store.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/shell/ui-state-store.ts) | Central Zustand store for UI layout and persisted preferences. |
| [`apps/app/src/react-app/domains/session/notification-store.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/domains/session/notification-store.ts) | Domain-specific Zustand store demonstrating the `persist` middleware pattern. |
| [`apps/app/src/react-app/infra/query-client.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/infra/query-client.ts) | Creates the shared `QueryClient` and configures global GC policies. |
| [`apps/app/tests/notification-store.test.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/tests/notification-store.test.ts) | Unit tests validating Zustand persistence behavior. |

## Summary

- **Zustand** manages synchronous, client-only UI state like sidebar toggles and layout preferences, offering instantaneous updates and `localStorage` persistence via `persistUiState`.
- **TanStack Query** handles asynchronous server state including session transcripts and permission lists, providing caching, background refetching, and garbage collection via `gcTime` configuration.
- The libraries are designed to work together: use Zustand for local responsiveness and TanStack Query for remote data consistency.
- Reference [`ui-state-store.ts`](https://github.com/different-ai/openwork/blob/main/ui-state-store.ts) for Zustand patterns and [`query-client.ts`](https://github.com/different-ai/openwork/blob/main/query-client.ts) for TanStack Query configuration.

## Frequently Asked Questions

### Can I use Zustand for server state in OpenWork?

While technically possible, you should avoid using Zustand for server state. The OpenWork architecture reserves Zustand for client-only state because it lacks built-in caching, background refetching, and deduplication logic. TanStack Query provides these features through the `QueryClient` configured in [`query-client.ts`](https://github.com/different-ai/openwork/blob/main/query-client.ts), making it the correct choice for remote data.

### How does OpenWork persist Zustand state across reloads?

OpenWork uses Zustand’s `persist` middleware to synchronize state with `localStorage`. The [`ui-state-store.ts`](https://github.com/different-ai/openwork/blob/main/ui-state-store.ts) and [`notification-store.ts`](https://github.com/different-ai/openwork/blob/main/notification-store.ts) files implement this pattern to preserve UI layout and notification preferences across browser sessions without requiring backend storage.

### Why not use TanStack Query for UI state like sidebar toggles?

TanStack Query is optimized for asynchronous data fetching and caching, introducing unnecessary overhead for synchronous UI toggles. Using it for local UI state would create artificial network latency patterns and complicate the garbage collection lifecycle. Zustand provides the immediate, synchronous updates required for interface interactions.

### How do I configure the QueryClient in OpenWork?

Import `getReactQueryClient()` from [`apps/app/src/react-app/infra/query-client.ts`](https://github.com/different-ai/openwork/blob/main/apps/app/src/react-app/infra/query-client.ts) to access the shared instance. This function pre-configures default `gcTime` values and caching strategies used throughout the application, ensuring consistent behavior for queries like `["react-session-transcript"]`.