---
description: React webapp conventions (Math Tutoring App)
globs: webapp/**/*.{tsx,ts}
alwaysApply: false
---

# React Patterns — `webapp/`

Stack: **React 19**, **Vite**, **React Router**, **Firebase** (client SDK).

## useEffect rules (read first)

- **Computed values → plain vars, not state.** Never use `useEffect` to sync props into state. Derive inline.
- **Router-driven resets → `key`, not `useEffect`.** When a screen must reinitialize from a route param (or equivalent identity), let the parent set `key={…}` to remount instead of mirroring props into state in an effect.
- **Callbacks in deps → `useEffectEvent`.** Never put a parent-supplied callback directly in a `useEffect` dependency array — it may not be stable. Wrap it with `useEffectEvent`. Prefer `useEffectEvent` over `useCallback` for this.
- **“Latest value” reads → `useEffectEvent`.** If you need a value inside an effect but don’t want it to re-trigger the effect, read it inside a `useEffectEvent` wrapper.
- **Fetching / async load → custom hooks only.** Never perform data loading (Firestore queries, `fetch` to the Express API, etc.) directly inside `useEffect` in a route or feature component. Use a `useXxx` hook in `src/hooks/` that encapsulates loading (e.g. TanStack Query, or a dedicated hook that wraps your service calls). Components call hooks; they don’t inline async load effects.
- **Expensive calculations → `useMemo`.** Don’t compute inside an effect and write to state unless it’s truly an external sync; derive or memoize.

**Exceptions:** Long-lived **subscriptions** (e.g. Firebase `onAuthStateChanged` in `AuthProvider`) belong in an effect with a proper cleanup return — that is not “fetching”; keep listeners from leaking.

```tsx
// Rules 4 & 3 — useEffectEvent for latest values and unstable callbacks
import { useEffect, useEffectEvent, useState } from 'react';

function TypingIndicator({ conversationId, onTick }: Props) {
  const [dots, setDots] = useState(1);

  const fireTick = useEffectEvent(() => {
    setDots((d) => (d % 3) + 1);
    onTick(); // parent callback — not listed in useEffect deps
  });

  useEffect(() => {
    const id = setInterval(fireTick, 500);
    return () => clearInterval(id);
  }, [conversationId]); // only remount interval when conversation changes
}

// Rules 2 & 1 — key for remount; derived strings, not effect-synced state
// Parent (e.g. route wrapper)
<Chat key={conversationId ?? 'new'} />

// Inside Chat — no useEffect to copy props into state
function Chat({ conversationId }: { conversationId: string | null }) {
  const { data } = useConversationHistory(); // Rule 5 — load via hook
  const title = conversationId ? `Thread ${conversationId.slice(0, 8)}` : 'New chat'; // Rule 1
  const sortedMessages = useMemo(
    () => [...(data?.messages ?? [])].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()),
    [data?.messages]
  ); // Rule 6
}
```

## Custom hooks per resource

- Each **distinct data domain** gets its own hook in `src/hooks/`, named after that domain (`useAuth`, `useConversationHistory`, etc.).
- **One hook per resource shape** where it helps clarity: e.g. conversation list/history vs. auth session vs. a single upload flow — don’t stuff unrelated async into one mega-hook.
- **Services stay in `src/services/`** (`chatService`, `api`, `storageService`); hooks orchestrate those (and caching libraries if you add them). Screen components should not import `chatService` / call `fetch` directly for load paths that belong in a hook.

```ts
// ✅ GOOD — names reflect resources / concerns
useAuth();
useConversationHistory();

// ❌ BAD — Chat.tsx importing loadConversationHistory and wiring useEffect inline for the main load path
```

## Layout

| Area | Path |
|------|------|
| App shell & routes | `webapp/src/App.tsx`, `webapp/src/main.tsx` |
| Screens / features | `webapp/src/components/` (e.g. `Chat.tsx`, `Login.tsx`, `SignUp.tsx`) |
| Design system | `webapp/src/components/design-system/` |
| Hooks | `webapp/src/hooks/` |
| Context | `webapp/src/contexts/` |
| Client API / streaming | `webapp/src/services/api.ts` |
| Firestore / persistence | `webapp/src/services/chatService.ts`, `storageService.ts` |
| Global styles / tokens | `webapp/src/styles/tokens.ts` |

There is no `src/pages/` tree; route-level views live under `src/components/`.

## Data & side effects

- **Firestore reads/writes** run in the **webapp** via the Firebase client SDK — not from the Express API for normal chat persistence.
- **LLM / OpenAI** runs only **server-side** (`api/`). The webapp uses Express for chat streaming (`api.ts` / SSE) and sends **Firebase ID tokens**; never embed provider API keys in the client.
- Prefer **custom hooks** (`useAuth`, resource hooks above) for shared data and UI coordination.

## Components

- Prefer **focused components** under `src/components/` over single huge files; shared primitives live in `design-system/`.
- Keep **accessibility** in mind for interactive controls (buttons, forms, chat input).
