rules/react/patterns.md
This file extends typescript/patterns.md and common/patterns.md with React specific content. For hook-specific rules see hooks.md.
Container components own data fetching, state, and side effects. Presentational components receive props and render — no service calls, no hooks beyond local UI state.
// Container — owns data
export function UserPage({ userId }: { userId: string }) {
const { data: user, isLoading } = useUser(userId);
if (isLoading) return <Spinner />;
if (!user) return <NotFound />;
return <UserCard user={user} onSelect={handleSelect} />;
}
// Presentational — pure
export function UserCard({ user, onSelect }: { user: User; onSelect: (id: string) => void }) {
return <button onClick={() => onSelect(user.id)}>{user.name}</button>;
}
useState inside itContext misused for frequently changing values causes every consumer to re-render on every update.
await directly"use client" at the top of the filechildren or named slots// Server (default)
export default async function Page() {
const user = await fetchUser();
return <UserClient user={user} />;
}
// Client
"use client";
export function UserClient({ user }: { user: User }) {
const [tab, setTab] = useState("profile");
return <Tabs value={tab} onChange={setTab}>{user.name}</Tabs>;
}
"server-only" packages (DB clients, secrets) from a Client Component file — wrap them in a Server Component or Server Actionimport "server-only" so the bundler errors if a client file imports themEvery Suspense boundary needs an Error Boundary above it. The pair handles both states.
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<Skeleton />}>
<UserDetails id={id} />
</Suspense>
</ErrorBoundary>
react-error-boundaryPrefer uncontrolled inputs with form actions when the form has a clear submit step. The browser owns the value; React reads it via FormData on submit.
async function action(formData: FormData) {
"use server";
await saveUser({ name: String(formData.get("name")) });
}
export function UserForm() {
return (
<form action={action}>
<input name="name" required />
<button type="submit">Save</button>
</form>
);
}
Use controlled inputs when the value drives other UI, requires real-time validation, or formatting.
const [email, setEmail] = useState("");
return <input value={email} onChange={(e) => setEmail(e.target.value)} />;
For complex forms (multi-step, dynamic field arrays, cross-field validation), use a library:
| Strategy | When |
|---|---|
RSC fetch (await in Server Component) | Per-request data in Next.js App Router, no client-side cache needed |
| TanStack Query | Client-side cache, mutations, optimistic updates, polling |
| SWR | Lightweight cache + revalidation, simpler than TanStack Query |
fetch in useEffect | Avoid — race conditions, no cache, no retry. Only acceptable for one-off fire-and-forget |
Never fetch in a useEffect when a real cache library is available — they handle deduping, cache invalidation, error retry, and Suspense integration.
key must be stable across renders — never index for any list that can reorder, insert, or deletekey must be unique among siblings, not globallychildren for slot-style compositionrenderItem={UserRow}For related controls (Tabs, Accordion, Menu), use compound components sharing state via Context:
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Profile</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><ProfileForm /></Tabs.Panel>
<Tabs.Panel value="settings"><SettingsForm /></Tabs.Panel>
</Tabs>
Use createPortal for modals, tooltips, toast containers — anything that must escape the parent's overflow: hidden or z-index stacking context. Render to a stable DOM node mounted in index.html.
React 19 lets function components accept ref as a regular prop — forwardRef is no longer required.
export function Input({ ref, ...rest }: { ref?: React.Ref<HTMLInputElement> } & InputProps) {
return <input ref={ref} {...rest} />;
}
Older codebases on React 18 still need forwardRef.
rules/nextjs/ trackPlatform.OS, .ios.tsx / .android.tsx), StyleSheet, navigation libraries (React Navigation, Expo Router)rules/react-native/ is not yet presentFor React-specific deep dives see skills/react-patterns/SKILL.md. For cross-framework frontend concerns see skills/frontend-patterns/SKILL.md. For accessibility see skills/accessibility/SKILL.md.