# State Management — Xertica UI

Projects scaffolded with `xertica-ui init` use a layered state strategy:

| Layer                     | Tool                            | Responsibility                                                                          |
| ------------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
| **Server state**          | TanStack React Query v5         | Data that originates from an API — fetching, caching, background refetch, loading/error |
| **Client UI state**       | Zustand v5                      | State that lives only in the browser — filters, toggles, form controls, selected tab    |
| **Auth state**            | `AuthContext`                   | Current user session, login, logout                                                     |
| **Layout state**          | `LayoutContext` / `useLayout()` | Sidebar width/expanded, assistant expanded                                              |
| **Local component state** | `useState`                      | Ephemeral state that does not need to outlive the component (dialogs, inline edits)     |

---

## Features Slice Structure

All server data and client UI state lives in `src/features/<domain>/`. The scaffold template (`npx xertica-ui init`) generates the following structure:

```
src/features/
├── home/
│   ├── data/
│   │   └── mock.ts              ← types + mock data + async fetch functions
│   ├── hooks/
│   │   └── useFeatureCards.ts   ← React Query hook (language-aware)
│   ├── store/
│   │   └── dashboardStore.ts    ← Zustand store
│   ├── ui/
│   │   └── HomeContent.tsx      ← page content component
│   └── index.ts                 ← barrel (re-exports everything)
└── assistant/
    ├── data/
    │   └── mock.ts              ← AssistantConfig + fetchAssistantConfig + factory functions
    ├── hooks/
    │   └── useAssistantConfig.ts ← React Query hook (language-aware)
    └── index.ts
```

> The library source (`xertica-ui` package) contains additional hooks (`useDashboardStats`, `useTeamMembers`) for the design-system showcase pages. These are not generated in the scaffold template.

### Adding a New Feature

1. Create `features/<name>/data/mock.ts` with your types and mock fetch functions
2. Create `features/<name>/hooks/use<Name>.ts` wrapping the fetch function in `useQuery`
3. Create `features/<name>/store/<name>Store.ts` for any client-side UI state
4. Re-export from `features/<name>/index.ts`
5. Consume in your component via the hook

---

## React Query (Server State)

### Setup

`QueryClientProvider` is placed at the root of the provider stack, outside `XerticaProvider`:

```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
});

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <XerticaProvider>
        <Router>...</Router>
      </XerticaProvider>
    </QueryClientProvider>
  );
}
```

### Writing a Hook

Hooks that return translated strings must include the active **language** in their `queryKey`. This gives each locale its own cache slot — switching language creates a new cache key, triggers a refetch in the new language, and switching back is an instant cache hit.

```ts
// features/home/hooks/useTeamMembers.ts
import { useQuery } from '@tanstack/react-query';
import { useLanguage } from 'xertica-ui/hooks';
import { fetchTeamMembers, type TeamMember } from '../data/mock';

export function useTeamMembers() {
  const { language } = useLanguage(); // ← current locale from LanguageContext
  return useQuery<TeamMember[]>({
    queryKey: ['home', 'team-members', language], // ← language as third element
    queryFn: fetchTeamMembers,
    staleTime: 2 * 60 * 1000, // 2 min
  });
}
```

> **Note:** Do not export a named `*_KEY` constant. The key now includes `language`, which is only available at hook call-time, making a module-level constant impossible. Keep the key inlined in the hook.

### Consuming in a Component

```tsx
import { useTeamMembers } from '../../../features/home';

export function TeamTable() {
  const { data: members = [], isLoading, isError } = useTeamMembers();

  if (isLoading) return <Skeleton />;
  if (isError) return <ErrorState />;

  return (
    <Table>
      <TableBody>
        {members.map(m => (
          <TableRow key={m.id}>
            <TableCell>{m.name}</TableCell>
            <TableCell>{m.email}</TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
```

### Connecting to a Real API

Replace only the `fetch*` function in `data/mock.ts` — the hook and component stay unchanged:

```ts
// features/home/data/mock.ts

// Before (mock)
export async function fetchTeamMembers(): Promise<TeamMember[]> {
  await new Promise(resolve => setTimeout(resolve, 250));
  return MOCK_TEAM_MEMBERS;
}

// After (real API)
export async function fetchTeamMembers(): Promise<TeamMember[]> {
  const res = await fetch('/api/team/members');
  if (!res.ok) throw new Error('Failed to fetch team members');
  return res.json();
}
```

### staleTime Reference

| Hook                 | staleTime | Rationale                     |
| -------------------- | --------- | ----------------------------- |
| `useFeatureCards`    | 10 min    | Feature list is nearly static |
| `useAssistantConfig` | 30 min    | Config changes very rarely    |

> Library-internal showcase hooks (`useDashboardStats` — 5 min, `useTeamMembers` — 2 min) are not generated in the scaffold template but follow the same language-aware pattern.

---

## Zustand (Client UI State)

Zustand manages state that never needs to be fetched from a server — filters, tab selections, progress bars, switch toggles, and other UI controls.

### Writing a Store

```ts
// features/home/store/dashboardStore.ts
import { create } from 'zustand';

interface DashboardStore {
  activeTab: string;
  setActiveTab: (tab: string) => void;
  progress: number;
  setProgress: (value: number) => void;
}

export const useDashboardStore = create<DashboardStore>(set => ({
  activeTab: 'overview',
  setActiveTab: tab => set({ activeTab: tab }),
  progress: 45,
  setProgress: value => set({ progress: value }),
}));
```

### Consuming in a Component

```tsx
import { useDashboardStore } from '../../../features/home';

function ProgressSection() {
  // Subscribe only to the slice you need — avoids unnecessary re-renders
  const progress = useDashboardStore(s => s.progress);
  const setProgress = useDashboardStore(s => s.setProgress);

  return <Progress value={progress} onChange={setProgress} />;
}
```

### When to Use Zustand vs useState

| Use `useState`          | Use `useDashboardStore` (Zustand)             |
| ----------------------- | --------------------------------------------- |
| Dialog open/closed      | Active filter tab                             |
| Rename input value      | Slider value shared across sections           |
| Hover state             | Switch enabled state                          |
| Single-component toggle | Any state that must survive component unmount |

---

## AuthContext

Authentication state is managed by a dedicated context — not React Query and not Zustand.

### Setup

`AuthProvider` must be inside `<Router>` because it uses `useNavigate`:

```tsx
<Router>
  <AuthProvider>
    <AppRoutes />
  </AuthProvider>
</Router>
```

### Hook

```tsx
import { useAuth } from '../app/context/AuthContext';

function MyComponent() {
  const { user, isLoading, login, logout } = useAuth();

  if (isLoading) return null; // waiting for localStorage hydration

  return user ? (
    <button onClick={logout}>Sign out {user.email}</button>
  ) : (
    <button onClick={() => login('user@example.com', 'password')}>Sign in</button>
  );
}
```

### `isLoading` Flag

On first render, `AuthProvider` reads `localStorage` asynchronously. Until it resolves, `isLoading` is `true`. Route guards check this flag before deciding to redirect:

```tsx
function ProtectedRoute({ children }) {
  const { user, isLoading } = useAuth();
  if (isLoading) return null; // render nothing until hydrated
  if (!user) return <Navigate to="/login" replace />;
  return <>{children}</>;
}
```

---

## Decision Tree

```
Need data from a server or async function?
  └─ YES → React Query (useQuery in features/*/hooks/)
      └─ Is it mock data? → Use features/*/data/mock.ts fetch function
      └─ Is it a real API? → Replace fetch function body, keep everything else

State is browser-only (no server involved)?
  └─ Shared across multiple components → Zustand (features/*/store/)
  └─ Used by a single component → useState

Authentication / current user?
  └─ useAuth() from AuthContext

Sidebar width / assistant panel?
  └─ useLayout() from xertica-ui/hooks
```

---

## AI Rules

- **Never hardcode mock data in components** — all data arrays (team members, stats, feature cards, assistant suggestions) must live in `features/*/data/mock.ts`
- **Never call `fetch` directly inside a component** — always wrap in a React Query hook in `features/*/hooks/`
- **Never use `useState` for server data** — `useState([])` + `useEffect(() => fetch(...))` is the old pattern; use `useQuery` instead
- **`QueryClientProvider` goes outside `XerticaProvider`** — it must be the outermost provider so all nested providers can use React Query if needed
- **`AuthProvider` goes inside `<Router>`** — it uses `useNavigate` which requires a Router ancestor
- **Use `staleTime`** — always set a `staleTime` appropriate to how often the data changes; never leave it at the default `0` (which re-fetches on every focus)
- **Zustand selectors prevent re-renders** — always subscribe with a selector: `useDashboardStore(s => s.progress)` instead of `const store = useDashboardStore()`
- **`isLoading` from `useAuth()` prevents flash redirects** — always check `isLoading` in route guards before reading `user`
