# React Frontend State Management Architecture in Thunderbolt: Zustand, Context, and React Query

> Explore Thunderbolt's React state management: Zustand for global stores, Context for cross-cutting concerns, and React Query with PowerSync for real-time data. Optimize your frontend.

- Repository: [Thunderbird/thunderbolt](https://github.com/thunderbird/thunderbolt)
- Tags: architecture
- Published: 2026-04-19

---

**Thunderbolt’s React frontend combines Zustand for global stores, React Context for cross-cutting concerns, and React Query with PowerSync for real-time data synchronization.**

The Thunderbird Thunderbolt codebase implements a layered state management strategy that separates global mutable state, asynchronous data fetching, and local UI concerns. This architecture leverages modern React patterns while integrating PowerSync’s real-time synchronization capabilities for a desktop email client environment.

## Global State Management with Zustand

Thunderbolt uses **Zustand** to eliminate prop-drilling for application-wide mutable state. The stores are defined in standalone TypeScript files and consumed via hooks throughout the component tree.

### Central Chat Store

The primary global store resides in [`chats/chat-store.ts`](https://github.com/thunderbird/thunderbolt/blob/main/chats/chat-store.ts). It manages the conversation list, active chat selection, and optimistic UI updates:

```typescript
import { create } from 'zustand'
import { useShallow } from 'zustand/react/shallow'

export const useChatStore = create<ChatState>((set, get) => ({
  chats: [],
  activeChatId: null,
  setActiveChatId: (id) => set({ activeChatId: id }),
  addChat: (chat) => set((state) => ({ chats: [...state.chats, chat] })),
  // Additional mutating helpers for optimistic updates
}))

```

Components subscribe to specific slices using `useShallow` to prevent unnecessary re-renders:

```typescript
import { useChatStore } from '../../chats/chat-store'
import { useShallow } from 'zustand/react/shallow'

export const ChatListItem = ({ thread }) => {
  const { activeChatId, setActiveChatId } = useChatStore(
    useShallow(state => ({
      activeChatId: state.activeChatId,
      setActiveChatId: state.setActiveChatId,
    }))
  )
  // Render logic using activeChatId
}

```

### Additional Zustand Stores

Smaller scoped stores handle specific UI state:
- **[`components/welcome-dialog.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/components/welcome-dialog.tsx)**: Manages welcome modal visibility and dismissal state
- **[`layout/sidebar/chat-list-item.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/layout/sidebar/chat-list-item.tsx)**: Local UI state for sidebar interactions

## Cross-Cutting Concerns via React Context

For infrastructure and configuration that changes infrequently but must be accessible deep in the tree, Thunderbolt implements **React Context providers**. These wrap subsystems in dedicated context objects consumed via `useContext`.

| Context | Source File | Purpose |
|---------|-------------|---------|
| **ThemeProviderContext** | [`lib/theme-provider.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/lib/theme-provider.tsx) | Current theme (`light`/`dark`), `setTheme` function, and persistence |
| **MCPProviderContext** | [`lib/mcp-provider.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/lib/mcp-provider.tsx) | Multi-Channel PowerSync server connection, server list, reconnect logic |
| **TrayContext** | [`lib/tray.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/lib/tray.tsx) | System tray icon management, menu actions, click callbacks |
| **HttpClientContext** | [`contexts/http-client-context.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/contexts/http-client-context.tsx) | Pre-configured `HttpClient` for internal API calls |
| **DatabaseContext** | [`contexts/database-context.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/contexts/database-context.tsx) | PowerSync database instance and reactive helpers |
| **AuthContext** | [`contexts/auth-context.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/contexts/auth-context.tsx) | Current user, auth token, `signIn`, `signOut` methods |
| **ContentViewContext** | [`content-view/context.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/content-view/context.tsx) | Content view pane mode (webview vs. native) and preview visibility |
| **SignInModalContext** | [`contexts/sign-in-modal-context.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/contexts/sign-in-modal-context.tsx) | Sign-in modal and sync-setup modal visibility |

Each provider follows a standard implementation pattern:

```tsx
const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined)

export function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>('system')
  
  // Persistence and system preference logic
  
  return (
    <ThemeProviderContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeProviderContext.Provider>
  )
}

export const useTheme = () => {
  const context = useContext(ThemeProviderContext)
  if (!context) throw new Error('useTheme must be used within ThemeProvider')
  return context
}

```

## Synchronized Data Fetching with React Query and PowerSync

Thunderbolt integrates **TanStack Query** (React Query) via the `@powersync/tanstack-react-query` package to handle asynchronous data synchronization. This pattern appears throughout settings pages and task management views.

### Real-time Database Integration

Components query the PowerSync database reactively, with automatic caching and background refetching:

```tsx
import { useQuery } from '@powersync/tanstack-react-query'

export const ModelsList = () => {
  const { data: models = [], isLoading } = useQuery({
    queryKey: ['models'],
    queryFn: async () => {
      const res = await db.select().from(modelsTable).all()
      return res
    }
  })
  
  return (
    <div>
      {models.map(model => <ModelCard key={model.id} model={model} />)}
    </div>
  )
}

```

Files implementing this pattern include:
- [`settings/models/layout.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/models/layout.tsx)
- [`settings/models/index.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/models/index.tsx)
- [`settings/devices.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/devices.tsx)
- [`tasks/index.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/tasks/index.tsx)

## Local Component State Patterns

For UI-specific concerns that do not require global visibility, Thunderbolt uses standard React hooks.

### Simple UI State with useState

Over 100 components employ `useState` for ephemeral state such as modal visibility, form inputs, and loading indicators. Examples include [`widgets/link-preview/display.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/widgets/link-preview/display.tsx) and [`layout/sidebar/sidebar-header.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/layout/sidebar/sidebar-header.tsx).

### Complex Forms with useReducer

When state logic involves multiple actions or complex transitions, Thunderbolt implements `useReducer`. The Preferences panel ([`settings/preferences.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/preferences.tsx)) and Model editor ([`settings/models/index.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/models/index.tsx)) use this pattern to centralize state transitions and keep component code readable.

## Reusable Utility Hooks

The `src/hooks/` directory contains stateless utility hooks that encapsulate common behavior:

- **`useThrottle`** ([`hooks/use-throttle.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/hooks/use-throttle.tsx)): Limits value update frequency for performance-sensitive inputs
- **`useDebounce`** ([`hooks/use-debounce.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/hooks/use-debounce.tsx)): Delays value updates for search inputs and API calls
- **`useAutoScroll`** ([`hooks/use-auto-scroll.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/hooks/use-auto-scroll.tsx)): Maintains scroll position at the bottom of chat message containers when new content arrives

These hooks do not manage global state but provide deterministic UI logic that components consume declaratively.

## Architectural Flow and Provider Composition

The state management architecture follows a strict initialization sequence in [`src/index.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/src/index.tsx):

1. **Provider Wrapping**: The root component nests all context providers (`ThemeProvider`, `AuthProvider`, `DatabaseProvider`, `MCPProvider`, etc.) to make infrastructure available throughout the tree
2. **Global Store Instantiation**: Zustand stores initialize once on module import and remain available for subscription anywhere in the application
3. **Async Data Hydration**: Components mount and invoke `useQuery` hooks, which fetch initial data from PowerSync and establish reactive subscriptions
4. **UI Interaction**: User actions update local `useState` or dispatch to `useReducer`, while cross-component updates flow through Zustand stores or Context values, triggering automatic re-renders in subscribers

## Summary

- **Zustand stores** in [`chats/chat-store.ts`](https://github.com/thunderbird/thunderbolt/blob/main/chats/chat-store.ts) and related files handle global mutable state accessible without prop-drilling, optimized with `useShallow` selectors
- **React Context providers** in `lib/` and `contexts/` directories encapsulate infrastructure concerns (theme, auth, database, MCP, tray) for deep component access
- **React Query via PowerSync** enables real-time data synchronization with automatic caching, implemented in settings and task views
- **Local state** uses `useState` for simple UI concerns and `useReducer` for complex forms like the preferences panel
- **Utility hooks** in `src/hooks/` provide reusable behavior (throttle, debounce, auto-scroll) without state side effects

## Frequently Asked Questions

### What state management library does Thunderbolt use for global state?

Thunderbolt uses **Zustand** for global state management. The primary store is defined in [`chats/chat-store.ts`](https://github.com/thunderbird/thunderbolt/blob/main/chats/chat-store.ts) and manages chat conversations, active thread selection, and optimistic UI updates. Components subscribe to specific store slices using the `useShallow` helper from `zustand/react/shallow` to prevent unnecessary re-renders.

### How does Thunderbolt handle real-time data synchronization?

Real-time synchronization is implemented through **React Query** (TanStack Query) integrated with **PowerSync** via the `@powersync/tanstack-react-query` package. Components use the `useQuery` hook to select from the local PowerSync database, which automatically synchronizes with the backend. This pattern appears in [`settings/models/layout.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/settings/models/layout.tsx), [`tasks/index.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/tasks/index.tsx), and other data-heavy views, providing automatic caching and background refetching.

### Why does Thunderbolt use both Zustand and React Context?

Thunderbolt uses **Zustand** for mutable global state that changes frequently and must be accessible across many components (like chat lists), while **React Context** provides dependency injection for infrastructure that is relatively stable but required deep in the component tree (themes, authentication, database connections, and the MCP provider). This separation optimizes performance: Zustand prevents context-induced re-render cascades for high-frequency updates, while Context cleanly injects singleton services without prop drilling.

### Where is the chat state defined in the codebase?

The chat state is defined in **[`src/chats/chat-store.ts`](https://github.com/thunderbird/thunderbolt/blob/main/src/chats/chat-store.ts)**. This Zustand store exports the `useChatStore` hook and contains the `chats` array, `activeChatId`, and action methods for managing conversations. Components like [`layout/sidebar/chat-list-item.tsx`](https://github.com/thunderbird/thunderbolt/blob/main/layout/sidebar/chat-list-item.tsx) import this store to read and update chat-related state.