React — Rules and Conventions
1. Philosophy
- Functional components only — No class components. Hooks for all state/logic.
- Server Components by default — Use
'use client'only when needed (interactivity, hooks, browser APIs). - TypeScript strict — All components typed. Props interfaces exported.
- Composition over inheritance — Build UI from small, composable components.
- Performance conscious — Memoize only when measured. Trust the compiler (React 19+).
2. Version Baseline
| Technology | Version |
|---|---|
| React | 18.3+ (19 RC) |
| React DOM | 18.3+ |
| React Router | 7.0+ |
| React Hook Form | 7.51+ |
| TypeScript | 5.4+ |
| Node.js | 22+ |
3. Setup & Project Structure
Vite (SPA)
pnpm create vite@latest my-app -- --template react-ts
src/
├── app/ # App shell, providers
├── components/ # Shared UI components
├── features/ # Feature-specific components + logic
├── hooks/ # Shared custom hooks
├── lib/ # Utilities, API clients
├── routes/ # Route components (lazy loaded)
├── styles/ # Global CSS, Tailwind entry
├── main.tsx # Entry point
└── vite-env.d.ts
Next.js (App Router)
pnpm create next-app@latest my-app --typescript --tailwind --eslint
src/
├── app/ # Routes + Server Components
├── components/ # Client components ('use client')
├── lib/ # Utilities, DB, auth
├── hooks/ # Shared hooks
└── types/ # Global types
4. Functional Components
// Component with props interface
interface ButtonProps {
variant: "primary" | "secondary";
onClick: () => void;
children: React.ReactNode;
disabled?: boolean;
}
export function Button({ variant, onClick, children, disabled }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
disabled={disabled}
>
{children}
</button>
);
}
Rules
- Named exports —
export function Component() - Props interface — co-located, exported
childrentyped —React.ReactNode- Optional props —
?with default in destructuring
5. Hooks — Rules & Closures
Rules of Hooks
- Top level only — no loops, conditions, nested functions
- Only in React functions — components, custom hooks
- Name with
useprefix —useCustomHook
Closure pitfalls
// ❌ Stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []); // Missing count dependency
}
// ✅ Functional update
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []); // No deps needed
}
6. Essential Hooks Patterns
State
const [count, setCount] = useState(0);
const [user, setUser] = useState<User | null>(null);
// Lazy init (expensive computation)
const [data, setData] = useState(() => expensiveInitial());
Side Effects
// Mount only
useEffect(() => {
const subscription = api.subscribe();
return () => subscription.unsubscribe();
}, []);
// With deps
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// Cleanup sync (React 18+)
useEffect(() => {
return () => {
/* cleanup */
};
}, [dep]);
Context
// ThemeContext.tsx
const ThemeContext = createContext<ThemeContextType>(defaultValue);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Usage
const { theme, setTheme } = useContext(ThemeContext);
Reducer (complex state)
interface State {
status: "idle" | "loading" | "success" | "error";
data: Data[];
}
type Action =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: Data[] }
| { type: "FETCH_ERROR" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_START":
return { ...state, status: "loading" };
case "FETCH_SUCCESS":
return { status: "success", data: action.payload };
case "FETCH_ERROR":
return { status: "error", data: [] };
}
}
const [state, dispatch] = useReducer(reducer, initialState);
Refs
// DOM ref
const inputRef = useRef<HTMLInputElement>(null)
<input ref={inputRef} />
// Mutable value (no re-render)
const renderCount = useRef(0)
renderCount.current++
Memoization
// Referential equality
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b])
const memoizedCallback = useCallback(() => doSomething(a), [a])
// Component memo
const MemoizedChild = memo(function Child({ a, b }) { ... })
Transitions (React 18+)
function SearchResults({ query }: { query: string }) {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
startTransition(() => {
setResults(search(value)); // Urgent: input update
});
}
// ...
}
Deferred Value (React 18+)
function SearchResults({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(deferredQuery), [deferredQuery]);
// ...
}
Optimistic Updates (React 19+)
function LikeButton({ postId }: { postId: string }) {
const [likes, setLikes] = useState(0);
const [optimistic, setOptimistic] = useOptimistic(
likes,
(state, action: "like" | "unlike") =>
action === "like" ? state + 1 : state - 1,
);
async function handleClick() {
setOptimistic("like");
try {
await api.like(postId);
setLikes((l) => l + 1);
} catch {
setOptimistic("unlike"); // Rollback
}
}
return <button ❤️</button>;
}
Form Status / Action State (React 19+)
function LoginForm() {
const { pending, data, method, action } = useFormStatus();
// or useActionState for server actions
}
7. Conditional Rendering
// Ternary
{
isLoggedIn ? <Dashboard /> : <Login />;
}
// Short-circuit
{
isLoading && <Spinner />;
}
// Nullish coalescing
{
data?.items ?? [];
}
// Early return
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <Content data={data} />;
8. Lists & Keys
// ✅ Stable, unique ID
{
items.map((item) => <Item key={item.id} {...item} />);
}
// ❌ Index as key (breaks with reorder)
{
items.map((item, i) => <Item key={i} {...item} />);
}
// ❌ Random/non-unique
{
items.map((item) => <Item key={Math.random()} {...item} />);
}
9. Controlled vs Uncontrolled
// Controlled (React owns state)
function ControlledInput() {
const [value, setValue] = useState("");
return <input value={value} => setValue(e.target.value)} />;
}
// Uncontrolled (DOM owns state)
function UncontrolledInput() {
const ref = useRef<HTMLInputElement>(null);
return <input ref={ref} defaultValue="initial" />;
}
// Use controlled for: forms, validation, dependent fields
// Use uncontrolled for: simple inputs, file inputs, integration with non-React
10. Context API
// Create context with undefined default (forces provider)
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const login = async (creds) => {
/* ... */
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
// Hook for consumption
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error("useAuth must be used within AuthProvider");
return context;
}
Rules Context API
- Split contexts — separate rarely-changing from frequently-changing
- Memoize provider value —
useMemofor object values - Custom hook for consumption — enforces provider usage
11. Forms (React Hook Form)
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: "", password: "" },
});
const (data: FormData) => {
await api.login(data);
};
return (
<form
<input {...register("email")} type="email" placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input {...register("password")} type="password" placeholder="Password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Loading..." : "Login"}
</button>
</form>
);
}
12. React Router v7
// routes.tsx
import { createBrowserRouter } from "react-router";
export const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ index: true, element: <Home /> },
{ path: "about", element: <About /> },
{ path: "dashboard", element: <Dashboard /> },
{ path: "settings", element: <Settings /> },
],
},
{ path: "*", element: <NotFound /> },
]);
// main.tsx
import { RouterProvider } from "react-router";
import { router } from "./routes";
createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />,
);
Data loading (loaders)
const router = createBrowserRouter([
{
path: "/posts/:id",
element: <PostDetail />,
loader: async ({ params }) => {
const post = await api.getPost(params.id);
return post;
},
},
]);
// In component
function PostDetail() {
const post = useLoaderData<Post>();
// ...
}
Actions (mutations)
<Form method="post" action="/posts">
<input name="title" />
<button type="submit">Create</button>
</Form>;
// Server action
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
await api.createPost(Object.fromEntries(formData));
return redirect("/posts");
}
13. Performance
Full performance rules: see
performanceskill.
Key patterns
// Memoize component
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <ComplexView data={data} />;
});
// Memoize callbacks passed to memoized children
function Parent() {
const handleClick = useCallback(() => {
/* ... */
}, [dep]);
return <ExpensiveComponent />;
}
// Virtualize long lists
import { FixedSizeList } from "react-window";
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => <div style={style}>{items[index].name}</div>}
</FixedSizeList>
);
}
14. Code Splitting
// Route-level (React Router v7)
const router = createBrowserRouter([
{
path: "/admin",
lazy: () =>
import("./routes/admin").then((m) => ({ default: m.AdminLayout })),
},
]);
// Component-level
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
);
}
Build config: see
viteandesbuildskills.
15. Server Components (Next.js)
Full Next.js patterns: see
nextjsskill.
// app/posts/page.tsx (Server Component by default)
async function PostsPage() {
const posts = await db.posts.findMany(); // Direct DB access
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Client Component (interactivity)
("use client");
export function LikeButton({ postId }) {
const [likes, setLikes] = useState(0);
// ...
}
Rules Server Components
- Default to Server Components — data fetching, static content
'use client'only when needed — hooks, browser APIs, interactivity- Pass data as props — Server → Client
- Actions for mutations — Server Actions
16. Error Handling
// Error Boundary (class component required)
class ErrorBoundary extends Component<
{ children: React.ReactNode },
{ error: Error | null }
> {
state = { error: null };
static getDerivedStateFromError(error: Error) {
return { error };
}
componentDidCatch(error: Error, info: ErrorInfo) {
logError(error, info);
}
render() {
if (this.state.error) return <ErrorFallback error={this.state.error} />;
return this.props.children;
}
}
// Usage
<ErrorBoundary fallback={<ErrorFallback />}>
<App />
</ErrorBoundary>;
React 19+ (useActionState for forms)
function Form() {
const [state, action] = useActionState(
async (prev, formData) => {
try {
await api.submit(formData);
return { success: true };
} catch (e) {
return { error: e.message };
}
},
{ success: false, error: null },
);
return <form action={action}>...</form>;
}
17. Methodology
Before using ANY React pattern not documented in this skill:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor React, React Router, React Hook Form. - Official docs: react.dev, reactrouter.com, react-hook-form.com — verify current APIs.
- Project config:
package.json,tsconfig.json, router config — verify against actual setup. - HARD RULE: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.
18. Prohibitions
- ❌ Do not use class components — functional + hooks only
- ❌ Do not mutate state directly —
setState(prev => ...) - ❌ Do not use
useEffectfor data fetching without cleanup - ❌ Do not pass inline objects to memoized children
—
style={{}},onClick={() => {}} - ❌ Do not use
useMemo/useCallbackeverywhere — only when measured - ❌ Do not skip
keyin lists — use stable IDs - ❌ Do not use context for high-frequency updates — splits contexts
- ❌ Do not use
'use client'by default — Server Components first - ❌ Do not call hooks conditionally — Rules of Hooks
19. References
Note: For HTML conventions (JSX semantics), see HTML Note: For CSS conventions (styling), see CSS Note: For JavaScript conventions, see JavaScript Note: For TypeScript rules, see TypeScript Note: For Performance rules, see Performance Note: For Next.js patterns, see Next.js Note: For Vite config, see Vite Note: For Tailwind CSS, see Tailwind CSS Note: For Sass, see Sass Note: For Component Design, see Component Design
Last updated: 2026-08