|$ curl https://forge-ai.dev/api/markdown?path=docs/react/hooks
$cat docs/react-hooks.md
updated Recently·45 min read·published

React Hooks

ReactHooksBeginner to Advanced🎯Free Tools
Introduction

Hooks are functions that let you "hook into" React state and lifecycle features from function components. Introduced in React 16.8, hooks eliminated the need for class components in most cases. They let you reuse stateful logic between components without changing your component hierarchy.

Hooks follow two fundamental rules: only call hooks at the top level (never inside loops, conditions, or nested functions), and only call hooks from React function components or custom hooks. These rules ensure hooks work correctly across re-renders.

rules-of-hooks.jsx
JSX
1// The Rules of Hooks
2// 1. Only call hooks at the top level
3// 2. Only call hooks from React functions
4
5// ✅ Correct
6function MyComponent() {
7 const [count, setCount] = useState(0); // top level
8 useEffect(() => {}, []); // top level
9 return <div>{count}</div>;
10}
11
12// ❌ Wrong — hook inside condition
13function BadComponent({ show }) {
14 if (show) {
15 const [value, setValue] = useState(0); // conditional!
16 }
17}
18
19// ❌ Wrong — hook inside loop
20function BadListComponent({ items }) {
21 items.forEach(item => {
22 const [selected, setSelected] = useState(false); // in loop!
23 });
24}
useState

useState declares a state variable. It returns a pair: the current state value and a function to update it. The state persists across re-renders. When the setter is called, React re-renders the component with the new value.

usestate.jsx
JSX
1import { useState } from "react";
2
3function Counter() {
4 const [count, setCount] = useState(0);
5
6 return (
7 <div>
8 <p>Count: {count}</p>
9 <button onClick={() => setCount(count + 1)}>Increment</button>
10 <button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
11 <button onClick={() => setCount(0)}>Reset</button>
12 </div>
13 );
14}

For complex state updates, always use the functional updater form to avoid stale closures:

usestate-advanced.jsx
JSX
1function TodoApp() {
2 const [todos, setTodos] = useState([]);
3
4 // ✅ Functional updater — uses latest state
5 const addTodo = (text) => {
6 setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
7 };
8
9 const toggleTodo = (id) => {
10 setTodos(prev =>
11 prev.map(todo =>
12 todo.id === id ? { ...todo, done: !todo.done } : todo
13 )
14 );
15 };
16
17 const removeTodo = (id) => {
18 setTodos(prev => prev.filter(todo => todo.id !== id));
19 };
20
21 // ❌ Direct state reference — may be stale in async
22 const addTodoBad = (text) => {
23 setTodos([...todos, { id: Date.now(), text }]); // stale todos!
24 };
25
26 return (
27 <ul>
28 {todos.map(todo => (
29 <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
30 {todo.done ? "✓" : "○"} {todo.text}
31 </li>
32 ))}
33 </ul>
34 );
35}

info

Always use the functional updater form (setX(prev => ...)) when the new state depends on the previous state. This avoids bugs from stale closures, especially inside event handlers and effects.
useEffect

useEffect lets you perform side effects in function components: data fetching, subscriptions, DOM manipulation, timers, and more. It runs after render and can return a cleanup function.

useeffect.jsx
JSX
1import { useState, useEffect } from "react";
2
3function UserProfile({ userId }) {
4 const [user, setUser] = useState(null);
5 const [loading, setLoading] = useState(true);
6
7 useEffect(() => {
8 let cancelled = false;
9
10 async function fetchUser() {
11 setLoading(true);
12 try {
13 const res = await fetch(`/api/users/${userId}`);
14 const data = await res.json();
15 if (!cancelled) setUser(data);
16 } catch (err) {
17 console.error("Failed to fetch user:", err);
18 } finally {
19 if (!cancelled) setLoading(false);
20 }
21 }
22
23 fetchUser();
24
25 // Cleanup function — runs before next effect or unmount
26 return () => { cancelled = true; };
27 }, [userId]); // Re-run when userId changes
28
29 if (loading) return <p>Loading...</p>;
30 if (!user) return <p>User not found</p>;
31
32 return (
33 <div>
34 <h2>{user.name}</h2>
35 <p>{user.email}</p>
36 </div>
37 );
38}

Common useEffect patterns and their dependency arrays:

useeffect-patterns.jsx
JSX
1// Run on every render (no dependency array)
2useEffect(() => {
3 console.log("Rendered");
4});
5
6// Run once on mount
7useEffect(() => {
8 console.log("Mounted");
9 return () => console.log("Unmounted");
10}, []);
11
12// Run when specific values change
13useEffect(() => {
14 document.title = `Count: ${count}`;
15}, [count]);
16
17// Run when userId changes (with cleanup)
18useEffect(() => {
19 const subscription = subscribe(userId);
20 return () => subscription.unsubscribe();
21}, [userId]);
22
23// Subscribing to browser events
24useEffect(() => {
25 const handler = (e) => console.log("Key:", e.key);
26 window.addEventListener("keydown", handler);
27 return () => window.removeEventListener("keydown", handler);
28}, []);

warning

Never omit the dependency array when your effect references values from the component scope. Missing dependencies cause stale closures where the effect uses outdated values. Use the exhaustive-deps ESLint rule to catch these bugs automatically.
useReducer

useReducer is an alternative to useState for complex state logic. It uses a reducer function (similar to Redux) where actions describe what happened and the reducer computes the next state.

usereducer.jsx
JSX
1import { useReducer } from "react";
2
3const initialState = { items: [], total: 0 };
4
5function cartReducer(state, action) {
6 switch (action.type) {
7 case "ADD_ITEM": {
8 const existing = state.items.find(i => i.id === action.item.id);
9 if (existing) {
10 return {
11 ...state,
12 items: state.items.map(i =>
13 i.id === action.item.id ? { ...i, quantity: i.quantity + 1 } : i
14 ),
15 total: state.total + action.item.price,
16 };
17 }
18 return {
19 ...state,
20 items: [...state.items, { ...action.item, quantity: 1 }],
21 total: state.total + action.item.price,
22 };
23 }
24 case "REMOVE_ITEM": {
25 const item = state.items.find(i => i.id === action.id);
26 return {
27 ...state,
28 items: state.items.filter(i => i.id !== action.id),
29 total: state.total - (item?.price * item?.quantity || 0),
30 };
31 }
32 case "CLEAR_CART":
33 return initialState;
34 default:
35 return state;
36 }
37}
38
39function ShoppingCart() {
40 const [state, dispatch] = useReducer(cartReducer, initialState);
41
42 return (
43 <div>
44 <h2>Cart ({state.items.length} items)</h2>
45 {state.items.map(item => (
46 <div key={item.id}>
47 {item.name} x{item.quantity}
48 <button onClick={() => dispatch({ type: "REMOVE_ITEM", id: item.id })}>
49 Remove
50 </button>
51 </div>
52 ))}
53 <p>Total: ${state.total.toFixed(2)}</p>
54 <button onClick={() => dispatch({ type: "CLEAR_CART" })}>Clear Cart</button>
55 </div>
56 );
57}

best practice

Use useReducer when state logic is complex, involves multiple sub-values, or when the next state depends on the previous one in complicated ways. For simple state, useState is cleaner and more direct.
useContext

useContext reads and subscribes to a React Context. It accepts a context object (created by React.createContext) and returns the current context value. The component re-renders when the provider value changes.

usecontext.jsx
JSX
1import { createContext, useContext, useState } from "react";
2
3const ThemeContext = createContext("light");
4
5function ThemeProvider({ children }) {
6 const [theme, setTheme] = useState("light");
7
8 const toggleTheme = () => {
9 setTheme(prev => prev === "light" ? "dark" : "light");
10 };
11
12 return (
13 <ThemeContext.Provider value={{ theme, toggleTheme }}>
14 {children}
15 </ThemeContext.Provider>
16 );
17}
18
19function ThemedButton() {
20 const { theme, toggleTheme } = useContext(ThemeContext);
21
22 return (
23 <button
24 onClick={toggleTheme}
25 className={theme === "dark" ? "bg-gray-800 text-white" : "bg-white text-gray-800"}
26 >
27 Current theme: {theme}. Click to toggle.
28 </button>
29 );
30}
31
32function App() {
33 return (
34 <ThemeProvider>
35 <ThemedButton />
36 <ThemedButton /> {/* Both stay in sync via context */}
37 </ThemeProvider>
38 );
39}

warning

Context causes all consumers to re-render when the provider value changes, even if they don't use the changed part. For performance-sensitive contexts, split them into smaller contexts or use a state management library like Zustand.
useRef

useRef creates a mutable ref object whose .currentproperty persists across re-renders without causing re-renders when it changes. It's used for DOM references, storing previous values, and holding mutable values that don't trigger re-renders.

useref.jsx
JSX
1import { useRef, useState, useEffect } from "react";
2
3function TextInput() {
4 const inputRef = useRef(null);
5
6 const focusInput = () => {
7 inputRef.current?.focus();
8 };
9
10 return (
11 <div>
12 <input ref={inputRef} type="text" placeholder="Type here..." />
13 <button onClick={focusInput}>Focus Input</button>
14 </div>
15 );
16}
17
18// Storing previous value
19function usePrevious(value) {
20 const ref = useRef();
21
22 useEffect(() => {
23 ref.current = value;
24 }, [value]);
25
26 return ref.current;
27}
28
29function Counter() {
30 const [count, setCount] = useState(0);
31 const prevCount = usePrevious(count);
32
33 return (
34 <div>
35 <p>Now: {count}, Before: {prevCount}</p>
36 <button onClick={() => setCount(c => c + 1)}>Increment</button>
37 </div>
38 );
39}
40
41// Storing interval ID without re-rendering
42function Timer() {
43 const [seconds, setSeconds] = useState(0);
44 const intervalRef = useRef(null);
45
46 const start = () => {
47 intervalRef.current = setInterval(() => {
48 setSeconds(s => s + 1);
49 }, 1000);
50 };
51
52 const stop = () => {
53 clearInterval(intervalRef.current);
54 };
55
56 return (
57 <div>
58 <p>Seconds: {seconds}</p>
59 <button onClick={start}>Start</button>
60 <button onClick={stop}>Stop</button>
61 </div>
62 );
63}
useMemo

useMemo memoizes the result of an expensive computation. It only recalculates when one of its dependencies changes. Use it for expensive calculations, filtering/sorting large lists, and creating stable object references.

usememo.jsx
JSX
1import { useMemo, useState } from "react";
2
3function ProductList({ products, filter, sortBy }) {
4 const [search, setSearch] = useState("");
5
6 // Expensive computation — only recalculates when inputs change
7 const filteredProducts = useMemo(() => {
8 console.log("Recomputing filtered products...");
9 return products
10 .filter(p => p.category === filter)
11 .filter(p => p.name.toLowerCase().includes(search.toLowerCase()))
12 .sort((a, b) => {
13 if (sortBy === "price") return a.price - b.price;
14 if (sortBy === "name") return a.name.localeCompare(b.name);
15 return 0;
16 });
17 }, [products, filter, search, sortBy]);
18
19 return (
20 <div>
21 <input
22 value={search}
23 onChange={e => setSearch(e.target.value)}
24 placeholder="Search products..."
25 />
26 <p>{filteredProducts.length} products found</p>
27 {filteredProducts.map(product => (
28 <ProductCard key={product.id} product={product} />
29 ))}
30 </div>
31 );
32}

info

Don't memoize everything. useMemo has overhead — it stores the previous result and compares dependencies. Only use it for genuinely expensive computations (sorting 1000+ items, complex filtering, or when passing objects as props to memoized children).
useCallback

useCallback returns a memoized version of a callback function. It only changes when its dependencies change. This prevents unnecessary re-renders of child components that receive the function as a prop.

usecallback.jsx
JSX
1import { useState, useCallback, memo } from "react";
2
3// Without useCallback — new function reference every render
4function ParentUnoptimized() {
5 const [count, setCount] = useState(0);
6
7 const handleClick = () => console.log("clicked");
8 // handleClick is a new reference every render
9
10 return (
11 <div>
12 <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
13 <ExpensiveChild onClick={handleClick} /> {/* re-renders every time! */}
14 </div>
15 );
16}
17
18// With useCallback — stable function reference
19function ParentOptimized() {
20 const [count, setCount] = useState(0);
21
22 const handleClick = useCallback(() => {
23 console.log("clicked");
24 }, []); // No dependencies, never changes
25
26 return (
27 <div>
28 <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
29 <ExpensiveChild onClick={handleClick} /> {/* only re-renders once */}
30 </div>
31 );
32}
33
34const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
35 console.log("ExpensiveChild rendered");
36 return <button onClick={onClick}>Click me</button>;
37});

best practice

useCallback only makes a difference when the callback is passed to a memoized child component (React.memo). Without memo, the child re-renders regardless of the callback's reference stability.
Custom Hooks

Custom hooks are functions that start with "use" and can call other hooks. They let you extract component logic into reusable functions. Custom hooks share stateful logic, not state itself — each call gets its own state.

custom-hooks.jsx
JSX
1import { useState, useEffect, useCallback } from "react";
2
3// Custom hook: useFetch
4function useFetch(url, options = {}) {
5 const [data, setData] = useState(null);
6 const [error, setError] = useState(null);
7 const [loading, setLoading] = useState(true);
8
9 useEffect(() => {
10 const controller = new AbortController();
11
12 async function fetchData() {
13 setLoading(true);
14 try {
15 const res = await fetch(url, { ...options, signal: controller.signal });
16 if (!res.ok) throw new Error(`HTTP ${res.status}`);
17 const json = await res.json();
18 setData(json);
19 } catch (err) {
20 if (err.name !== "AbortError") setError(err);
21 } finally {
22 setLoading(false);
23 }
24 }
25
26 fetchData();
27 return () => controller.abort();
28 }, [url]);
29
30 return { data, error, loading };
31}
32
33// Custom hook: useLocalStorage
34function useLocalStorage(key, initialValue) {
35 const [value, setValue] = useState(() => {
36 try {
37 const stored = localStorage.getItem(key);
38 return stored ? JSON.parse(stored) : initialValue;
39 } catch {
40 return initialValue;
41 }
42 });
43
44 useEffect(() => {
45 localStorage.setItem(key, JSON.stringify(value));
46 }, [key, value]);
47
48 return [value, setValue];
49}
50
51// Custom hook: useDebounce
52function useDebounce(value, delay = 300) {
53 const [debouncedValue, setDebouncedValue] = useState(value);
54
55 useEffect(() => {
56 const timer = setTimeout(() => setDebouncedValue(value), delay);
57 return () => clearTimeout(timer);
58 }, [value, delay]);
59
60 return debouncedValue;
61}
62
63// Using custom hooks together
64function SearchApp() {
65 const [query, setQuery] = useLocalStorage("search-query", "");
66 const debouncedQuery = useDebounce(query, 500);
67 const { data, loading, error } = useFetch(
68 debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
69 );
70
71 return (
72 <div>
73 <input value={query} onChange={e => setQuery(e.target.value)} />
74 {loading && <p>Searching...</p>}
75 {error && <p>Error: {error.message}</p>}
76 {data && <SearchResults results={data} />}
77 </div>
78 );
79}
🔥

pro tip

Name custom hooks starting with "use" — this convention lets React's linter enforce hook rules. Popular custom hooks to build: useDebounce, useMediaQuery, useIntersectionObserver, useClickOutside, usePrevious.
Other Built-in Hooks

React provides additional hooks for specific use cases:

other-hooks.jsx
JSX
1import { useId, useTransition, useDeferredValue, useLayoutEffect,
2 useImperativeHandle, forwardRef } from "react";
3
4// useId — stable unique IDs for accessibility
5function FormField({ label }) {
6 const id = useId();
7 return (
8 <div>
9 <label htmlFor={id}>{label}</label>
10 <input id={id} type="text" />
11 </div>
12 );
13}
14
15// useTransition — mark non-urgent updates
16function TabContainer() {
17 const [isPending, startTransition] = useTransition();
18 const [tab, setTab] = useState("home");
19
20 const selectTab = (nextTab) => {
21 startTransition(() => {
22 setTab(nextTab);
23 });
24 };
25
26 return (
27 <div>
28 <TabButtons onSelect={selectTab} />
29 {isPending && <Spinner />}
30 <TabContent activeTab={tab} />
31 </div>
32 );
33}
34
35// useDeferredValue — defer rendering of non-urgent content
36function SearchResults({ query }) {
37 const deferredQuery = useDeferredValue(query);
38 return <ResultsList query={deferredQuery} />;
39}
40
41// useImperativeHandle — customize ref handle
42const FancyInput = forwardRef(function FancyInput(props, ref) {
43 const inputRef = useRef(null);
44
45 useImperativeHandle(ref, () => ({
46 focus: () => inputRef.current?.focus(),
47 clear: () => { inputRef.current.value = ""; },
48 }));
49
50 return <input ref={inputRef} {...props} />;
51});
📝

note

useTransition and useDeferredValue are React 18+ concurrent features. They let you keep the UI responsive during expensive updates by deprioritizing certain renders.