Problem Context
You started with class components β this.state, componentDidMount, this.setState. Then hooks landed in 2019 and the entire ecosystem rewired around function components. Now in 2026 with React 19 stable and the React Compiler in production, the rules have shifted again: useMemo and useCallback are mostly automatable, forwardRef is gone, and use()lets you read promises and context conditionally.
Most teams still write hooks like it's 2021 β over-memoizing, missing dependencies, calling them inside conditionals, and reaching foruseEffect when a derived value or event handler would do. This guide is the modern mental model for hooks under React 19.
- You wrap every callback in
useCallback"just in case" - Your
useEffectdependency array has 8 items and you can't remember why - You added
useMemoaround a value and didn't measure whether it actually helped - You're still using
forwardRefin new React 19 components - You reach for
useEffectto sync derived state instead of computing it during render
This is a React 19 hooks refresher built around the React Compiler era β what to write, what to delete, what changed.
Concept Explanation
A hook is a function that lets a function component "hook into" React features β state, lifecycle, refs, context. The rules are non-negotiable: hooks run in the same order on every render, so you call them at the top level (never inside conditions or loops) and only from React function components or other hooks. The order is how React matches a hook call to its slot in the fiber.
flowchart TB
A["Function Component renders"] --> B["Hook calls (in order)"]
B --> C["useState β local state"]
B --> D["useReducer β complex state"]
B --> E["useRef β mutable box"]
B --> F["useEffect β sync with external"]
B --> G["useMemo / useCallback β perf escape hatch"]
B --> H["use() β read promise/context"]
B --> I["useActionState β form state"]
B --> J["useOptimistic β pending UI"]
style A fill:#4f46e5,color:#fff,stroke:#4338ca
style H fill:#059669,color:#fff,stroke:#047857
style I fill:#059669,color:#fff,stroke:#047857
style J fill:#059669,color:#fff,stroke:#047857
The 2026 shift: with the React Compiler stable, manual useMemo/useCallback/React.memo are largely unnecessary in new code β the compiler memoizes automatically based on data flow. forwardRef is gone β ref is now a normal prop on function components. use() can read promises (suspends) or context (works conditionally). Server Actions changed how forms work via useActionState and useFormStatus.
Implementation
Step 1: useState β initialize lazy, update functional
// BAD β runs the heavy compute on every render
const [tree] = useState(buildHugeTree(props.input));
// GOOD β pass an initializer fn, runs once
const [tree] = useState(() => buildHugeTree(props.input));
// BAD β stale closure when updating from async/event
const [count, setCount] = useState(0);
const onClick = () => setCount(count + 1); // captured at render
// GOOD β functional updater always sees latest
const onClick = () => setCount(c => c + 1);Step 2: useEffectβ sync, don't orchestrate
Effects exist to synchronize the component with something outside React (DOM events, subscriptions, network). They are nota place to derive state, run on user actions, or chain effects.
// BAD β using useEffect to derive state from props
function Cart({ items }: { items: Item[] }) {
const [total, setTotal] = useState(0);
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price, 0));
}, [items]); // extra render + flicker
return <div>{total}</div>;
}
// GOOD β compute during render
function Cart({ items }: { items: Item[] }) {
const total = items.reduce((sum, i) => sum + i.price, 0);
return <div>{total}</div>;
}
// GOOD β useEffect for actual external sync
function ChatRoom({ roomId }: { roomId: string }) {
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect(); // cleanup is mandatory
}, [roomId]);
}Step 3: useRef β mutable box that survives renders
// DOM ref β passed via the ref prop (no forwardRef in React 19)
function TextInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
return <input ref={ref} />;
}
// Mutable instance value β does NOT trigger re-render
function Stopwatch() {
const intervalId = useRef<number | null>(null);
const startedAt = useRef<number>(0);
const start = () => {
startedAt.current = Date.now();
intervalId.current = window.setInterval(tick, 100);
};
// ...
}Step 4: use() β the React 19 universal reader
import { use, Suspense } from 'react';
// 1) Read a promise β suspends until resolved
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
// Parent passes the promise WITHOUT awaiting β child suspends
function Page() {
const promise = fetchComments();
return (
<Suspense fallback={<p>Loadingβ¦</p>}>
<Comments commentsPromise={promise} />
</Suspense>
);
}
// 2) Read context β unlike useContext, can be called conditionally
function Header() {
if (someCondition) return null;
const theme = use(ThemeContext); // legal, useContext would not be
return <header className={theme}>β¦</header>;
}Step 5: useActionState + useFormStatus for forms
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
async function signup(prevState: State, formData: FormData) {
const email = formData.get('email') as string;
const res = await fetch('/api/signup', { method: 'POST', body: formData });
return res.ok ? { ok: true } : { ok: false, error: 'Email already used' };
}
export function SignupForm() {
const [state, formAction, isPending] = useActionState(signup, { ok: false });
return (
<form action={formAction}>
<input name="email" type="email" required />
<SubmitButton />
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus(); // status of nearest parent form
return <button disabled={pending}>{pending ? 'Sendingβ¦' : 'Sign up'}</button>;
}Step 6: Custom hooks β extract behavior, not lines
// Extract when behavior is reused OR when naming improves the call site
function useOnlineStatus() {
const [online, setOnline] = useState(() => navigator.onLine);
useEffect(() => {
const on = () => setOnline(true);
const off = () => setOnline(false);
window.addEventListener('online', on);
window.addEventListener('offline', off);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', off);
};
}, []);
return online;
}
// Usage β call site reads like English
function StatusBar() {
const isOnline = useOnlineStatus();
return <span>{isOnline ? 'β Online' : 'β Offline'}</span>;
}Pitfalls
1. Over-memoizing. Wrapping every value in useMemo and every callback in useCallback adds bookkeeping cost and makes code harder to read. With the React Compiler stable, this is now actively harmful β the compiler does it better. Profile before reaching for these.
2. Missing dependencies. Lying to the dependency array (// eslint-disable-next-line) creates stale-closure bugs that only show up under specific timing. If a value isn't safe to depend on, lift it into a ref or restructure.
3. useEffect for everything. The most common React anti-pattern: using effects to derive state, fetch data on the client when a Server Component would, transform props, or run on user clicks. The official guide You Might Not Need an Effect still applies.
4. useState for non-rendering values. If a value never appears in JSX, it doesn't belong in state β useuseRef. Setting state you never read causes wasted re-renders.
5. Using forwardRef in new code. React 19 removed the need entirely β ref is a normal prop. Codemods exist for the migration. Keep forwardRef only in libraries that still target React 18.
Practical Takeaways
- Default to deriving values during render. Reach for
useEffectonly when you need to sync with something outside React. - Use the functional updater form of setters whenever the new value depends on the old one.
- With the React Compiler enabled, delete most
useMemo/useCallback/React.memoin new code. - Prefer
use(promise)+<Suspense>overuseEffect+useStatefor data fetching in Client Components that receive a promise from a Server Component. - For forms,
useActionState+useFormStatushandle 90% of cases without a state library. - Drop
forwardRefβ passrefas a prop in React 19. - Keep custom hooks small and named after the behavior (
useOnlineStatus), not the implementation (useEventListenerForOnlineEvents).

