| Pattern | Use | Avoid |
|---|---|---|
| Components | Function + default params | Class, defaultProps, prop-types |
| Refs | ref as prop |
forwardRef |
| Context | <Context value={...}> |
<Context.Provider> |
| Metadata | <title> in component |
<Head> component |
| Forms | <form action={serverFn}> |
onSubmit handler |
| Pending state | useFormStatus() |
Custom loading state |
| Form state | useActionState() |
Custom error state |
| Optimistic UI | useOptimistic() |
Manual state |
| Data fetching | use() + Suspense |
useEffect + loading state |
| Performance | React Compiler (auto) | useMemo, useCallback |
| Deferred values | useDeferredValue(val, initial) |
Custom debouncing |
// Default parameters instead of defaultProps
function Button({ variant = "primary", children }: ButtonProps) {
return <button className={variant}>{children}</button>;
}function Input({
ref,
...props
}: InputProps & { ref?: Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}<ThemeContext value={theme}>
<App />
</ThemeContext><div
ref={(node) => {
if (!node) return;
const observer = new ResizeObserver(() => {
/* handle resize */
});
observer.observe(node);
return () => observer.disconnect(); // Cleanup function
}}
/>Read Promises and Context with Suspense. No useEffect needed. Can be called in conditionals/loops (unlike other hooks).
import { use, Suspense } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}
// Parent creates promise, wraps in Suspense
<Suspense fallback={<Loading />}>
<UserProfile userPromise={fetchUser(id)} />
</Suspense>;Rules:
- Promise must come from props/context (not created in render)
- Errors bubble to nearest ErrorBoundary
- Works with Context too:
const theme = use(ThemeContext)
"use server";
async function createItem(formData: FormData) {
const name = formData.get("name") as string;
await repo.create({ name });
revalidatePath("/items");
}// Usage - progressive enhancement, works without JS
<form action={createItem}>
<input name="name" required />
<SubmitButton />
</form>Access parent form state. Must be in a child component of the form.
"use client";
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Saving..." : "Save"}
</button>
);
}Handle form state and validation errors.
"use client";
import { useActionState } from "react";
function CreateForm() {
const [state, formAction] = useActionState(createItem, {
message: "",
errors: {},
});
return (
<form action={formAction}>
<input name="name" required />
{state.errors.name && <p className="error">{state.errors.name}</p>}
<SubmitButton />
</form>
);
}// Server Function with validation
"use server";
async function createItem(prevState: State, formData: FormData) {
const name = formData.get("name") as string;
if (!name || name.length < 3) {
return {
message: "",
errors: { name: "Name must be at least 3 characters" },
};
}
await repo.create({ name });
return { message: "Created!", errors: {} };
}Instant UI feedback while async action runs.
"use client";
import { useOptimistic } from "react";
function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo],
);
async function addTodo(formData: FormData) {
const tempTodo = {
id: crypto.randomUUID(),
title: formData.get("title") as string,
completed: false,
};
addOptimistic(tempTodo);
await createTodo(tempTodo.title);
}
return (
<>
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<form action={addTodo}>
<input name="title" required />
<button>Add</button>
</form>
</>
);
}Compiler auto-memoizes. Do not add manual memoization (useMemo, useCallback, memo) unless:
- Justified with benchmark
- Compiler unavailable
- Extremely expensive operation
// Let compiler optimize - no manual memoization needed
function ExpensiveComponent({ data }: { data: Data[] }) {
const processed = data.map((item) => transform(item));
return (
<div>
{processed.map((item) => (
<Item key={item.id} item={item} />
))}
</div>
);
}Defer expensive re-renders. New initialValue option in React 19.
"use client";
import { useDeferredValue, useState } from "react";
function SearchResults() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query, ""); // initialValue for first render
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ExpensiveList query={deferredQuery} />
</>
);
}Prefer event handlers over useEffect. Use useEffect only for:
- Synchronizing with external systems (WebSocket, DOM APIs)
- Managing cleanup (event listeners, timers)
// Event handler for side effects (preferred)
function Component() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
logEvent("button_clicked", { count: count + 1 });
}
return <button onClick={handleClick}>Count: {count}</button>;
}React hoists <title>, <meta>, <link> to <head> automatically.
function ProductPage({ product }: { product: Product }) {
return (
<>
<title>{product.name} | My Store</title>
<meta name="description" content={product.description} />
<div>{/* Page content */}</div>
</>
);
}// React deduplicates and orders stylesheets by precedence
<link rel="stylesheet" href="/styles/component.css" precedence="default" />
// Async scripts are SSR-safe
<script async src="https://analytics.example.com/script.js" />New/experimental features:
- Activity: Pre-render hidden UI (
visible/hiddenmodes) without impacting visible performance - useEffectEvent: Extract event logic from effects without stale closure issues
- ViewTransition: Suspense animations during SSR
- Partial Pre-rendering: Pre-render static parts, resume for dynamic content
// Use element.props.ref (not element.ref)
function getChildRef(element: ReactElement) {
return element.props.ref;
}Use Testing Library + React.act. Avoid react-dom/test-utils and shallow renderer (removed).
import { render, screen } from "@testing-library/react";
test("renders component", () => {
render(<Component />);
expect(screen.getByText("Hello")).toBeInTheDocument();
});