How to Use React Hooks: A Complete Guide with Examples
How to Use React Hooks: A Complete Guide with Examples
When React 16.8 introduced Hooks in early 2019, it fundamentally changed how developers build React applications by enabling function components to manage state, side effects, and context without writing class components . Understanding how to use React hooks effectively is now essential for any modern React developer, as Hooks have become "the only way most React code is written" . This guide will walk you through the essential built-in Hooks, explain the rules that govern them, and provide practical examples you can use immediately.
What You'll Learn
By the end of this guide, you'll confidently use React's core Hooks—useState, useEffect, useContext, useRef, useReducer, useMemo, and useCallback—in your own projects. You'll understand when to choose one Hook over another, how to avoid common pitfalls like unnecessary re-renders, and how to extract reusable logic into custom Hooks. The most important takeaway is that Hooks are just functions—learning to compose them is the key to mastering React.
Understanding the Rules of Hooks
Before diving into individual Hooks, you need to understand two non-negotiable rules enforced by the eslint-plugin-react-hooks package:
Only call Hooks at the top level. Never call Hooks inside loops, conditions, or nested functions. React relies on the order in which Hooks are called to preserve state between renders—if the order changes, React cannot correctly associate state with its Hook .
Only call Hooks from React functions. You may only call Hooks from function components or from other custom Hooks. A plain JavaScript function cannot call
useStateoruseEffect.Google AdInline article slot
A convention follows from the second rule: any function that calls Hooks must start with use (e.g., useCustomHook). The linter uses this prefix to enforce the rules correctly .
The Most Essential Hooks You'll Use Every Day
useState: Managing Component State
The useState Hook is the most fundamental Hook, allowing you to add state to function components . It returns an array with two elements: the current state value and a function to update it .
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Initial state is 0
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
When updating state based on the previous value, use the functional update form to avoid race conditions, especially with React's automatic batching :
// Safe: uses the previous state value
setCount(prevCount => prevCount + 1);
// Also safe for objects: spreads previous state
setUser(prevUser => ({ ...prevUser, name: 'Jane' }));
If computing the initial state is expensive, pass a function instead of a value—React will execute it only on the initial render :
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
useEffect: Handling Side Effects
The useEffect Hook lets you perform side effects in function components, such as fetching data, setting up subscriptions, or manually changing the DOM . By default, effects run after every completed render, but you can control when they fire using the dependency array .
import { useEffect, useState } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// This effect runs after the component mounts and when userId changes
fetchUser(userId).then(data => setUser(data));
// Optional cleanup function runs before the next effect and on unmount
return () => {
// Cancel any pending requests or clean up subscriptions
};
}, [userId]); // Dependency array: re-run only when userId changes
return <div>{user?.name}</div>;
}
Key patterns for useEffect:
- Empty dependency array
[]: Effect runs once after the initial render (mount) . - No dependency array: Effect runs after every render (rarely what you want).
- Cleanup function: Prevents memory leaks by cleaning up subscriptions, timers, or pending network requests .
Important warning: Not every state change requires an effect. If you can compute a value during rendering, do it directly instead of using useEffect:
// ❌ Unnecessary effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ Compute directly during render
const fullName = `${firstName} ${lastName}`;
useContext: Consuming Context Without Prop Drilling
The useContext Hook lets you read values from React Context without wrapping your component in a Consumer component :
import { useContext } from 'react';
// Create a context
const ThemeContext = React.createContext('light');
function ThemedButton() {
// Read context value directly
const theme = useContext(ThemeContext);
return <button className={theme}>Click me</button>;
}
When the context provider's value changes, any component using useContext re-renders automatically .
useRef: Referencing Values That Don't Trigger Re-renders
The useRef Hook creates a mutable ref object whose .current property persists across renders. Unlike state, updating a ref does not cause a re-render . This makes refs ideal for:
- Storing DOM element references
- Keeping track of timeout IDs
- Persisting values that shouldn't trigger UI updates
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input after the component mounts
inputRef.current.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
useReducer: Managing Complex State Logic
For state that involves multiple sub-values or complex transitions, useReducer is often preferable to useState. It accepts a reducer function and an initial state, returning the current state and a dispatch function :
import { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return { count: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset', payload: 0 })}>Reset</button>
</>
);
}
Performance Optimization Hooks
useMemo: Caching Expensive Calculations
useMemo caches the result of a computation and only recomputes it when dependencies change :
import { useMemo } from 'react';
function TodoList({ todos, filter }) {
// Only recompute when todos or filter change
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.status === filter);
}, [todos, filter]);
return (
<ul>
{filteredTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
useCallback: Memoizing Function References
useCallback returns a memoized version of a callback function that only changes if dependencies change. This is particularly useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary re-renders :
import { useCallback } from 'react';
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Function reference stays stable across renders
return <ChildButton onClick={handleClick} />;
}
Custom Hooks: Reusing Logic Across Components
The true power of Hooks emerges when you create custom Hooks to extract and reuse stateful logic . A custom Hook is simply a JavaScript function whose name starts with use and that can call other Hooks.
Example: useLocalStorage
This custom Hook syncs state with localStorage, persisting data across page reloads :
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
if (typeof window === 'undefined') return initialValue;
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
);
}
Example: useMediaQuery
This Hook lets you conditionally render components based on CSS media queries, such as user preference for reduced motion :
import { useState, useEffect } from 'react';
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQueryList = window.matchMedia(query);
const onChange = (event) => setMatches(event.matches);
mediaQueryList.addEventListener('change', onChange);
return () => mediaQueryList.removeEventListener('change', onChange);
}, [query]);
return matches;
}
// Usage
function MotionAwareComponent() {
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return prefersReducedMotion ? <StaticHero /> : <AnimatedHero />;
}
React 19: New Hooks for Forms and Async Data
React 19 (released December 2024) introduced several new Hooks designed to simplify form handling and asynchronous operations .
useActionState: Simplifying Form Submission
useActionState manages form submission state, returning the latest result, an action function, and a pending flag :
import { useActionState } from 'react';
async function subscribeAction(prevState, formData) {
const email = formData.get('email');
try {
await subscribe(email);
return { ok: true };
} catch (error) {
return { ok: false, error: error.message };
}
}
function NewsletterForm() {
const [state, formAction, isPending] = useActionState(
subscribeAction,
{ ok: false }
);
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{state.ok && <p>You're subscribed!</p>}
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
</form>
);
}
useFormStatus: Accessing Parent Form State
useFormStatus allows nested components to know whether their parent form is being submitted :
import { useFormStatus } from 'react';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
Integrating with Redux: The useSelector Hook
When using Redux with React, the useSelector Hook is the modern, recommended way to access state from the Redux store . It replaces the older connect Higher-Order Component and works better with TypeScript .
import { useSelector, useDispatch } from 'react-redux';
function CounterComponent() {
// Extract specific data from the Redux store
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>Increment</button>
</div>
);
}
Important: useSelector uses strict === reference equality by default. Returning a new object every time will force re-renders. Use memoized selectors or call useSelector multiple times for individual fields to optimize performance :
// ❌ This causes re-renders on every action
const { count, user } = useSelector((state) => ({
count: state.count,
user: state.user
}));
// ✅ Call useSelector for each field
const count = useSelector((state) => state.count);
const user = useSelector((state) => state.user);
Sources
- React Official Documentation: Built-in React Hooks
- React Official Documentation: Hooks API Reference
- React Redux Documentation: Hooks API
- Patterns.dev: Hooks Pattern
- React Official Documentation: Using the State Hook
— Editorial Team
No comments yet.