Skip to main content

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

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 level
function Component() {
const [state, setState] = useState(0);
useEffect(() => {}, []);
return <div>{state}</div>;
}
// ❌ Wrong - hook in condition
function Component() {
if (condition) {
const [state, setState] = useState(0); // Error!
}
return <div>...</div>;
}
// ❌ Wrong - hook in loop
function 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 usage
function 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 state
function 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 state
function 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:

PatternUse CaseExample
Direct valueSimple updatessetCount(5)
Functional updateDepends on prev statesetCount(prev => prev + 1)
Object spreadUpdate object propertysetState(prev => ({...prev, key: value}))
Array spreadAdd to arraysetItems(prev => [...prev, item])
Array filterRemove from arraysetItems(prev => prev.filter(x => x.id !== id))

useReducer 🎯

Complex state logic with reducer pattern (like Redux).

import { useReducer } from "react";
// Reducer function
function 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 usage
function 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 function
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(
counterReducer,
{ count: initialCount },
(initialState) => {
// Lazy initialization
return { count: initialState.count * 2 };
},
);
return <div>{state.count}</div>;
}
// Form reducer example
function 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:

useStateuseReducer
Simple stateComplex state logic
Single valueMultiple related values
Direct updatesMultiple update types
Local component stateState 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 render
function Component() {
useEffect(() => {
console.log("Component rendered");
});
return <div>Hello</div>;
}
// Empty dependency array - runs once on mount
function Component() {
useEffect(() => {
console.log("Component mounted");
// Cleanup runs on unmount
return () => {
console.log("Component unmounted");
};
}, []); // Empty array = run once
}
// With dependencies - runs when deps change
function 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 effects
function 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 effects
function Component({ shouldFetch }) {
useEffect(() => {
if (shouldFetch) {
fetchData();
}
}, [shouldFetch]);
return <div>...</div>;
}
// Async operations in useEffect
function 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 ArrayBehavior
[]Run once on mount, cleanup on unmount
[dep1, dep2]Run when any dependency changes
No arrayRun 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 flicker
function 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 measurements

useEffect vs useLayoutEffect:

useEffectuseLayoutEffect
AsynchronousSynchronous
After paintBefore paint
Most use casesDOM measurements, prevent flicker
Non-blockingBlocking

Context & Ref Hooks

useContext πŸ—‚οΈ

Access React Context without prop drilling.

import { createContext, useContext, useState } from "react";
// Create context
const ThemeContext = createContext("light"); // Default value
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumer component
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
style={{ background: theme === "light" ? "#fff" : "#000" }}
>
Toggle Theme
</button>
);
}
// Usage
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
);
}
// Multiple contexts
const 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 wrapper
function 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 reference
function 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 tracking
function 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 pattern
function 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:

useRefuseState
Mutable .current propertyImmutable state
No re-render on changeTriggers re-render
DOM referencesComponent state
Previous value trackingCurrent value

useImperativeHandle πŸŽ›οΈ

Customize instance value exposed to parent via ref (use with forwardRef).

import { forwardRef, useImperativeHandle, useRef } from "react";
// Custom input with exposed methods
const 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} />;
});
// Usage
function 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 calculation
function 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 stability
function Component({ userId }) {
const userConfig = useMemo(
() => ({
id: userId,
theme: "dark",
preferences: {},
}),
[userId],
); // New object only when userId changes
return <ChildComponent config={userConfig} />;
}
// Complex calculation
function 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 useMemoDon’t use useMemo
Expensive calculationsSimple operations
Reference equality neededPrimitive values
Prevent child re-rendersAlready optimized
Large array transformationsSmall datasets

useCallback 🎣

Memoize function references to prevent unnecessary re-renders.

import { useCallback, useState, memo } from "react";
// Without useCallback - new function every render
function 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 dependencies
function 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 parameters
function 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:

useCallbackuseMemo
Memoizes functionsMemoizes values
Returns functionReturns computed value
useCallback(fn, deps)useMemo(() => fn, deps)
Prevent child re-rendersCache 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 transitions
function 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 submission
function 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 validation
function 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 JavaScript
function 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 APIOld name, deprecated
[state, action, isPending][state, action]
Includes isPending flagNo pending state
RecommendedUse 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 component
function SubmitButton({ children }) {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : children}
</button>
);
}
// Usage in form
function ContactForm() {
return (
<form action={handleSubmit}>
<input name="email" type="email" />
<textarea name="message" />
<SubmitButton>Send Message</SubmitButton>
</form>
);
}
// Loading indicator component
function FormLoadingIndicator() {
const { pending } = useFormStatus();
if (!pending) return null;
return (
<div className="loading">
<Spinner />
<span>Processing...</span>
</div>
);
}
// Error display component
function 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 example
function 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 method
function 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:

PropertyTypeDescription
pendingbooleanWhether form is submitting
dataFormData | nullForm data being submitted
method'get' | 'post'HTTP method
actionstring | ((formData: FormData) => void) | nullForm action

⚠️ Important: useFormStatus must 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 update
function 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 updates
function 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 update
function 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 updates
function 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 useOptimisticDon’t use useOptimistic
Immediate UI feedback neededCan wait for server response
User-initiated actionsBackground sync operations
Like/favorite buttonsCritical financial transactions
Add/remove list itemsOperations 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 Promise
function 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 Suspense
function App() {
const userPromise = fetchUser(123);
return (
<Suspense fallback={<div>Loading user...</div>}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Reading from Context
const 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 reading
function 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 Boundary
function App() {
return (
<ErrorBoundary fallback={<ErrorDisplay />}>
<Suspense fallback={<Loading />}>
<DataComponent dataPromise={fetchData()} />
</Suspense>
</ErrorBoundary>
);
}
// Reading from multiple promises
function 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 rendering
function 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);
}
// Usage
function Component() {
const data = useAsyncData(fetchData());
return <div>{data.value}</div>;
}

use() vs useEffect:

use()useEffect
Reads in renderRuns after render
Suspends componentNo suspension
Works with SuspenseManual loading states
Simpler async codeMore 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:

HookPurposeReact Version
useActionStateForm actions & async state19+
useFormStatusForm submission status19+
useOptimisticOptimistic UI updates19+
useRead Promises & Context19+

Custom Hooks

Creating Custom Hooks 🎨

Extract and reuse stateful logic.

// Custom hook: useCounter
function 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 };
}
// Usage
function 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: useFetch
function 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 };
}
// Usage
function 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: useLocalStorage
function 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];
}
// Usage
function Component() {
const [name, setName] = useLocalStorage("name", "");
return <input value={name} onChange={(e) => setName(e.target.value)} />;
}
// Custom hook: useDebounce
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function 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: usePrevious
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// Usage
function Component({ count }) {
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
</div>
);
}

Custom Hook Naming:

  • βœ… Always start with use prefix: 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 };
}
// Usage
function 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];
}
// Usage
function 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;
}
// Usage
function 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;
}
// Usage
function 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 hooks
function useUserData(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
return user;
}
// βœ… Use functional updates for state depending on prev state
setCount((prev) => prev + 1);
// βœ… Include all dependencies in dependency arrays
useEffect(() => {
fetchData(id, filter);
}, [id, filter]); // Include all dependencies
// βœ… Clean up subscriptions and timers
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, []);
// βœ… Use useMemo/useCallback for expensive operations
const expensiveValue = useMemo(() => compute(value), [value]);
const memoizedCallback = useCallback(() => doSomething(), [dep]);
// βœ… Use useReducer for complex state logic
const [state, dispatch] = useReducer(reducer, initialState);
// βœ… Name custom hooks with 'use' prefix
function useCustomHook() {
/* ... */
}

❌ Don’ts

// ❌ Don't call hooks conditionally
if (condition) {
const [state, setState] = useState(0); // Error!
}
// ❌ Don't forget dependencies
useEffect(() => {
fetchData(id); // Missing 'id' in dependency array
}, []); // Should be [id]
// ❌ Don't create new objects/arrays in render without memoization
function 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 directly
const [items, setItems] = useState([]);
items.push(newItem); // Wrong! Use setItems([...items, newItem])
// ❌ Don't use refs for values that should trigger re-renders
const countRef = useRef(0);
countRef.current = countRef.current + 1; // Won't trigger re-render
// ❌ Don't forget cleanup in useEffect
useEffect(() => {
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 problem
function 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 loop
function 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 render
function 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: