# Step 5 — Local & Global State with Zustand

Not all state belongs in a server response. "Is this dialog open?", "which files are mid-delete?", "is this task's agent currently stopping?" — that's **client-only UI state**, and iKanban reaches for [Zustand](https://github.com/pmndrs/zustand) when that state needs to be shared across components that aren't parent/child. Let's read the real store: `src/stores/useTaskDetailsUiStore.ts`.

## The problem it solves

A task detail panel and a task list row might both need to know "is this task's delete-in-progress spinner showing?" — but they're not nested inside each other. Passing that state down through props ("prop drilling") gets awkward fast. Zustand gives every component that calls the store's hook a shared, reactive slice of state, without a Context Provider wrapping your tree.

## Anatomy of a Zustand store

```ts
interface TaskUiState {
  loading: boolean;
  isStopping: boolean;
  deletingFiles: Set<string>;
  fileToDelete: string | null;
}

interface UiStateMap {
  [taskId: string]: TaskUiState;
}

interface TaskDetailsUiStore {
  ui: UiStateMap;
  getUiState: (taskId: string) => TaskUiState;
  setUiState: (taskId: string, partial: Partial<TaskUiState>) => void;
  clearUiState: (taskId: string) => void;
}

const useTaskDetailsUiStore = create<TaskDetailsUiStore>((set, get) => ({
  ui: {},
  getUiState: (taskId) => get().ui[taskId] ?? defaultUiState,
  setUiState: (taskId, partial) => {
    set((state) => ({
      ui: { ...state.ui, [taskId]: { ...defaultUiState, ...state.ui[taskId], ...partial } },
    }));
  },
  clearUiState: (taskId) => {
    set((state) => {
      const newUi = { ...state.ui };
      delete newUi[taskId];
      return { ui: newUi };
    });
  },
}));
```

Notice the shape: this store is **keyed by `taskId`**, not a single flat state object. iKanban can have many task detail panels' worth of UI state alive at once (e.g. across browser tabs or a multi-panel layout), so `UiStateMap` gives each task its own isolated `TaskUiState` slice.

`create<TaskDetailsUiStore>((set, get) => ({ ... }))` is the entire API surface: `set` merges a partial update into state (triggering re-renders in subscribed components), `get` reads current state without subscribing. There's no reducer, no action types, no dispatch — just a plain object of state and functions that update it.

## A derived, narrow hook

```ts
export const useTaskStopping = (taskId: string) => {
  const { getUiState, setUiState } = useTaskDetailsUiStore();
  const { isStopping } = getUiState(taskId);

  return {
    isStopping,
    setIsStopping: (value: boolean) => setUiState(taskId, { isStopping: value }),
  };
};
```

Rather than have every consumer call `useTaskDetailsUiStore()` and manually pull out `isStopping`, the store file exports **purpose-built hooks** like `useTaskStopping`. A component that only cares whether the agent is stopping imports this one hook and gets exactly `{ isStopping, setIsStopping }` — nothing else, and no risk of accidentally reading or mutating unrelated UI state.

## When to reach for Zustand vs. plain `useState`

- **Plain `useState`**: state used by one component and its direct children — pass it down as props.
- **Zustand**: state needed by components that don't share a close parent, or that must persist across a component unmounting/remounting (e.g. switching away from a task panel and back).
- **Neither** — for state that comes from the server (a list of tags, a task's details): that's Step 6's job, TanStack Query, not Zustand. iKanban keeps a firm line between "UI state" (Zustand) and "server state" (TanStack Query) rather than stuffing API responses into a Zustand store.

## Checkpoint

In `useTaskDetailsUiStore.ts`, find the `deletingFiles: Set<string>` field and see how `setUiState` handles it specially (hint: `Set` isn't a plain object, so a shallow spread `{ ...old }` wouldn't create a new `Set` — read the `deletingFiles: partial.deletingFiles ? new Set(...) : ...` line).

Continue to **Step 6 — Routing with react-router-dom + Data Fetching with TanStack Query**, where we bring in data that comes from the network instead of the client.
