React 19 Hooks
Advanced Todo App
Comprehensive React 19 Hooks Demo
This example demonstrates how multiple React 19 hooks work together in a real-world application. It showcases a complete todo app with optimistic updates, server actions, form handling, and smooth UI transitions.
React 19 Hooks Used:
- useTransition: Makes filter changes non-urgent, preventing UI blocking. Filter buttons respond immediately while the list update is non-blocking.
- useOptimistic: Shows new todos instantly with a "(saving...)" label. The UI updates immediately before server confirmation for better perceived performance.
- useActionState: Manages async form submission, errors, and success states in one unified place, replacing the need for separate useState calls.
- useFormStatus: The submit button automatically knows when the form is submitting without needing to pass pending state as props. This enables better component composition.
Code Example:
import { useTransition, useOptimistic, useActionState } from 'react';
function TodoApp() {
// useTransition: Non-urgent filter updates
const [isPending, startTransition] = useTransition();
// useOptimistic: Instant UI updates
const [todos, addOptimistic] = useOptimistic(
state.todos,
(current, newTodo) => [...current, newTodo]
);
// useActionState: Form state + server action
const [state, formAction] = useActionState(addTodo, initialState);
// useFormStatus: Auto-detect form submission (in child)
function handleFilterChange(filter) {
startTransition(() => setFilter(filter));
}
return <form action={formAction}>...</form>;
}React useTransition Hook
What is useTransition?
useTransition lets you mark some state updates as non-urgent. Other state updates in your component will interrupt the transition, allowing urgent updates (like user input) to be handled immediately.
Key Features:
- Marks state updates as non-urgent (low priority)
- Keeps the UI responsive during expensive updates
- Allows urgent updates (like typing) to interrupt transitions
- Provides
isPendingto show loading states
When to Use:
- Tab switching or filter changes that trigger expensive re-renders
- Large list filtering or sorting operations
- Any state update that can be delayed without hurting UX
- When you want to prioritize user input over background updates
Code Example:
import { useTransition, useState } from 'react';
function TabComponent() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('home');
function handleTabChange(newTab) {
// Non-urgent update - doesn't block UI
startTransition(() => {
setTab(newTab);
});
}
return (
<div>
{isPending && <Spinner />}
<button onClick={() => handleTabChange('home')}>
Home
</button>
</div>
);
}Tab Navigation:
Current Tab: home
Generate Items:
Items generated: 0
Try clicking multiple times quickly - the UI stays responsive!
React 19 useOptimistic Hook
What is useOptimistic?
useOptimistic enables optimistic UI updates by showing immediate feedback before server confirmation. If the action fails, the UI automatically reverts to the previous state, ensuring consistency.
Key Features:
- Updates UI instantly before server response
- Automatically reverts if the action fails
- Provides better user experience with immediate feedback
- Perfect for actions like likes, comments, or updates
When to Use:
- Actions that are likely to succeed (likes, follows, saves)
- When you want instant UI feedback
- Improving perceived performance
- Reducing perceived latency in user interactions
Code Example:
import { useOptimistic, useState } from 'react';
function LikeButton() {
const [likes, setLikes] = useState(0);
const [optimisticLikes, addOptimistic] = useOptimistic(
likes,
(current, amount) => current + amount
);
async function handleLike() {
addOptimistic(1); // Update UI immediately
await likePost(); // Server call
setLikes(prev => prev + 1); // Update real state
}
return <button onClick={handleLike}>Likes: {optimisticLikes}</button>;
}UI updates instantly, then syncs with server
React 19 useActionState Hook
What is useActionState?
useActionState (formerly useFormState) manages form state and server actions in one place. It handles async submission, error states, and pending states automatically.
Key Features:
- Unified state management for form submissions and server actions
- Automatic error and success state handling
- Built-in pending state tracking
- Works seamlessly with Next.js Server Actions
When to Use:
- Forms with server-side validation and processing
- When you need centralized error and state management
- Combining with useFormStatus for form submission feedback
- Replaces the older useFormState pattern
Code Example:
import { useActionState } from 'react';
async function submitForm(prevState, formData) {
const email = formData.get('email');
// Validation and server logic
return { message: 'Success!', error: null };
}
function Form() {
const [state, formAction, pending] = useActionState(
submitForm,
{ message: null, error: null }
);
return (
<form action={formAction}>
<input name="email" disabled={pending} />
{state.error && <p>{state.error}</p>}
<button disabled={pending}>Submit</button>
</form>
);
}Manages form state, validation, and server actions automatically
React 19 useFormStatus Hook
What is useFormStatus?
useFormStatus lets child components access the submission status of their nearest parent <form> element. This eliminates the need to pass pending state as props.
Key Features:
- Automatically detects form submission status
- Works only with Server Actions or form actions
- No prop drilling needed for pending state
- Must be used inside a form element
When to Use:
- Submit buttons that need to show loading states
- Form validation messages that depend on submission status
- Avoiding prop drilling for form state
- Better component composition with form elements
Code Example:
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={serverAction}>
<input name="email" />
<SubmitButton /> {/* Knows form status automatically */}
</form>
);
}The button above uses useFormStatus to automatically detect when the form is submitting, without needing to pass pending as a prop.
React 19 use Hook
What is use?
use is a React hook that lets you read the value of a resource like a promise or context. It can unwrap promises directly in render, automatically suspending until the promise resolves.
Key Features:
- Unwraps promises and suspends until they resolve
- Works with React's Suspense boundaries
- Can also read context values
- Simplifies async data handling in components
When to Use:
- Reading promises from data fetching libraries
- Accessing context values conditionally
- Simpler async data handling compared to useEffect
- Working with Suspense for better loading states
Code Example:
import { use, Suspense, useMemo } from 'react';
function UserProfile({ userPromise }) {
// Unwraps promise, suspends until resolved
const user = use(userPromise);
return <div>{user.name}</div>;
}
function App() {
// Memoize promise to prevent recreation
const promise = useMemo(() => fetchUser(), []);
return (
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={promise} />
</Suspense>
);
}Handles promises directly in render, suspends automatically
Core Hooks
React useState Hook
What is useState?
useState is the most fundamental React hook for managing component state in functional components. It allows you to add stateful logic to components that were previously stateless.
Key Features:
- Returns the current state value and a function to update it
- Triggers re-renders when state changes
- Can accept an initial value or a function that returns an initial value
- State updates are batched for performance
When to Use:
- Managing simple component state (counters, form inputs, toggles)
- When state logic is straightforward and doesn't require complex updates
- For state that's specific to a single component
Code Example:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}Counter: 0
useState is the most basic hook for managing component state. It returns the current state value and a function to update it.
React useEffect Hook
What is useEffect?
useEffect lets you perform side effects in functional components. It serves as a replacement for lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
Key Features:
- Runs after the component renders to the screen
- Can optionally clean up effects (return a cleanup function)
- Controls when effects run using dependency arrays
- Non-blocking - doesn't delay the browser from updating the screen
When to Use:
- Data fetching from APIs or subscriptions
- Setting up event listeners or timers
- Manually updating the DOM
- Any operation that should happen "outside" of the render cycle
Code Example:
import { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch user data on mount
fetchUser().then(setUser);
// Cleanup function (runs on unmount)
return () => {
// Cancel any pending requests
};
}, []); // Empty deps = run once on mount
return <div>{user?.name}</div>;
}Counter: 0
Check the console to see effects running
Window Width: 0px
Resize the window to see the effect update
Component Status: Not Mounted
Common Use Cases:
- Data fetching from APIs
- Setting up subscriptions or event listeners
- Manually changing the DOM
- Cleanup when component unmounts
React useContext Hook
What is useContext?
useContext allows you to read and subscribe to context values without prop drilling. It lets you access values from a React Context anywhere in your component tree without passing props through intermediate components.
Key Features:
- Eliminates the need to pass props through multiple component levels
- Subscribes to context changes and re-renders when values update
- Works with the Context API (createContext + Provider)
- Can consume multiple contexts in a single component
When to Use:
- Sharing data across many components in the tree
- Theming, authentication, language preferences
- When prop drilling becomes cumbersome
- For data that doesn't need to be stored globally (use Redux/Zustand for that)
Code Example:
import { createContext, useContext } from 'react';
const ThemeContext = createContext('light');
function Button() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
function App() {
return (
<ThemeContext.Provider value="dark">
<Button /> {/* Uses 'dark' theme */}
</ThemeContext.Provider>
);
}Both components below share the same theme state through context:
Current Theme: light
Benefits of useContext:
- Avoids prop drilling (passing props through multiple levels)
- Shared state across component tree
- Cleaner component APIs
- Great for themes, authentication, language preferences
React useReducer Hook
What is useReducer?
useReducer is an alternative to useState for managing complex state logic. It follows the reducer pattern similar to Redux, where you dispatch actions to update state through a reducer function.
Key Features:
- Centralized state updates through a reducer function
- Predictable state transitions via action dispatches
- Better for complex state with multiple sub-values
- Easier to test and reason about state changes
When to Use:
- Complex state logic with multiple related values
- When next state depends on previous state
- Managing state objects with nested properties
- When you need more predictable state updates
Code Example:
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<button onClick={() => dispatch({ type: 'increment' })}>
Count: {state.count}
</button>
);
}Count: 0
History:
When to use useReducer:
- Complex state logic with multiple sub-values
- When next state depends on previous state
- Better testability and predictability
- Easier to reason about state transitions
Performance Hooks
React useCallback Hook
What is useCallback?
useCallback returns a memoized version of a callback function that only changes if one of the dependencies has changed. It's useful for passing stable callbacks to child components, preventing unnecessary re-renders.
Key Features:
- Returns the same function reference if dependencies haven't changed
- Prevents child components from re-rendering unnecessarily
- Essential when passing callbacks to memoized components (React.memo)
- Similar to useMemo but specifically for functions
When to Use:
- Passing callbacks to memoized child components
- When function identity matters (useEffect dependencies, etc.)
- Optimizing performance in lists with many items
- Preventing infinite loops in useEffect dependencies
Code Example:
import { useCallback, memo } from 'react';
const TodoItem = memo(({ id, onRemove }) => {
return <button onClick={() => onRemove(id)}>Remove</button>;
});
function TodoList() {
const [todos, setTodos] = useState([]);
// Memoized callback - same reference unless deps change
const handleRemove = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, []);
return todos.map(todo => (
<TodoItem key={todo.id} onRemove={handleRemove} />
));
}Counter (unrelated state): 0
💡 Increment the counter and check the console. TodoItem components won't re-render because handleRemove is memoized with useCallback.
Key Benefits:
- Prevents unnecessary re-renders of child components
- Stable function reference across renders
- Essential when passing callbacks to memoized components
React useMemo Hook
What is useMemo?
useMemo memoizes the result of an expensive computation, returning a cached value until one of its dependencies changes. It helps optimize performance by avoiding expensive calculations on every render.
Key Features:
- Only recalculates when dependencies change
- Returns cached value if dependencies haven't changed
- Prevents unnecessary expensive computations
- Can memoize objects and arrays to prevent reference changes
When to Use:
- Expensive calculations that depend on specific values
- Preventing unnecessary recalculations on every render
- Optimizing performance when passing values as props
- Memoizing derived state from complex computations
Code Example:
import { useMemo, useState } from 'react';
function ExpensiveComponent({ items, filter }) {
// Only recalculates when items or filter changes
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.includes(filter)
);
}, [items, filter]);
return <div>{filteredItems.map(...)}</div>;
}Counter (triggers re-render): 0
⚠️ Click this button and watch the console - "Without useMemo" recalculates every time, but "With useMemo" stays cached!
Multiplier: 1
Changing multiplier triggers recalculation for both (since multiplier is a dependency)
Results:
Without useMemo:
249,750,000
Executed 2 times - Check console to see it runs on every render!
With useMemo:
249,750,000
Executed 1 time - Only recalculates when multiplier changes!
💡 Test it: Increment the counter multiple times. Check your browser console - you'll see "Without useMemo" logs every time, but "With useMemo" only logs when multiplier changes!
When to use useMemo:
- Expensive calculations that depend on specific values
- Preventing unnecessary recalculations on every render
- Optimizing performance for computed values
- When the computation cost is significant
React useDeferredValue Hook
What is useDeferredValue?
useDeferredValue defers updating a value until after more urgent updates have completed. It's similar to debouncing but works with React's concurrent rendering, keeping the UI responsive during expensive operations.
Key Features:
- Defers value updates until urgent updates complete
- Keeps the UI responsive during expensive operations
- Works automatically with React's concurrent rendering
- Returns the previous value while update is pending
- Perfect for search inputs and filtering large lists
When to Use:
- Search/filter inputs with expensive filtering operations
- Rendering large lists based on user input
- Any expensive computation triggered by user input
- When you want to prioritize input responsiveness over immediate results
Code Example:
import { useState, useDeferredValue, useMemo } from 'react';
function SearchProducts() {
const [query, setQuery] = useState('');
// Defer the query update until urgent updates complete
const deferredQuery = useDeferredValue(query);
// Expensive filtering uses deferred query
const filteredProducts = useMemo(() => {
return products.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
);
}, [deferredQuery]);
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
{isStale && <div>Searching...</div>}
<ProductList products={filteredProducts} />
</div>
);
}Interactive Search Demo:
Type in the search box to see how useDeferredValue keeps the input responsive while deferring expensive filtering operations. Notice how typing feels instant even though we're filtering through 5,000 products.
How It Works:
Without useDeferredValue:
- Every keystroke triggers expensive filtering immediately
- Input may feel laggy or unresponsive
- UI blocks while filtering completes
With useDeferredValue:
- Input updates immediately (urgent update)
- Filtering is deferred until input settles
- UI stays responsive during typing
- Previous results shown while new ones are computed
Performance Tips:
- Combine with
useMemoto memoize expensive computations based on the deferred value - Use
isPendingpattern to show loading indicators when value is stale - Works best with concurrent rendering (React 18+) where urgent updates can interrupt non-urgent ones
- Consider
useTransitionfor marking entire state updates as non-urgent
React useLayoutEffect Hook
What is useLayoutEffect?
useLayoutEffect is identical to useEffect, but it runs synchronously after all DOM mutations and before the browser paints. This makes it useful for reading layout from the DOM and synchronously re-rendering.
Key Features:
- Runs synchronously before the browser paints the screen
- Blocks visual updates until it completes
- Prevents visual flickering when DOM measurements are needed
- Same API as useEffect (function, dependency array, cleanup)
When to Use:
- DOM measurements that affect visual layout
- Preventing visual flickering or "flash of unstyled content"
- When you need to read layout and synchronously update before paint
- Prefer useEffect for most cases (better performance)
Real-Life Use Cases:
- Tooltip/Popover Positioning: Measuring an element's position and dynamically positioning a tooltip or popover to ensure it stays within the viewport without causing a visual flash.
- Dynamic Height Calculations: Calculating and setting heights for animated accordions, collapsible sections, or expanding menus before the browser paints to prevent layout shift.
- Scroll Position Restoration: Restoring scroll position after navigation or content changes, ensuring users don't see the content jump from one position to another.
- Focus Management: Automatically focusing inputs or buttons after modal dialogs open, preventing the visible focus state from appearing before the element is ready.
- Responsive Layout Adjustments: Measuring container widths to conditionally render different layouts (e.g., showing/hiding sidebar) before the first paint.
Code Example:
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip() {
const [width, setWidth] = useState(0);
const ref = useRef(null);
// Runs before paint - prevents flickering
useLayoutEffect(() => {
if (ref.current) {
setWidth(ref.current.offsetWidth);
}
}, []);
return <div ref={ref} style={{ width }}>Tooltip</div>;
}Interactive Demonstrations:
Hover over the button to see the tooltip position itself without flickering.
useLayoutEffect vs useEffect:
- useLayoutEffect: Runs synchronously before browser paint
- useEffect: Runs asynchronously after browser paint
- Use useLayoutEffect when you need to prevent visual flickering
- Use useLayoutEffect for DOM measurements that affect layout
- Prefer useEffect for most cases (better performance)
Utility Hooks
React useRef Hook
What is useRef?
useRef returns a mutable ref object whose current property is initialized to the passed argument. The ref object persists across renders but changing it doesn't trigger a re-render.
Key Features:
- Maintains the same ref object across re-renders
- Changing
.currentdoesn't cause re-renders - Can hold any mutable value (not just DOM elements)
- Commonly used to access DOM elements directly
When to Use:
- Accessing DOM elements directly (focus, scroll, measurements)
- Storing mutable values that shouldn't trigger re-renders
- Keeping track of previous values
- Storing timers, intervals, or other imperative handles
Code Example:
import { useRef } from 'react';
function TextInput() {
const inputRef = useRef(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} />
<button onClick={handleFocus}>Focus Input</button>
</>
);
}Counter: 0
Previous count: N/A
Previous Value (stored in ref):
Previous count: N/A
Note: The ref value persists across renders but doesn't cause re-renders when updated directly.
Common useRef Use Cases:
- Accessing DOM elements directly
- Storing mutable values that don't trigger re-renders
- Keeping previous values
- Storing timers, intervals, or other imperatives
React useId Hook
What is useId?
useId generates a unique ID that is stable across server and client renders. It's particularly useful for accessibility attributes like htmlFor and id that need to match.
Key Features:
- Generates unique, stable IDs across renders
- SSR-safe (same ID on server and client)
- Includes a colon (:) to ensure uniqueness within a component
- Doesn't change between re-renders of the same component
When to Use:
- Accessibility attributes (label/input, aria-labelledby, etc.)
- Form field IDs that need to match labels
- Server-side rendering where IDs must match between server and client
- Avoiding ID conflicts in components rendered multiple times
Code Example:
import { useId } from 'react';
function FormField({ label }) {
const id = useId(); // Stable ID across renders
return (
<>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</>
);
}Counter: 0
IDs remain stable even when component re-renders
Generated IDs:
When to use useId:
- Generate unique IDs for accessibility (label/input pairing)
- Stable IDs across server and client renders (SSR)
- Avoiding ID conflicts in lists or multiple instances
- Creating unique keys that don't depend on data
Advanced Hooks
React useSyncExternalStore Hook
What is useSyncExternalStore?
useSyncExternalStore lets you subscribe to an external data source. It's designed to work with React's concurrent rendering features and is the recommended way to integrate external stores (like Redux, Zustand, or custom stores) with React.
Key Features:
- Subscribe to external data sources safely
- Works correctly with concurrent rendering and automatic batching
- Prevents tearing (inconsistent UI state) during concurrent updates
- Requires a subscribe function and a getSnapshot function
When to Use:
- Integrating external state management libraries (Redux, Zustand, etc.)
- Subscribing to browser APIs (localStorage, window size, network status)
- Connecting to real-time data sources (WebSockets, Server-Sent Events)
- Any external data source that needs to work with concurrent React
Code Example:
import { useSyncExternalStore } from 'react';
// Simple external store
class Store {
private value = 0;
private listeners = new Set();
subscribe = (listener) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
getSnapshot = () => this.value;
increment = () => {
this.value++;
this.listeners.forEach(l => l());
};
}
const store = new Store();
function Counter() {
const value = useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
return (
<div>
Count: {value}
<button onClick={store.increment}>+</button>
</div>
);
}External Store Demo:
This counter uses an external store that multiple components could subscribe to. The store value is synced using useSyncExternalStore.
0
From external store
Local state counter: 0 (updates separately to show difference)
localStorage Store Demo:
This demonstrates subscribing to localStorage, which syncs across browser tabs. Try opening this page in multiple tabs and updating the value.
Current value: React Hooks
This value persists across page refreshes and syncs across tabs!
useSyncExternalStore vs Other Approaches:
- useSyncExternalStore: Safe for concurrent rendering, prevents tearing
- useState + useEffect: Can cause tearing in concurrent mode, not recommended for external stores
- Direct subscription: Works but doesn't integrate well with React's concurrent features
- Library authors should use useSyncExternalStore for state management libraries
React useInsertionEffect Hook
What is useInsertionEffect?
useInsertionEffect is a hook primarily used by library authors to inject styles into the DOM before layout effects run. It runs synchronously before all DOM mutations and before useLayoutEffect. This prevents visual flickering when styles need to be applied before layout measurements.
Key Features:
- Runs synchronously before all DOM mutations
- Runs before useLayoutEffect (ensures styles exist before layout reads)
- Primarily for CSS-in-JS library authors
- Rarely needed in regular applications
- Prevents FOUC (Flash of Unstyled Content)
When to Use:
- Library authors: Building CSS-in-JS libraries (styled-components, emotion, etc.)
- Dynamic style injection: When styles must exist before layout effects
- Preventing flicker: When styles are computed dynamically
- Most apps: You probably don't need this - use CSS classes or CSS-in-JS libraries
Execution Order:
- Component renders
useInsertionEffectruns (inject styles)- DOM mutations complete
useLayoutEffectruns (can read computed styles)- Browser paints
useEffectruns (async, after paint)
Code Example:
import { useInsertionEffect, useRef } from 'react';
function StyledButton({ color }: { color: string }) {
const idRef = useRef(`style-${Math.random()}`);
const classNameRef = useRef(`btn-${idRef.current}`);
// Inject styles before layout effects run
useInsertionEffect(() => {
const css = `
.${classNameRef.current} {
background-color: ${color};
color: white;
padding: 8px 16px;
}
`;
// Inject into <head>
const style = document.createElement('style');
style.id = idRef.current;
style.textContent = css;
document.head.appendChild(style);
// Cleanup
return () => {
document.getElementById(idRef.current)?.remove();
};
}, [color]);
return <button className={classNameRef.current}>Click me</button>;
}Dynamic Styled Component:
This button's styles are injected dynamically using useInsertionEffect. Change the color to see styles update before layout effects run.
Styles are injected synchronously before any layout measurements, preventing visual flicker.
Theme-Based Styling:
This component changes appearance based on theme. Styles are injected with useInsertionEffect to ensure they exist before the component renders.
Current theme: light
Styles injected with useInsertionEffect before layout
Execution Timing:
Click the button to see the execution order. Check your browser console to see that useInsertionEffect runs before useLayoutEffect.
⚠️ Important Notes:
- For library authors: useInsertionEffect is essential for CSS-in-JS libraries
- For app developers: You rarely need this - use CSS classes, CSS modules, or existing CSS-in-JS libraries instead
- Only use when you need to inject styles that must exist before layout effects run
- If you're building a CSS-in-JS library, this is the correct hook to use
useInsertionEffect vs Other Hooks:
Runs: Before DOM mutations, before useLayoutEffect
Runs: After DOM mutations, before paint
Runs: After paint (async)