Why Every Hook Matters
Most tutorials only cover useState, useEffect, and maybe useContext. But React ships with 19 hooks, and each one exists to solve a specific, real problem.
In this guide, every hook gets its own dedicated section with a thorough explanation, working code examples, and the gotchas that trip up even experienced developers.
Rule of Hooks: Always call hooks at the top level of your component. Never call hooks inside loops, conditions, or nested functions. The only exception is the new use() hook in React 19.
Let's start with the original 10 hooks from React 16.8, then move to the concurrent rendering hooks of React 18, and finish with the groundbreaking new hooks in React 19.
1. useState — Adding State to Components
The useState hook lets you add a state variable to your component. It returns an array with two values: the current state and a setter function to update it. This is the most fundamental hook in React.
Functional Updates (Avoiding Stale State)
React batches state updates. If you call setCount(count + 1) twice in the same event handler, both calls read the same stale value of count, so it only increments by 1. To fix this, pass a callback function that receives the previous state: setCount(prev => prev + 1). This guarantees each update builds on the latest value.
No Auto-Merging (Unlike Class setState)
In class components, this.setState({ age: 30 }) would merge with existing state. But useState completely replaces the state value. If your state is an object, you must spread the old state manually: setUser(prev => ({ ...prev, age: 30 })).
Lazy Initialization
If the initial state requires an expensive calculation (like parsing JSON from localStorage), pass a function to useState instead of calling the function directly. useState(() => JSON.parse(data)) runs the function only once on mount, while useState(JSON.parse(data)) would re-run it on every render.
import { useState } from 'react';
function Counter() {
// Basic usage
const [count, setCount] = useState(0);
// Lazy initialization (expensive calc runs only once)
const [data, setData] = useState(() => {
return JSON.parse(localStorage.getItem('saved-data'));
});
const incrementTwice = () => {
// WRONG: Both read count=0, result is 1
// setCount(count + 1);
// setCount(count + 1);
// CORRECT: Each reads the latest value
setCount(prev => prev + 1);
setCount(prev => prev + 1); // Result is 2
};
return <button onClick={incrementTwice}>Count: {count}</button>;
}
Never mutate state directly! setItems(items.push(newItem)) mutates the array. Instead, create a new array: setItems([...items, newItem]).
2. useEffect — Synchronizing with External Systems
The useEffect hook lets you perform side effects: fetching data, setting up subscriptions, manually changing the DOM, and more. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components — all in one unified API. The key to mastering useEffect is understanding its dependency array.
No Dependency Array — Runs After EVERY Render
If you omit the second argument entirely, the effect runs after every single render (initial + every update). This is rarely what you want. If you set state inside this effect, it triggers another render, which triggers the effect again, creating an infinite loop.
// ⚠️ DANGEROUS: Runs after EVERY render
useEffect(() => {
console.log("I run after every single render!");
// setCount(count + 1); // This would cause an infinite loop!
});
Empty Array [] — Runs ONCE on Mount
Passing an empty array tells React: 'This effect has no dependencies, so it never needs to re-run.' It fires once after the initial render (equivalent to componentDidMount). Perfect for initial data fetching, setting up event listeners, or starting timers.
// ✅ Runs only once when the component first appears
useEffect(() => {
const data = fetchInitialData();
setUsers(data);
window.addEventListener('resize', handleResize);
// Cleanup runs when component unmounts (componentWillUnmount)
return () => window.removeEventListener('resize', handleResize);
}, []);
Variables in Array [x, y] — Runs When Dependencies Change
When you put variables in the array, the effect runs on mount AND re-runs whenever any of those variables change. React compares the current values with the previous ones using Object.is(). If any value is different, the effect re-runs. This is equivalent to componentDidUpdate with a condition.
// ✅ Re-fetches data whenever userId changes
useEffect(() => {
fetchUser(userId).then(data => setUser(data));
}, [userId]);
// ✅ Re-runs when EITHER searchQuery OR page changes
useEffect(() => {
fetchResults(searchQuery, page);
}, [searchQuery, page]);
Cleanup Functions — Preventing Memory Leaks
Return a function from your effect to clean up. React calls this cleanup before re-running the effect AND when the component unmounts. This is critical for clearing intervals, unsubscribing from WebSockets, or removing event listeners.
Common mistake: Forgetting to add variables used inside the effect to the dependency array. The React ESLint plugin (eslint-plugin-react-hooks) will warn you about missing dependencies.
3. useContext — Bypassing Prop Drilling
The useContext hook lets you read and subscribe to context from your component. Context provides a way to pass data through the component tree without having to pass props down manually at every level — solving the notorious 'prop drilling' problem.
How It Works
You create a context with createContext(), wrap a parent component with a Provider that holds the value, and then any child (no matter how deeply nested) can read that value with useContext(). No props needed in between.
Re-render Warning
When the Provider's value changes, ALL components calling useContext() for that context will re-render — even if they only use a small part of the context object. React.memo does NOT prevent this. To optimize, split your context into smaller, focused contexts (e.g., ThemeContext and AuthContext separately).
Common Use Cases
Themes (dark/light mode), authenticated user sessions, locale/language preferences, and global UI state like sidebar open/closed.
import { createContext, useContext } from 'react';
// 1. Create context with a default value
const ThemeContext = createContext('light');
// 2. Provider wraps the app (usually in App.js)
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Dashboard />
</ThemeContext.Provider>
);
}
// 3. Any nested child reads the value directly
function ThemedButton() {
const theme = useContext(ThemeContext); // 'dark'
return <button className={`btn-${theme}`}>Click me</button>;
}
4. useRef — Persisting Values Without Re-renders
The useRef hook returns a mutable object with a .current property that persists across renders. Unlike useState, updating .current does NOT trigger a re-render. It has two primary use cases: accessing DOM elements directly, and storing mutable values that need to survive renders.
Accessing DOM Elements
Attach a ref to a JSX element via the ref attribute. After the component mounts, ref.current points to the actual DOM node, letting you call imperative methods like .focus(), .scrollIntoView(), or .getBoundingClientRect().
const inputRef = useRef(null);
// After mount, inputRef.current is the <input> DOM node
const handleClick = () => inputRef.current.focus();
return <input ref={inputRef} />;
Storing Mutable Values (No Re-render)
Need to track an interval ID, a previous state value, or a WebSocket connection across renders without triggering a re-render when it changes? useRef is your tool. Think of it as a class instance variable (this.intervalId) for function components.
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => tick(), 1000);
return () => clearInterval(intervalRef.current);
}, []);
Tracking Previous Values
A common pattern: use useRef combined with useEffect to remember the previous value of a prop or state variable, since the ref update doesn't cause a re-render.
const prevCount = useRef(count);
useEffect(() => {
prevCount.current = count; // Updates after render
}, [count]);
// prevCount.current holds the PREVIOUS value of count
5. useMemo — Caching Expensive Calculations
The useMemo hook caches (memoizes) the result of a calculation between re-renders. It only recomputes when one of its dependencies changes. This prevents expensive operations like filtering large arrays or complex math from running on every single render.
When to Use It
Use useMemo when you have a computationally expensive operation (e.g., sorting 10,000 items, complex regex, or data transformations) AND the component re-renders frequently for other reasons (like typing in an input field).
When NOT to Use It
Don't wrap trivial calculations in useMemo. The memoization itself has a small cost (storing the cached value and comparing dependencies). If the calculation is fast (adding two numbers, simple string concatenation), useMemo actually makes it slightly slower.
Referential Equality
useMemo also prevents creating a new object/array reference on every render. This matters when passing props to child components wrapped in React.memo — without useMemo, the child would re-render every time because the prop is a 'new' object.
import { useMemo } from 'react';
function ProductList({ products, filter }) {
// Only re-filters when 'products' or 'filter' change
// NOT when other state (like a modal being open) changes
const filtered = useMemo(() => {
console.log('Filtering...'); // You'll see this only when deps change
return products
.filter(p => p.category === filter)
.sort((a, b) => a.price - b.price);
}, [products, filter]);
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}
6. useCallback — Caching Function Definitions
The useCallback hook caches a function definition between re-renders. In JavaScript, () => {} !== () => {} — every render creates a brand new function object. This breaks React.memo optimizations on child components. useCallback solves this by returning the same function reference as long as its dependencies haven't changed.
useMemo vs useCallback
useMemo caches a VALUE (the result of calling a function). useCallback caches the FUNCTION ITSELF. In fact, useCallback(fn, deps) is identical to useMemo(() => fn, deps). They serve different purposes but use the same memoization mechanism.
When It Matters
useCallback is most useful when passing callbacks to optimized child components that use React.memo or shouldComponentUpdate. Without it, the child re-renders on every parent render because the function prop is technically a new reference.
import { useCallback, memo } from 'react';
// Child component only re-renders if props actually change
const ExpensiveChild = memo(({ onClick }) => {
console.log('Child rendered!');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Without useCallback: new function every render → child re-renders
// With useCallback: same function reference → child skips re-render
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // No dependencies, function never changes
return (
<>
<input value={name} onChange={e => setName(e.target.value)} />
<ExpensiveChild onClick={handleClick} />
</>
);
}
7. useReducer — Complex State Logic
The useReducer hook is an alternative to useState for managing complex state that involves multiple sub-values or when the next state depends on the previous one. It works exactly like Redux: you dispatch actions to a pure reducer function that calculates the new state.
When to Choose useReducer over useState
Use useReducer when: (1) Your state is an object with many related fields. (2) Multiple actions can update the same state. (3) The next state depends on the previous state. (4) You want centralized, testable state logic. If you just need a single boolean or number, useState is simpler.
The Reducer Pattern
A reducer takes the current state and an action, and returns a brand new state. It must be a pure function — no side effects, no API calls, no mutations. The action object typically has a 'type' field and optionally a 'payload' for data.
import { useReducer } from 'react';
// Pure reducer function (no side effects!)
function todoReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, { id: Date.now(), text: action.payload, done: false }];
case 'TOGGLE':
return state.map(todo =>
todo.id === action.payload ? { ...todo, done: !todo.done } : todo
);
case 'DELETE':
return state.filter(todo => todo.id !== action.payload);
default:
throw new Error('Unknown action: ' + action.type);
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<>
<button onClick={() => dispatch({ type: 'ADD', payload: 'New task' })}>
Add Todo
</button>
{todos.map(todo => (
<div key={todo.id} onClick={() => dispatch({ type: 'TOGGLE', payload: todo.id })}>
{todo.done ? '✅' : '⬜'} {todo.text}
</div>
))}
</>
);
}
8. useLayoutEffect — Measuring DOM Before Paint
The useLayoutEffect hook is identical to useEffect in its API, but it fires synchronously AFTER the DOM has been updated but BEFORE the browser has painted the screen. This makes it perfect for reading layout measurements and making synchronous visual adjustments.
useEffect vs useLayoutEffect
useEffect runs AFTER the browser paints — so the user might briefly see a 'flash' of the wrong layout. useLayoutEffect runs BEFORE the paint, blocking the browser until your code finishes. This prevents visual flickering but can hurt performance if your code is slow.
When to Use It
Use useLayoutEffect when you need to: measure an element's dimensions (width, height, position), adjust a tooltip or popover position based on available space, or prevent a visible 'jump' when repositioning elements.
Performance Warning
Because useLayoutEffect blocks the browser from painting, expensive computations inside it will make your app feel sluggish. Keep the code inside as minimal as possible. For most effects, stick with useEffect.
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip({ children, targetRef }) {
const tooltipRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
// Runs BEFORE browser paints → no flickering!
useLayoutEffect(() => {
const rect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
setPosition({
top: rect.top - tooltipRect.height - 8,
left: rect.left + (rect.width - tooltipRect.width) / 2,
});
}, [targetRef]);
return (
<div ref={tooltipRef} style={{ position: 'fixed', ...position }}>
{children}
</div>
);
}
9. useImperativeHandle — Customizing Exposed Refs
The useImperativeHandle hook lets you customize the value that a parent component receives when it attaches a ref to your component. Instead of exposing the raw DOM node, you can expose a custom object with only the methods you want the parent to call.
Used with forwardRef
This hook only works inside a component wrapped with forwardRef(). forwardRef lets your component receive a ref from its parent, and useImperativeHandle lets you control what that ref contains.
Encapsulation
Instead of giving the parent full access to your component's DOM (which they could misuse), you expose a clean API. For example, expose only focus() and scrollToTop(), hiding the internal DOM structure entirely.
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// Parent's ref.current will have these methods, NOT the raw DOM node
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; },
getValue: () => inputRef.current.value,
}));
return <input ref={inputRef} placeholder="Type here..." />;
});
// Parent component
function Form() {
const fancyRef = useRef(null);
return (
<>
<FancyInput ref={fancyRef} />
<button onClick={() => fancyRef.current.focus()}>Focus</button>
<button onClick={() => fancyRef.current.clear()}>Clear</button>
</>
);
}
10. useDebugValue — Labeling Custom Hooks in DevTools
The useDebugValue hook lets you add a custom label to your custom hooks in React DevTools. It is purely a developer experience tool — it has zero effect on behavior or rendering. It only works inside custom hooks.
When to Use It
Use useDebugValue in custom hooks that are shared across your team or published as a library. It makes the hook's internal state visible at a glance in React DevTools without having to expand and inspect the hook's internals.
Deferred Formatting
Pass a formatter function as the second argument: useDebugValue(date, d => d.toISOString()). The formatting function only runs when DevTools is actually open, avoiding unnecessary computation in production.
import { useDebugValue, useState, useEffect } from 'react';
// Custom hook
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
const handle = () => setIsOnline(navigator.onLine);
window.addEventListener('online', handle);
window.addEventListener('offline', handle);
return () => {
window.removeEventListener('online', handle);
window.removeEventListener('offline', handle);
};
}, []);
// Shows "OnlineStatus: Online ✅" in React DevTools
useDebugValue(isOnline ? 'Online ✅' : 'Offline ❌');
return isOnline;
}
React 18: The Concurrent Rendering Era
React 18 introduced a brand-new concurrent rendering engine that can pause, resume, and even abandon renders. The following 5 hooks give you direct control over this powerful system.
Concurrent rendering means React can work on multiple UI updates simultaneously, prioritizing urgent ones (like typing) over expensive ones (like filtering a list of 10,000 items).
11. useTransition — Non-Blocking State Updates
The useTransition hook lets you mark a state update as a 'transition' — a non-urgent update that React can interrupt if something more urgent comes in (like the user typing). It returns an isPending flag and a startTransition function.
How It Works
Wrap the expensive state update in startTransition(). React will render it in the background without freezing the UI. If the user performs another action mid-render, React will abandon the stale transition and start a fresh one.
The isPending Flag
isPending is true while the transition is rendering in the background. Use it to show a loading spinner or reduce the opacity of stale content, giving the user visual feedback that new data is being prepared.
Real-World Example
A search page: updating the input field is urgent (must feel instant), but filtering and rendering 10,000 results is expensive. Wrap the results update in startTransition so typing stays smooth.
import { useState, useTransition } from 'react';
function SearchPage({ allItems }) {
const [query, setQuery] = useState('');
const [filtered, setFiltered] = useState(allItems);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
setQuery(e.target.value); // URGENT: update input instantly
startTransition(() => {
setFiltered( // NON-URGENT: filter in background
allItems.filter(item => item.includes(e.target.value))
);
});
};
return (
<>
<input value={query} onChange={handleChange} />
<div style={{ opacity: isPending ? 0.6 : 1 }}>
{filtered.map(item => <div key={item}>{item}</div>)}
</div>
</>
);
}
12. useDeferredValue — Deferring Non-Critical Updates
The useDeferredValue hook accepts a value and returns a 'deferred' copy of it that lags behind the original. React will update the deferred value in the background with lower priority, keeping the UI responsive for the original value.
useTransition vs useDeferredValue
Use useTransition when you own the state setter (you can wrap it in startTransition). Use useDeferredValue when you receive a value as a prop from a parent component and can't control how it's set.
How It Behaves
On the initial render, the deferred value equals the original. When the original changes, React first renders with the OLD deferred value (fast), then starts a background render with the NEW value. If another update arrives mid-render, React restarts with the latest value.
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ query }) {
// query updates instantly, but deferredQuery lags behind
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// Expensive filtering uses the DEFERRED (lagging) value
const results = useMemo(() => {
return hugeList.filter(item => item.includes(deferredQuery));
}, [deferredQuery]);
return (
<div style={{ opacity: isStale ? 0.7 : 1 }}>
{results.map(r => <div key={r}>{r}</div>)}
</div>
);
}
13. useId — Generating Unique, Stable IDs
The useId hook generates a unique ID string that is stable between server-side rendering (SSR) and client-side hydration. This solves the common problem of generating IDs for accessibility attributes like htmlFor, aria-describedby, and aria-labelledby.
Why Not Math.random() or a Counter?
Math.random() generates different IDs on the server vs client, causing hydration mismatches. A global counter (let id = 0) also fails because the server and client increment differently. useId guarantees the same ID on both sides.
Multiple IDs from One Call
Call useId() once and use the returned value as a prefix. Append suffixes for related elements: id + '-email', id + '-password'. This keeps IDs unique and avoids calling the hook multiple times.
import { useId } from 'react';
function LoginForm() {
const id = useId(); // e.g., ":r1:"
return (
<form>
<div>
<label htmlFor={id + '-email'}>Email</label>
<input id={id + '-email'} type="email" />
</div>
<div>
<label htmlFor={id + '-password'}>Password</label>
<input id={id + '-password'} type="password"
aria-describedby={id + '-hint'} />
<p id={id + '-hint'}>Must be at least 8 characters</p>
</div>
</form>
);
}
14. useSyncExternalStore — Subscribing to External Stores
The useSyncExternalStore hook lets you safely subscribe to an external data store (like Redux, Zustand, or browser APIs like navigator.onLine) in a way that is compatible with concurrent rendering. It prevents 'tearing' — a bug where different parts of the UI show different versions of the same data.
What is Tearing?
In concurrent mode, React can pause a render midway. If an external store updates during the pause, half the UI shows old data and half shows new data. useSyncExternalStore forces React to read from the store synchronously, preventing this inconsistency.
Three Arguments
subscribe: A function that subscribes to the store and returns an unsubscribe function. getSnapshot: A function that returns the current value from the store. getServerSnapshot (optional): Returns the value used during SSR.
import { useSyncExternalStore } from 'react';
// Subscribe to browser online/offline status
function useOnlineStatus() {
return useSyncExternalStore(
// 1. subscribe: called with a callback to notify React of changes
(callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
// 2. getSnapshot: returns current value
() => navigator.onLine,
// 3. getServerSnapshot: for SSR (server has no navigator)
() => true
);
}
function StatusBar() {
const isOnline = useOnlineStatus();
return <p>{isOnline ? '🟢 Online' : '🔴 Offline'}</p>;
}
15. useInsertionEffect — Injecting Styles Before Layout
The useInsertionEffect hook fires synchronously BEFORE any DOM mutations. It was designed exclusively for CSS-in-JS library authors (like styled-components or Emotion) to inject dynamic style tags into the document before useLayoutEffect reads layout measurements.
Execution Order
The three effect hooks fire in this exact order: (1) useInsertionEffect — before DOM mutations. (2) useLayoutEffect — after DOM mutations, before paint. (3) useEffect — after paint. This ordering ensures styles are injected before any layout measurement code runs.
You Probably Don't Need This
This hook is intended for CSS-in-JS library internals. If you are not building a CSS-in-JS library, use useEffect or useLayoutEffect instead. You cannot update state or schedule refs inside useInsertionEffect.
import { useInsertionEffect } from 'react';
// ⚠️ Only for CSS-in-JS library authors!
function useCSS(rule) {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, [rule]);
}
// Usage inside a CSS-in-JS library
function StyledButton({ children }) {
useCSS('.fancy-btn { background: linear-gradient(135deg, #667eea, #764ba2); }');
return <button className="fancy-btn">{children}</button>;
}
React 19: The Next Generation Hooks
React 19 officially eliminated form handling boilerplate. With Server Components and Actions baked into the core, these 4 new hooks make data mutations, optimistic updates, and async data reading easier than ever before.
React 19 is a paradigm shift. The new use() hook even breaks the 'Rules of Hooks' by being callable inside conditionals! These hooks represent the future of React development.
16. use — Reading Promises and Context Anywhere
The use hook is revolutionary. It is the ONLY React hook that can be called inside if statements, loops, and after early returns. It reads the resolved value of a Promise (integrating with Suspense) or reads a Context value. It replaces many patterns that previously required useEffect + useState for async data fetching.
Reading Promises (with Suspense)
Pass a Promise to use() and it will suspend the component until the Promise resolves. The nearest Suspense boundary shows a fallback. No more useState + useEffect + isLoading boilerplate! The Promise must be created outside the component (e.g., in a parent or a cache).
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends until resolved!
return <h1>{user.name}</h1>;
}
// Parent wraps it in Suspense
<Suspense fallback={<Spinner />}>
<UserProfile userPromise={fetchUser(id)} />
</Suspense>
Conditional Usage (Breaks the Rules!)
Unlike every other hook, use() CAN be called inside if/else blocks. This lets you conditionally read a Promise or Context based on runtime logic.
function Component({ shouldLoad, dataPromise }) {
if (shouldLoad) {
const data = use(dataPromise); // Totally valid!
return <Display data={data} />;
}
return <Placeholder />;
}
17. useActionState — Managing Form Actions
The useActionState hook manages the entire lifecycle of a form action: the pending state, the returned result, and error handling. It replaces the common pattern of manually managing isSubmitting, error, and result state variables. Pass it an async action function and an initial state.
What It Returns
It returns [state, formAction, isPending]. 'state' is the current result (or error) returned by your action. 'formAction' is a function you pass to
Progressive Enhancement
When used with Server Actions, forms using useActionState work even before JavaScript loads. The form submits normally, the server processes it, and React hydrates the result — giving you progressive enhancement for free.
import { useActionState } from 'react';
async function submitComment(previousState, formData) {
const comment = formData.get('comment');
const result = await postComment(comment);
if (result.error) return { error: result.error };
return { success: true, message: 'Comment posted!' };
}
function CommentForm() {
const [state, formAction, isPending] = useActionState(submitComment, null);
return (
<form action={formAction}>
<textarea name="comment" required />
<button disabled={isPending}>
{isPending ? 'Posting...' : 'Post Comment'}
</button>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">{state.message}</p>}
</form>
);
}
18. useFormStatus — Reading Parent Form State
The useFormStatus hook lets any component inside a <form> read the submission status of that form — without prop drilling. It returns an object with pending, data, method, and action properties. It must be called from a component that is rendered inside a <form>.
No More Prop Drilling for isPending
Before useFormStatus, you had to pass isPending as a prop from the form to every button, input, and spinner. Now, any deeply nested component can simply call useFormStatus() to know if the form is currently submitting.
Important Limitation
useFormStatus reads the status of the PARENT form. It does not work if called in the same component that renders the
import { useFormStatus } from 'react-dom';
// This component MUST be rendered inside a <form>
function SubmitButton() {
const { pending, data } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
// Usage
function MyForm() {
return (
<form action={serverAction}>
<input name="email" type="email" />
<SubmitButton /> {/* Reads form status automatically! */}
</form>
);
}
19. useOptimistic — Instant UI Updates
The useOptimistic hook lets you show a different state while an async action (like a network request) is in progress. It immediately updates the UI with the expected result, then automatically reverts if the action fails. This creates a lightning-fast user experience.
How It Works
You give it the current state and an update function. Call the setter to instantly show the 'optimistic' value. When the underlying async action resolves, React replaces the optimistic value with the real server response. If it fails, the optimistic value is automatically reverted.
Real-World: Like Button
When a user taps 'Like', the heart turns red INSTANTLY (optimistic). The API call happens in the background. If it fails, the heart reverts to grey. The user never sees a loading spinner — the UI feels instant.
const [optimisticLiked, setOptimisticLiked] = useOptimistic(
isLiked,
(currentState, optimisticValue) => optimisticValue
);
async function handleLike() {
setOptimisticLiked(true); // Instant! No waiting
await fetch('/api/like', { method: 'POST' }); // Background
}
import { useOptimistic } from 'react';
function MessageThread({ messages, sendMessage }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMessage) => [
...currentMessages,
{ text: newMessage, sending: true } // Show with 'sending' indicator
]
);
async function handleSubmit(formData) {
const text = formData.get('message');
addOptimisticMessage(text); // Instantly appears in the list!
await sendMessage(text); // Server processes in background
}
return (
<>
{optimisticMessages.map((msg, i) => (
<div key={i} style={{ opacity: msg.sending ? 0.6 : 1 }}>
{msg.text} {msg.sending && '⏳'}
</div>
))}
<form action={handleSubmit}>
<input name="message" />
<button>Send</button>
</form>
</>
);
}