# Maya

> React frontend standards for a Supabase + FastAPI stack — components, hooks, state, data fetching, forms, and chat UI. Use when writing or reviewing React components, hooks, JSX/TSX, frontend state management, client-side data fetching, or building chat/streaming UI.

- Skill: `arjuncrevathi/maya` (Agent Skill)
- Install (CLI): `npx skillmds@latest add arjuncrevathi/maya`
- Raw SKILL.md: https://api.skillmd.com/api/skills/arjuncrevathi/maya/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: arjuncrevathi (https://skillmd.com/u/arjuncrevathi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/arjuncrevathi/maya

---


# Maya — The Divine Illusion (React Frontend)

Maya governs appearance: the UI is the illusion layer over the system. The illusion must never lie — every state the system can be in, the interface must be able to show.

## Components

- Function components only. No classes except error boundaries (until `use` + error handling covers them).
- One component per file; the file is named after the component (`ChatMessage.tsx`).
- A component over ~150 lines or with more than one reason to change gets split. Extract by responsibility, not by line count.
- Props are typed with an explicit `interface`/`type` — never inline object literals in the signature, never `any`.
- Derive, don't sync: values computable from props or existing state are computed in render, not mirrored into `useState`. Mirrored state is the root of most React bugs.
- Children over configuration: prefer composition (`<Card><CardHeader/></Card>`) to boolean-prop explosions (`<Card withHeader headerText=…>`).

## Hooks

- Call order rules are absolute: no hooks in conditions, loops, or after early returns.
- `useEffect` is for synchronizing with external systems (subscriptions, DOM, analytics) — not for data fetching, not for reacting to state you set yourself. Most `useEffect`s in app code are a smell.
- Every effect that subscribes returns a cleanup that unsubscribes. Test by mounting twice (StrictMode does this for you — never disable it).
- Custom hooks (`useConversation`, `useAuth`) are the unit of frontend logic reuse. If two components duplicate stateful logic, extract the hook.

## State — the two-store rule

- **Server state** (anything persisted in Supabase or behind FastAPI) lives in TanStack Query — never copied into `useState`/`useReducer`. Caching, refetching, and invalidation are Query's job.
- **UI state** (open modals, input drafts, selected tab) lives in `useState` local to the closest owner. Lift only when two components genuinely share it.
- Global client state (theme, current user) gets one small context or a zustand store — not Redux, not context-per-feature sprawl.
- Never store a Supabase session in your own state: read it from `supabase.auth` via the official helper/hook and subscribe to `onAuthStateChange`.

## Data fetching

- Two data paths, never blurred:
  - **App data** → Supabase client directly from the frontend, guarded by RLS (see `yama`).
  - **Chat + AI operations** → FastAPI on Render (see `vayu`), authenticated with the Supabase access token in `Authorization: Bearer`.
- All reads go through TanStack Query with structured keys: `["conversations", userId]`, `["messages", conversationId]`. Mutations invalidate exactly the keys they change.
- No `fetch` in `useEffect`. No unawaited promises in handlers — every mutation surfaces its error state.
- Define one typed API client module for FastAPI calls (`src/lib/api.ts`); components never call `fetch` directly, never build URLs inline.
- Generate Supabase types (`supabase gen types typescript`) and use the typed client. A schema change that breaks the frontend must break it at compile time.

## Every async view has three states

- Loading, error, and empty are designed states, not afterthoughts: skeleton (not spinner) for loading, a retry affordance for errors, and an inviting empty state that says what to do first.
- Errors show what happened and what to do next, in user language — never a raw exception, never a bare "Something went wrong" without a retry.
- Wrap feature roots in an error boundary so one crashed widget doesn't blank the page.

## Forms

- `react-hook-form` + a `zod` schema per form. The schema is the single source of validation truth; share it with the API layer where shapes match.
- Validate on blur and on submit; never only on submit for long forms.
- Disable the submit button while submitting; re-enable on settled. Double-submits are a frontend bug, not a backend problem.

## Chat & streaming UI (the AI-native part)

- Render streamed tokens incrementally from the FastAPI SSE stream; never buffer a full completion before showing anything.
- Optimistic append: the user's message enters the list immediately with a `pending` marker; reconcile with the server-assigned ID on ack, mark `failed` with a retry action on error.
- Preserve scroll intent: autoscroll only when the user is already at the bottom; a scroll-up pins the view and shows a "jump to latest" affordance.
- Abort in-flight generations with `AbortController` when the user navigates away or hits stop — and tell the backend (see `vayu`), don't just drop the reader.
- Render model output as sanitized markdown; never `dangerouslySetInnerHTML` raw model text. Model output is untrusted input (see `muruka`).

## Performance

- Measure before memoizing: `React.memo`/`useMemo`/`useCallback` only on demonstrated re-render cost, not as decoration.
- Long message lists are virtualized past ~100 items.
- Route-level code splitting via `lazy` — the chat bundle should not pay for the settings page.
- Images: explicit dimensions, lazy loading below the fold.

## Environment & configuration

- Only `VITE_`/`NEXT_PUBLIC_`-prefixed vars reach the browser, and everything so prefixed is public by definition — the Supabase anon key qualifies; the service-role key and any provider API key never do (see `kubera`).
- All config read in one `src/lib/env.ts` that validates presence at startup — no scattered `import.meta.env` reads.

## Before merging frontend work — checklist

- [ ] No server state in `useState`; all fetches through TanStack Query with structured keys
- [ ] Loading, error, and empty states designed for every async view
- [ ] Forms validated by a zod schema; submit protected against double-fire
- [ ] Streamed output renders incrementally; stop/abort works
- [ ] No secret reachable in browser code; env access centralized and validated
- [ ] Supabase types regenerated if the schema changed

