React 19 + Vite + Tailwind v4 — Frontend conventions
Structure
frontend/src/
├── pages/ # Page components (one per route)
├── components/ # ui/ primitives + [feature]/ folders
├── hooks/ # Custom hooks
├── services/ # API calls (fetch wrapper)
├── contexts/ # React contexts (auth, theme)
└── styles/ # Global CSS, Tailwind variables
Components
- One component per file. No multi-export unless sub-components are colocated.
- Max ~200 lines per component. Beyond that, extract sub-components or hooks.
- Destructure props in the signature, with default values.
- No business logic in components. Extract to a hook or a service.
// GOOD
function ItemCard({ item, onFavorite, isFavorited = false }) { /* ... */ }
// BAD — unstructured props, component doing too much
function ItemCard(props) {
const [data, setData] = useState(null);
useEffect(() => { fetch(`/api/items/${props.id}`); }, []);
}
Hooks
- Prefix with
use. One hook = one responsibility.
- Prefer returning a named object over an array (except simple two-value hooks like useState) — makes destructuring self-documenting. This is a team preference, not a framework rule.
State management
- React Context for low-frequency state (theme, auth, locale). Context re-renders every consumer on every change — fine when changes are rare.
- Zustand (or similar) for high-frequency shared state that updates often. Selectors avoid the Context re-render problem.
- Local state by default. Lift only when necessary.
React 19
use() (React 19) — unwraps a Promise or a Context inside render. Key constraints:
- Promise-only for data.
use() works on an already-existing Promise (or Context); it is not a general side-effect mechanism and is not a replacement for useEffect.
- Requires a parent
<Suspense> boundary. Calling use(promise) suspends the component; the closest <Suspense> ancestor catches the suspension and renders its fallback. No boundary → the suspension propagates up the tree and nothing renders (in a Vite app without Suspense wrappers, the whole subtree appears frozen).
- Use it when a promise is passed as a prop from a Server Component, or handed down through an explicit Suspense boundary you control.
- For ad-hoc client-side fetching, use TanStack Query (or SWR). Do not hand-roll fetch flows inside components.
useActionState — wire forms to async actions, track pending/error state without manual useState.
useFormStatus — read parent form's pending state from a child submit button, no prop drilling.
- Server Components awareness — even in a client-heavy Vite app, know the
"use client" boundary. Eases any later migration to Next.js or React Router framework mode.
- React Compiler / React Forget — when enabled, it auto-memoizes and you can skip manual
useMemo/useCallback. Check if the project has it enabled (look for babel-plugin-react-compiler or the reactCompiler Vite plugin). If it's NOT enabled, use useMemo/useCallback where the profiler shows a real need — don't add them preemptively, but don't ignore measurable perf issues either.
Tailwind v4
- Use the design system's CSS variables — no hardcoded values.
- Utility classes inline in JSX.
cn() / clsx() for conditionals.
- Dark mode via
class strategy. Mobile-first responsive.
Forms & API
- React Hook Form + Zod resolver; share the Zod schema between frontend and backend when possible.
- Centralized
fetch wrapper in services/api.js — no direct fetch inside components.
- Every call exposes loading + error states.
Accessibility (minimum)
aria-label on icon-only buttons, focus trapping in modals, sufficient contrast, keyboard nav.
Anti-patterns
- ❌
useEffect to derive state — use useMemo or compute during render
- ❌ Props drilling deeper than 3 levels — use a Context or store
- ❌
index as key in dynamic lists
- ❌ Components over 200 lines without extraction
- ❌
any in TypeScript / JSDoc
1---2name: react-frontend3description: React 19 + Vite + Tailwind v4 frontend conventions. Activates when working on components, pages, hooks, state management, or styling.4---56# React 19 + Vite + Tailwind v4 — Frontend conventions78## Structure910```11frontend/src/12├── pages/ # Page components (one per route)13├── components/ # ui/ primitives + [feature]/ folders14├── hooks/ # Custom hooks15├── services/ # API calls (fetch wrapper)16├── contexts/ # React contexts (auth, theme)17└── styles/ # Global CSS, Tailwind variables18```1920## Components2122- **One component per file.** No multi-export unless sub-components are colocated.23- **Max ~200 lines per component.** Beyond that, extract sub-components or hooks.24- **Destructure props** in the signature, with default values.25- **No business logic in components.** Extract to a hook or a service.2627```jsx28// GOOD29function ItemCard({ item, onFavorite, isFavorited = false }) { /* ... */ }3031// BAD — unstructured props, component doing too much32function ItemCard(props) {33 const [data, setData] = useState(null);34 useEffect(() => { fetch(`/api/items/${props.id}`); }, []);35}36```3738## Hooks3940- Prefix with `use`. One hook = one responsibility.41- Prefer returning a named object over an array (except simple two-value hooks like useState) — makes destructuring self-documenting. This is a team preference, not a framework rule.4243## State management4445- **React Context** for low-frequency state (theme, auth, locale). Context re-renders every consumer on every change — fine when changes are rare.46- **Zustand** (or similar) for high-frequency shared state that updates often. Selectors avoid the Context re-render problem.47- **Local state by default.** Lift only when necessary.4849## React 195051- **`use()` (React 19)** — unwraps a Promise or a Context inside render. Key constraints:52 - **Promise-only for data.** `use()` works on an already-existing Promise (or Context); it is **not** a general side-effect mechanism and is not a replacement for `useEffect`.53 - **Requires a parent `<Suspense>` boundary.** Calling `use(promise)` suspends the component; the closest `<Suspense>` ancestor catches the suspension and renders its `fallback`. No boundary → the suspension propagates up the tree and nothing renders (in a Vite app without Suspense wrappers, the whole subtree appears frozen).54 - **Use it when** a promise is passed as a prop from a Server Component, or handed down through an explicit Suspense boundary you control.55 - **For ad-hoc client-side fetching, use TanStack Query** (or SWR). Do not hand-roll fetch flows inside components.56- **`useActionState`** — wire forms to async actions, track pending/error state without manual `useState`.57- **`useFormStatus`** — read parent form's pending state from a child submit button, no prop drilling.58- **Server Components awareness** — even in a client-heavy Vite app, know the `"use client"` boundary. Eases any later migration to Next.js or React Router framework mode.59- **React Compiler / React Forget** — when enabled, it auto-memoizes and you can skip manual `useMemo`/`useCallback`. Check if the project has it enabled (look for `babel-plugin-react-compiler` or the `reactCompiler` Vite plugin). If it's NOT enabled, use `useMemo`/`useCallback` where the profiler shows a real need — don't add them preemptively, but don't ignore measurable perf issues either.6061## Tailwind v46263- Use the design system's CSS variables — no hardcoded values.64- Utility classes inline in JSX. `cn()` / `clsx()` for conditionals.65- Dark mode via `class` strategy. Mobile-first responsive.6667## Forms & API6869- React Hook Form + Zod resolver; share the Zod schema between frontend and backend when possible.70- Centralized `fetch` wrapper in `services/api.js` — no direct `fetch` inside components.71- Every call exposes loading + error states.7273## Accessibility (minimum)7475- `aria-label` on icon-only buttons, focus trapping in modals, sufficient contrast, keyboard nav.7677## Anti-patterns7879- ❌ `useEffect` to derive state — use `useMemo` or compute during render80- ❌ Props drilling deeper than 3 levels — use a Context or store81- ❌ `index` as key in dynamic lists82- ❌ Components over 200 lines without extraction83- ❌ `any` in TypeScript / JSDoc