React Hooks Cheatsheet
Comprehensive reference for React hooks API including useState, useEffect, useContext, React 19 hooks (useActionState, useFormStatus, useOptimistic, use), and custom hooks patterns
Table of Contents
- Built-in Hooks
- State Management Hooks
- Effect Hooks
- Context & Ref Hooks
- Performance Hooks
- React 19 Hooks
- Custom Hooks
- Common Patterns
- Best Practices
Built-in Hooks
Prerequisites π
- React 16.8+ (hooks introduced)
- React 18+ recommended for concurrent features
- React 19+ for latest hooks (useActionState, useFormStatus, useOptimistic, use)
- Functional components only (hooks donβt work in class components)
Hook Rules β οΈ
Critical: Hooks must be called at the top level of your component. Never call hooks inside loops, conditions, or nested functions.
// β
Correct - hooks at top levelfunction Component() { const [state, setState] = useState(0); useEffect(() => {}, []); return <div>{state}</div>;}
// β Wrong - hook in conditionfunction Component() { if (condition) { const [state, setState] = useState(0); // Error! } return <div>...</div>;}
// β Wrong - hook in loopfunction Component() { for (let i = 0; i < 10; i++) { useEffect(() => {}, []); // Error! } return <div>...</div>;}State Management Hooks
useState π
Basic state management hook for functional components.
import { useState } from "react";
// Basic usagefunction Counter() { const [count, setCount] = useState(0);
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <button onClick={() => setCount(count - 1)}>Decrement</button> <button onClick={() => setCount(0)}>Reset</button> </div> );}
// Functional updates (prev state)function Counter() { const [count, setCount] = useState(0);
const increment = () => { setCount((prevCount) => prevCount + 1); // Use prev state };
return <button onClick={increment}>{count}</button>;}
// Object statefunction Form() { const [formData, setFormData] = useState({ name: "", email: "", });
const handleChange = (field) => (e) => { setFormData((prev) => ({ ...prev, [field]: e.target.value, })); };
return ( <form> <input value={formData.name} onChange={handleChange("name")} /> <input value={formData.email} onChange={handleChange("email")} /> </form> );}
// Array statefunction TodoList() { const [todos, setTodos] = useState([]);
const addTodo = (text) => { setTodos((prev) => [...prev, { id: Date.now(), text }]); };
const removeTodo = (id) => { setTodos((prev) => prev.filter((todo) => todo.id !== id)); };
return ( <div> {todos.map((todo) => ( <div key={todo.id}> {todo.text} <button onClick={() => removeTodo(todo.id)}>Remove</button> </div> ))} </div> );}
// Lazy initialization (expensive computation)function ExpensiveComponent() { const [data, setData] = useState(() => { // This function runs only once on mount return computeExpensiveValue(); });
return <div>{data}</div>;}Common Patterns:
| Pattern | Use Case | Example |
|---|---|---|
| Direct value | Simple updates | setCount(5) |
| Functional update | Depends on prev state | setCount(prev => prev + 1) |
| Object spread | Update object property | setState(prev => ({...prev, key: value})) |
| Array spread | Add to array | setItems(prev => [...prev, item]) |
| Array filter | Remove from array | setItems(prev => prev.filter(x => x.id !== id)) |
useReducer π―
Complex state logic with reducer pattern (like Redux).
import { useReducer } from "react";
// Reducer functionfunction counterReducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; case "reset": return { count: 0 }; case "set": return { count: action.payload }; default: throw new Error(`Unknown action: ${action.type}`); }}
// Basic usagefunction Counter() { const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: "increment" })}>+</button> <button onClick={() => dispatch({ type: "decrement" })}>-</button> <button onClick={() => dispatch({ type: "reset" })}>Reset</button> <button onClick={() => dispatch({ type: "set", payload: 10 })}> Set to 10 </button> </div> );}
// With initializer functionfunction Counter({ initialCount }) { const [state, dispatch] = useReducer( counterReducer, { count: initialCount }, (initialState) => { // Lazy initialization return { count: initialState.count * 2 }; }, );
return <div>{state.count}</div>;}
// Form reducer examplefunction formReducer(state, action) { switch (action.type) { case "SET_FIELD": return { ...state, [action.field]: action.value, }; case "RESET": return { name: "", email: "", message: "" }; default: return state; }}
function ContactForm() { const [formData, dispatch] = useReducer(formReducer, { name: "", email: "", message: "", });
return ( <form> <input value={formData.name} onChange={(e) => dispatch({ type: "SET_FIELD", field: "name", value: e.target.value, }) } /> {/* ... */} </form> );}When to use useReducer vs useState:
| useState | useReducer |
|---|---|
| Simple state | Complex state logic |
| Single value | Multiple related values |
| Direct updates | Multiple update types |
| Local component state | State machine pattern |
Effect Hooks
useEffect π¬
Side effects in functional components (data fetching, subscriptions, DOM manipulation).
import { useEffect, useState } from "react";
// Basic usage - runs after every renderfunction Component() { useEffect(() => { console.log("Component rendered"); });
return <div>Hello</div>;}
// Empty dependency array - runs once on mountfunction Component() { useEffect(() => { console.log("Component mounted"); // Cleanup runs on unmount return () => { console.log("Component unmounted"); }; }, []); // Empty array = run once}
// With dependencies - runs when deps changefunction UserProfile({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); // Runs when userId changes
return <div>{user?.name}</div>;}
// Cleanup function (subscriptions, timers)function Timer() { const [seconds, setSeconds] = useState(0);
useEffect(() => { const interval = setInterval(() => { setSeconds((prev) => prev + 1); }, 1000);
// Cleanup: clear interval on unmount return () => clearInterval(interval); }, []);
return <div>{seconds}s</div>;}
// Multiple effectsfunction Component() { useEffect(() => { // Effect 1: Setup subscription const subscription = subscribe(); return () => subscription.unsubscribe(); }, []);
useEffect(() => { // Effect 2: Update document title document.title = "My App"; }, []);
return <div>...</div>;}
// Conditional effectsfunction Component({ shouldFetch }) { useEffect(() => { if (shouldFetch) { fetchData(); } }, [shouldFetch]);
return <div>...</div>;}
// Async operations in useEffectfunction DataFetcher({ url }) { const [data, setData] = useState(null); const [error, setError] = useState(null);
useEffect(() => { let cancelled = false;
async function fetchData() { try { const response = await fetch(url); const json = await response.json(); if (!cancelled) { setData(json); } } catch (err) { if (!cancelled) { setError(err); } } }
fetchData();
return () => { cancelled = true; // Prevent state update if unmounted }; }, [url]);
if (error) return <div>Error: {error.message}</div>; if (!data) return <div>Loading...</div>; return <div>{JSON.stringify(data)}</div>;}Dependency Array Rules:
| Dependency Array | Behavior |
|---|---|
[] | Run once on mount, cleanup on unmount |
[dep1, dep2] | Run when any dependency changes |
| No array | Run after every render (usually avoid) |
[undefined] | Runs every render (undefined always changes) |
useLayoutEffect π
Synchronous version of useEffect - runs before browser paint.
import { useLayoutEffect, useState } from "react";
// Use when you need to measure DOM or prevent flickerfunction Tooltip({ children }) { const [position, setPosition] = useState({ top: 0, left: 0 }); const tooltipRef = useRef();
useLayoutEffect(() => { // Runs synchronously before paint const rect = tooltipRef.current.getBoundingClientRect(); setPosition({ top: rect.top, left: rect.left, }); }, []);
return ( <div ref={tooltipRef} style={{ position: "absolute", ...position }}> {children} </div> );}
// β οΈ Prefer useEffect unless you need synchronous DOM measurementsuseEffect vs useLayoutEffect:
| useEffect | useLayoutEffect |
|---|---|
| Asynchronous | Synchronous |
| After paint | Before paint |
| Most use cases | DOM measurements, prevent flicker |
| Non-blocking | Blocking |
Context & Ref Hooks
useContext ποΈ
Access React Context without prop drilling.
import { createContext, useContext, useState } from "react";
// Create contextconst ThemeContext = createContext("light"); // Default value
// Provider componentfunction ThemeProvider({ children }) { const [theme, setTheme] = useState("light");
return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> );}
// Consumer componentfunction ThemedButton() { const { theme, setTheme } = useContext(ThemeContext);
return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")} style={{ background: theme === "light" ? "#fff" : "#000" }} > Toggle Theme </button> );}
// Usagefunction App() { return ( <ThemeProvider> <ThemedButton /> </ThemeProvider> );}
// Multiple contextsconst UserContext = createContext(null);const SettingsContext = createContext(null);
function Component() { const user = useContext(UserContext); const settings = useContext(SettingsContext);
return ( <div> {user?.name} - {settings?.theme} </div> );}
// Custom hook wrapperfunction useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be used within ThemeProvider"); } return context;}useRef π―
Mutable reference that persists across renders without causing re-renders.
import { useRef, useEffect } from "react";
// DOM referencefunction TextInput() { const inputRef = useRef(null);
const focusInput = () => { inputRef.current?.focus(); };
return ( <div> <input ref={inputRef} type="text" /> <button onClick={focusInput}>Focus Input</button> </div> );}
// Mutable value (doesn't trigger re-render)function Timer() { const [count, setCount] = useState(0); const intervalRef = useRef(null);
useEffect(() => { intervalRef.current = setInterval(() => { setCount((prev) => prev + 1); }, 1000);
return () => clearInterval(intervalRef.current); }, []);
const stopTimer = () => { clearInterval(intervalRef.current); };
return ( <div> <p>{count}</p> <button onClick={stopTimer}>Stop</button> </div> );}
// Previous value trackingfunction Component({ value }) { const prevValueRef = useRef();
useEffect(() => { prevValueRef.current = value; });
const prevValue = prevValueRef.current;
return ( <div> <p>Current: {value}</p> <p>Previous: {prevValue}</p> </div> );}
// Callback ref patternfunction Component() { const [height, setHeight] = useState(0); const measuredRef = useCallback((node) => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []);
return ( <div ref={measuredRef}> <p>Height: {height}px</p> </div> );}useRef vs useState:
| useRef | useState |
|---|---|
Mutable .current property | Immutable state |
| No re-render on change | Triggers re-render |
| DOM references | Component state |
| Previous value tracking | Current value |
useImperativeHandle ποΈ
Customize instance value exposed to parent via ref (use with forwardRef).
import { forwardRef, useImperativeHandle, useRef } from "react";
// Custom input with exposed methodsconst CustomInput = forwardRef((props, ref) => { const inputRef = useRef(null);
useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, clear: () => { inputRef.current.value = ""; }, getValue: () => { return inputRef.current?.value; }, }));
return <input ref={inputRef} {...props} />;});
// Usagefunction Form() { const inputRef = useRef(null);
const handleSubmit = () => { const value = inputRef.current?.getValue(); console.log(value); };
return ( <form> <CustomInput ref={inputRef} /> <button onClick={handleSubmit}>Submit</button> </form> );}Performance Hooks
useMemo πΎ
Memoize expensive computations.
import { useMemo, useState } from "react";
// Expensive calculationfunction ExpensiveComponent({ items }) { const [filter, setFilter] = useState("");
// Recompute only when items or filter changes const filteredItems = useMemo(() => { return items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase()), ); }, [items, filter]);
return ( <div> <input value={filter} onChange={(e) => setFilter(e.target.value)} /> <ul> {filteredItems.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> </div> );}
// Object/array reference stabilityfunction Component({ userId }) { const userConfig = useMemo( () => ({ id: userId, theme: "dark", preferences: {}, }), [userId], ); // New object only when userId changes
return <ChildComponent config={userConfig} />;}
// Complex calculationfunction Fibonacci({ n }) { const result = useMemo(() => { if (n <= 1) return n; let a = 0, b = 1; for (let i = 2; i <= n; i++) { [a, b] = [b, a + b]; } return b; }, [n]);
return ( <div> Fibonacci({n}) = {result} </div> );}When to use useMemo:
| Use useMemo | Donβt use useMemo |
|---|---|
| Expensive calculations | Simple operations |
| Reference equality needed | Primitive values |
| Prevent child re-renders | Already optimized |
| Large array transformations | Small datasets |
useCallback π£
Memoize function references to prevent unnecessary re-renders.
import { useCallback, useState, memo } from "react";
// Without useCallback - new function every renderfunction Parent() { const [count, setCount] = useState(0);
const handleClick = useCallback(() => { console.log("Clicked"); }, []); // Stable reference
return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <ChildComponent onClick={handleClick} /> </div> );}
// Child component (memoized)const ChildComponent = memo(({ onClick }) => { console.log("Child rendered"); return <button onClick={onClick}>Click me</button>;});
// With dependenciesfunction SearchBox({ onSearch }) { const [query, setQuery] = useState("");
const handleSearch = useCallback(() => { onSearch(query); }, [query, onSearch]); // Recreate when query or onSearch changes
return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <button onClick={handleSearch}>Search</button> </div> );}
// Event handler with parametersfunction TodoList({ todos }) { const handleToggle = useCallback((id) => { // Toggle logic }, []);
return ( <ul> {todos.map((todo) => ( <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} /> ))} </ul> );}useCallback vs useMemo:
| useCallback | useMemo |
|---|---|
| Memoizes functions | Memoizes values |
| Returns function | Returns computed value |
useCallback(fn, deps) | useMemo(() => fn, deps) |
| Prevent child re-renders | Cache expensive calculations |
useTransition π
Mark state updates as non-urgent (React 18+).
import { useTransition, useState } from "react";
function SearchResults({ query }) { const [isPending, startTransition] = useTransition(); const [results, setResults] = useState([]);
const handleSearch = (newQuery) => { startTransition(() => { // Non-urgent update - React can interrupt setResults(expensiveSearch(newQuery)); }); };
return ( <div> {isPending && <div>Searching...</div>} <ul> {results.map((result) => ( <li key={result.id}>{result.name}</li> ))} </ul> </div> );}
// Multiple transitionsfunction Component() { const [isPending, startTransition] = useTransition(); const [items, setItems] = useState([]);
const loadMore = () => { startTransition(() => { setItems((prev) => [...prev, ...fetchMoreItems()]); }); };
return ( <div> {isPending && <Spinner />} <ItemList items={items} /> <button onClick={loadMore}>Load More</button> </div> );}useDeferredValue β±οΈ
Defer updating a value (React 18+).
import { useDeferredValue, useState, useMemo } from "react";
function SearchResults({ query }) { const deferredQuery = useDeferredValue(query);
// Expensive computation uses deferred value const results = useMemo(() => { return expensiveSearch(deferredQuery); }, [deferredQuery]);
return ( <div> {query !== deferredQuery && <div>Searching...</div>} <ResultsList results={results} /> </div> );}React 19 Hooks
useActionState π―
Manage state from form actions and async operations (React 19+). Replaces useFormState.
import { useActionState } from "react";
// Basic form submissionfunction ContactForm() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { // Action function receives previous state and form data const email = formData.get("email"); const message = formData.get("message");
// Simulate API call const response = await fetch("/api/contact", { method: "POST", body: JSON.stringify({ email, message }), });
if (!response.ok) { return { error: "Failed to send message" }; }
return { success: true, message: "Message sent!" }; }, null, // Initial state );
return ( <form action={formAction}> <input name="email" type="email" required /> <textarea name="message" required /> <button type="submit" disabled={isPending}> {isPending ? "Sending..." : "Send"} </button> {state?.error && <div className="error">{state.error}</div>} {state?.success && <div className="success">{state.success}</div>} </form> );}
// With validationfunction LoginForm() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { const email = formData.get("email"); const password = formData.get("password");
// Validation if (!email || !password) { return { error: "All fields required" }; }
try { const response = await fetch("/api/login", { method: "POST", body: JSON.stringify({ email, password }), });
const data = await response.json();
if (!response.ok) { return { error: data.message }; }
return { success: true, user: data.user }; } catch (error) { return { error: "Network error" }; } }, { error: null, success: false }, );
return ( <form action={formAction}> <input name="email" type="email" /> <input name="password" type="password" /> <button type="submit" disabled={isPending}> {isPending ? "Logging in..." : "Login"} </button> {state.error && <div>{state.error}</div>} </form> );}
// Progressive enhancement - works without JavaScriptfunction SearchForm() { const [state, formAction, isPending] = useActionState( async (prevState, formData) => { const query = formData.get("query"); const results = await searchAPI(query); return { results, query }; }, { results: [], query: "" }, );
return ( <form action={formAction}> <input name="query" defaultValue={state.query} /> <button type="submit" disabled={isPending}> Search </button> {isPending && <div>Searching...</div>} {state.results.length > 0 && ( <ul> {state.results.map((result) => ( <li key={result.id}>{result.title}</li> ))} </ul> )} </form> );}useActionState vs useFormState:
| useActionState (React 19) | useFormState (React 19, deprecated) |
|---|---|
| New name, same API | Old name, deprecated |
[state, action, isPending] | [state, action] |
Includes isPending flag | No pending state |
| Recommended | Use useActionState instead |
useFormStatus π
Access form submission status from child components (React 19+). Must be used within a <form> element.
import { useFormStatus } from "react-dom";
// Submit button componentfunction SubmitButton({ children }) { const { pending, data, method, action } = useFormStatus();
return ( <button type="submit" disabled={pending}> {pending ? "Submitting..." : children} </button> );}
// Usage in formfunction ContactForm() { return ( <form action={handleSubmit}> <input name="email" type="email" /> <textarea name="message" /> <SubmitButton>Send Message</SubmitButton> </form> );}
// Loading indicator componentfunction FormLoadingIndicator() { const { pending } = useFormStatus();
if (!pending) return null;
return ( <div className="loading"> <Spinner /> <span>Processing...</span> </div> );}
// Error display componentfunction FormError() { const { pending, data } = useFormStatus();
if (pending || !data) return null;
const error = data.get("error"); if (!error) return null;
return <div className="error">{error}</div>;}
// Complete form examplefunction LoginForm() { return ( <form action={loginAction}> <input name="email" type="email" required /> <input name="password" type="password" required />
<FormError /> <FormLoadingIndicator />
<SubmitButton>Login</SubmitButton> </form> );}
// Access form data and methodfunction FormDebug() { const { pending, data, method, action } = useFormStatus();
return ( <details> <summary>Form Status</summary> <pre> {JSON.stringify( { pending, method, action: action?.toString(), data: data ? Object.fromEntries(data) : null, }, null, 2, )} </pre> </details> );}useFormStatus Properties:
| Property | Type | Description |
|---|---|---|
pending | boolean | Whether form is submitting |
data | FormData | null | Form data being submitted |
method | 'get' | 'post' | HTTP method |
action | string | ((formData: FormData) => void) | null | Form action |
β οΈ Important:
useFormStatusmust be called inside a component that is a descendant of a<form>. It will throw an error if used outside a form context.
useOptimistic β‘
Optimistic UI updates for async operations (React 19+). Shows immediate feedback while request is pending.
import { useOptimistic, useState } from "react";
// Like button with optimistic updatefunction LikeButton({ postId, initialLikes }) { const [optimisticLikes, addOptimisticLike] = useOptimistic( initialLikes, (state, newLike) => state + 1, // Optimistic update function );
async function handleLike() { // Immediately update UI optimistically addOptimisticLike(1);
try { const response = await fetch(`/api/posts/${postId}/like`, { method: "POST", });
if (!response.ok) { throw new Error("Like failed"); }
// Server response will update state automatically } catch (error) { // On error, React reverts to previous state console.error("Failed to like:", error); } }
return <button onClick={handleLike}>β€οΈ {optimisticLikes}</button>;}
// Todo list with optimistic updatesfunction TodoList({ todos: initialTodos }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( initialTodos, (state, newTodo) => [...state, { ...newTodo, id: "temp-" + Date.now() }], );
async function addTodo(text) { const newTodo = { id: null, text, completed: false };
// Optimistically add todo addOptimisticTodo(newTodo);
try { const response = await fetch("/api/todos", { method: "POST", body: JSON.stringify({ text }), });
const savedTodo = await response.json(); // Replace optimistic todo with server response // (This happens automatically when state updates) } catch (error) { console.error("Failed to add todo:", error); // React reverts to previous state on error } }
return ( <div> {optimisticTodos.map((todo) => ( <div key={todo.id}> {todo.text} {todo.id?.startsWith("temp-") && <span> (saving...)</span>} </div> ))} </div> );}
// Comment deletion with optimistic updatefunction CommentList({ comments: initialComments }) { const [optimisticComments, updateOptimistic] = useOptimistic( initialComments, (state, { type, id }) => { if (type === "delete") { return state.filter((comment) => comment.id !== id); } return state; }, );
async function deleteComment(id) { // Optimistically remove comment updateOptimistic({ type: "delete", id });
try { await fetch(`/api/comments/${id}`, { method: "DELETE" }); } catch (error) { console.error("Failed to delete:", error); // Reverts on error } }
return ( <ul> {optimisticComments.map((comment) => ( <li key={comment.id}> {comment.text} <button onClick={() => deleteComment(comment.id)}>Delete</button> </li> ))} </ul> );}
// Shopping cart with optimistic updatesfunction ShoppingCart({ items: initialItems }) { const [optimisticItems, updateOptimistic] = useOptimistic( initialItems, (state, { action, item }) => { switch (action) { case "add": return [...state, { ...item, id: "temp-" + Date.now() }]; case "remove": return state.filter((i) => i.id !== item.id); case "update": return state.map((i) => (i.id === item.id ? item : i)); default: return state; } }, );
async function addItem(product) { updateOptimistic({ action: "add", item: product });
try { await fetch("/api/cart", { method: "POST", body: JSON.stringify(product), }); } catch (error) { console.error("Failed to add item:", error); } }
return ( <div> {optimisticItems.map((item) => ( <CartItem key={item.id} item={item} /> ))} </div> );}useOptimistic Pattern:
const [optimisticState, updateOptimistic] = useOptimistic( initialState, (currentState, optimisticValue) => { // Return new state based on optimistic update return /* new state */; },);When to use useOptimistic:
| Use useOptimistic | Donβt use useOptimistic |
|---|---|
| Immediate UI feedback needed | Can wait for server response |
| User-initiated actions | Background sync operations |
| Like/favorite buttons | Critical financial transactions |
| Add/remove list items | Operations requiring confirmation |
use Hook π
Read values from Promises and Context directly in render (React 19+). Simplifies async data handling.
import { use, Suspense } from "react";
// Reading from Promisefunction UserProfile({ userPromise }) { // use() automatically suspends until promise resolves const user = use(userPromise);
return ( <div> <h1>{user.name}</h1> <p>{user.email}</p> </div> );}
// Usage with Suspensefunction App() { const userPromise = fetchUser(123);
return ( <Suspense fallback={<div>Loading user...</div>}> <UserProfile userPromise={userPromise} /> </Suspense> );}
// Reading from Contextconst ThemeContext = createContext("light");
function ThemedButton() { // use() can read context directly const theme = use(ThemeContext);
return ( <button style={{ background: theme === "light" ? "#fff" : "#000" }}> Themed Button </button> );}
// Conditional context readingfunction ConditionalContext() { const theme = use(ThemeContext); const user = use(UserContext); // Can use multiple contexts
return ( <div> Theme: {theme}, User: {user?.name} </div> );}
// Error handling with use()function DataComponent({ dataPromise }) { try { const data = use(dataPromise); return <div>{data.content}</div>; } catch (error) { // Handle promise rejection return <div>Error: {error.message}</div>; }}
// With Error Boundaryfunction App() { return ( <ErrorBoundary fallback={<ErrorDisplay />}> <Suspense fallback={<Loading />}> <DataComponent dataPromise={fetchData()} /> </Suspense> </ErrorBoundary> );}
// Reading from multiple promisesfunction Dashboard({ userPromise, postsPromise, statsPromise }) { const user = use(userPromise); const posts = use(postsPromise); const stats = use(statsPromise);
return ( <div> <h1>Welcome, {user.name}</h1> <PostList posts={posts} /> <StatsDisplay stats={stats} /> </div> );}
// use() with conditional renderingfunction ConditionalData({ shouldLoad, dataPromise }) { if (!shouldLoad) { return <div>Not loading</div>; }
// use() only called when shouldLoad is true const data = use(dataPromise); return <div>{data.content}</div>;}
// Custom hook with use()function useAsyncData(promise) { return use(promise);}
// Usagefunction Component() { const data = useAsyncData(fetchData()); return <div>{data.value}</div>;}use() vs useEffect:
| use() | useEffect |
|---|---|
| Reads in render | Runs after render |
| Suspends component | No suspension |
| Works with Suspense | Manual loading states |
| Simpler async code | More boilerplate |
| React 19+ | React 16.8+ |
use() Rules:
- β Can read Promises (suspends until resolved)
- β Can read Context (no Provider needed for default values)
- β Must be wrapped in Suspense for Promise usage
- β Can use try/catch for error handling
- β Cannot be called conditionally (same as other hooks)
- β Promise must be stable reference (use useMemo if needed)
React 19 Hooks Summary:
| Hook | Purpose | React Version |
|---|---|---|
useActionState | Form actions & async state | 19+ |
useFormStatus | Form submission status | 19+ |
useOptimistic | Optimistic UI updates | 19+ |
use | Read Promises & Context | 19+ |
Custom Hooks
Creating Custom Hooks π¨
Extract and reuse stateful logic.
// Custom hook: useCounterfunction useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue);
const increment = useCallback(() => { setCount((prev) => prev + 1); }, []);
const decrement = useCallback(() => { setCount((prev) => prev - 1); }, []);
const reset = useCallback(() => { setCount(initialValue); }, [initialValue]);
return { count, increment, decrement, reset };}
// Usagefunction Counter() { const { count, increment, decrement, reset } = useCounter(0);
return ( <div> <p>{count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> );}
// Custom hook: useFetchfunction useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { let cancelled = false;
async function fetchData() { try { setLoading(true); const response = await fetch(url); if (!response.ok) throw new Error("Fetch failed"); const json = await response.json(); if (!cancelled) { setData(json); setError(null); } } catch (err) { if (!cancelled) { setError(err); } } finally { if (!cancelled) { setLoading(false); } } }
fetchData();
return () => { cancelled = true; }; }, [url]);
return { data, loading, error };}
// Usagefunction UserProfile({ userId }) { const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>; return <div>{user?.name}</div>;}
// Custom hook: useLocalStoragefunction useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { return initialValue; } });
const setValue = useCallback( (value) => { try { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(error); } }, [key], );
return [storedValue, setValue];}
// Usagefunction Component() { const [name, setName] = useLocalStorage("name", "");
return <input value={name} onChange={(e) => setName(e.target.value)} />;}
// Custom hook: useDebouncefunction useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay);
return () => { clearTimeout(handler); }; }, [value, delay]);
return debouncedValue;}
// Usagefunction SearchBox() { const [query, setQuery] = useState(""); const debouncedQuery = useDebounce(query, 500);
useEffect(() => { if (debouncedQuery) { performSearch(debouncedQuery); } }, [debouncedQuery]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;}
// Custom hook: usePreviousfunction usePrevious(value) { const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current;}
// Usagefunction Component({ count }) { const prevCount = usePrevious(count);
return ( <div> <p>Current: {count}</p> <p>Previous: {prevCount}</p> </div> );}Custom Hook Naming:
- β
Always start with
useprefix:useCounter,useFetch,useLocalStorage - β Return values/objects/functions as needed
- β Can use other hooks inside
- β Share stateful logic between components
Common Patterns
Form Handling π
function useForm(initialValues) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({});
const handleChange = useCallback( (name) => (e) => { const value = e.target.value; setValues((prev) => ({ ...prev, [name]: value })); // Clear error when user types if (errors[name]) { setErrors((prev) => ({ ...prev, [name]: "" })); } }, [errors], );
const handleSubmit = useCallback( (onSubmit) => (e) => { e.preventDefault(); onSubmit(values); }, [values], );
return { values, errors, handleChange, handleSubmit, setErrors };}
// Usagefunction LoginForm() { const { values, handleChange, handleSubmit } = useForm({ email: "", password: "", });
const onSubmit = (formData) => { console.log("Submit:", formData); };
return ( <form onSubmit={handleSubmit(onSubmit)}> <input name="email" value={values.email} onChange={handleChange("email")} /> <input name="password" type="password" value={values.password} onChange={handleChange("password")} /> <button type="submit">Login</button> </form> );}Toggle Hook π
function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => { setValue((prev) => !prev); }, []);
const setTrue = useCallback(() => { setValue(true); }, []);
const setFalse = useCallback(() => { setValue(false); }, []);
return [value, toggle, setTrue, setFalse];}
// Usagefunction Component() { const [isOpen, toggle, open, close] = useToggle(false);
return ( <div> <button onClick={toggle}>Toggle</button> <button onClick={open}>Open</button> <button onClick={close}>Close</button> {isOpen && <Modal />} </div> );}Intersection Observer Hook ποΈ
function useIntersectionObserver(ref, options = {}) { const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => { const element = ref.current; if (!element) return;
const observer = new IntersectionObserver(([entry]) => { setIsIntersecting(entry.isIntersecting); }, options);
observer.observe(element);
return () => { observer.disconnect(); }; }, [ref, options]);
return isIntersecting;}
// Usagefunction LazyImage({ src }) { const imgRef = useRef(null); const isVisible = useIntersectionObserver(imgRef);
return ( <img ref={imgRef} src={isVisible ? src : undefined} alt="Lazy loaded" /> );}Window Size Hook π
function useWindowSize() { const [size, setSize] = useState({ width: window.innerWidth, height: window.innerHeight, });
useEffect(() => { const handleResize = () => { setSize({ width: window.innerWidth, height: window.innerHeight, }); };
window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []);
return size;}
// Usagefunction ResponsiveComponent() { const { width, height } = useWindowSize();
return ( <div> <p> Window: {width}x{height} </p> {width < 768 && <MobileView />} {width >= 768 && <DesktopView />} </div> );}Best Practices
β Doβs
// β
Extract logic into custom hooksfunction useUserData(userId) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); return user;}
// β
Use functional updates for state depending on prev statesetCount((prev) => prev + 1);
// β
Include all dependencies in dependency arraysuseEffect(() => { fetchData(id, filter);}, [id, filter]); // Include all dependencies
// β
Clean up subscriptions and timersuseEffect(() => { const subscription = subscribe(); return () => subscription.unsubscribe();}, []);
// β
Use useMemo/useCallback for expensive operationsconst expensiveValue = useMemo(() => compute(value), [value]);const memoizedCallback = useCallback(() => doSomething(), [dep]);
// β
Use useReducer for complex state logicconst [state, dispatch] = useReducer(reducer, initialState);
// β
Name custom hooks with 'use' prefixfunction useCustomHook() { /* ... */}β Donβts
// β Don't call hooks conditionallyif (condition) { const [state, setState] = useState(0); // Error!}
// β Don't forget dependenciesuseEffect(() => { fetchData(id); // Missing 'id' in dependency array}, []); // Should be [id]
// β Don't create new objects/arrays in render without memoizationfunction Component({ items }) { const config = { theme: "dark" }; // New object every render return <Child config={config} />; // Causes unnecessary re-renders}
// β Don't use useMemo/useCallback everywhere (premature optimization)const simpleValue = useMemo(() => a + b, [a, b]); // Unnecessary
// β Don't mutate state directlyconst [items, setItems] = useState([]);items.push(newItem); // Wrong! Use setItems([...items, newItem])
// β Don't use refs for values that should trigger re-rendersconst countRef = useRef(0);countRef.current = countRef.current + 1; // Won't trigger re-render
// β Don't forget cleanup in useEffectuseEffect(() => { const timer = setInterval(() => {}, 1000); // Missing: return () => clearInterval(timer);}, []);β οΈ Common Pitfalls
Stale Closures: Values captured in closures may be stale. Use functional updates or include dependencies.
// β οΈ Stale closure problemfunction Component() { const [count, setCount] = useState(0);
useEffect(() => { const interval = setInterval(() => { setCount(count + 1); // Uses stale 'count' value }, 1000); return () => clearInterval(interval); }, []); // Missing 'count' dependency
// β
Fix: Use functional update useEffect(() => { const interval = setInterval(() => { setCount((prev) => prev + 1); // Always uses latest value }, 1000); return () => clearInterval(interval); }, []);}Infinite Loops: Updating state in useEffect without proper dependencies causes infinite loops.
// β οΈ Infinite loopfunction Component() { const [count, setCount] = useState(0);
useEffect(() => { setCount(count + 1); // Triggers re-render, which triggers effect again }); // Missing dependency array
// β
Fix: Add proper dependencies or use functional update useEffect(() => { setCount((prev) => prev + 1); }, []); // Run once on mount}Object/Array Dependencies: Objects and arrays are compared by reference, not value.
// β οΈ Effect runs every renderfunction Component({ config }) { useEffect(() => { doSomething(config); }, [config]); // 'config' is new object every render
// β
Fix: Memoize or use specific properties useEffect(() => { doSomething(config); }, [config.id, config.theme]); // Use specific properties}Performance Checklist: