
Introduction to React Hooks
React Hooks, introduced in React 16.8, revolutionized how developers write React components by enabling state and other React features in functional components. Before Hooks, functional components were considered “stateless” and developers had to use class components for state management and lifecycle methods. Hooks solved this limitation while promoting better code organization and reusability.
What Are Hooks?
Hooks are functions that “hook into” React state and lifecycle features from functional components. They don’t work inside classes – they let you use React without classes.
Built-in Hooks
useState
The useState Hook allows you to add state to functional components. It returns a stateful value and a function to update it.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Key features of useState:
- Initial state: Pass the initial state to useState()
- Array destructuring: useState returns a pair of values that we typically destructure
- Multiple states: You can use useState multiple times in a single component
- Functional updates: You can pass a function to the state updater
// Functional update pattern
setCount(prevCount => prevCount + 1);
useEffect
The useEffect Hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes.
import React, { useState, useEffect } from 'react';
function DocumentTitleUpdater() {
const [count, setCount] = useState(0);
useEffect(() => {
// This runs after every render
document.title = `You clicked ${count} times`;
// Optional cleanup function
return () => {
document.title = 'React App';
};
}, [count]); // Only re-run if count changes
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Key features of useEffect:
- Dependency array: The second argument controls when the effect runs
- Cleanup function: Return a function to clean up resources
- Multiple effects: You can use useEffect multiple times in a component
- Empty dependency array: [] means “run only once after initial render”
- No dependency array: No second argument means “run after every render”
useContext
The useContext Hook accepts a context object (created by React.createContext) and returns the current context value. This makes consuming context much more straightforward.
import React, { useContext } from 'react';
// Create a context
const ThemeContext = React.createContext('light');
function ThemedButton() {
// Consume the context
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme === 'dark' ? '#333' : '#fff',
color: theme === 'dark' ? '#fff' : '#333' }}>
I am styled based on the theme context!
</button>
);
}
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
useReducer
The useReducer Hook is an alternative to useState for complex state logic. It’s especially useful when the next state depends on the previous state.
import React, { 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 };
default:
throw new Error('Unexpected action');
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
</div>
);
}
useCallback
The useCallback Hook returns a memoized callback function that only changes if one of the dependencies has changed. This is useful for preventing unnecessary renders in child components that rely on reference equality.
import React, { useState, useCallback } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
// This function is recreated only when count changes
const handleClick = useCallback(() => {
console.log(`Button clicked with count: ${count}`);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildComponent onClick={handleClick} />
</div>
);
}
// This component re-renders only when props actually change
const ChildComponent = React.memo(function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
});
useMemo
The useMemo Hook returns a memoized value. It’s similar to useCallback, but for values instead of functions. Use it to avoid expensive calculations on every render.
import React, { useState, useMemo } from 'react';
function ExpensiveCalculation({ items }) {
const [count, setCount] = useState(0);
// This calculation runs only when items changes
const expensiveResult = useMemo(() => {
console.log('Calculating...');
return items.reduce((total, item) => total + item, 0);
}, [items]);
return (
<div>
<p>Count: {count}</p>
<p>Expensive Calculation Result: {expensiveResult}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useRef
The useRef Hook creates a mutable ref object whose .current property is initialized to the passed argument. The object persists for the full lifetime of the component.
import React, { useRef, useEffect } from 'react';
function FocusInput() {
// Create a ref
const inputRef = useRef(null);
// Focus the input when component mounts
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<input ref={inputRef} type="text" placeholder="I'll be focused on mount" />
);
}
Common use cases for useRef:
- Accessing DOM elements directly
- Keeping mutable values that don’t trigger re-renders
- Storing previous state values
useLayoutEffect
The useLayoutEffect Hook is identical to useEffect, but it fires synchronously after all DOM mutations. Use it when you need to read layout from the DOM and synchronously re-render.
import React, { useState, useLayoutEffect, useRef } from 'react';
function Tooltip() {
const [tooltipHeight, setTooltipHeight] = useState(0);
const tooltipRef = useRef(null);
useLayoutEffect(() => {
const height = tooltipRef.current.offsetHeight;
// This runs synchronously after DOM updates but before browser paint
setTooltipHeight(height);
}, []);
return (
<div>
<div ref={tooltipRef} className="tooltip">
This is a tooltip
</div>
<p>The tooltip height is: {tooltipHeight}px</p>
</div>
);
}
useImperativeHandle
The useImperativeHandle Hook customizes the instance value that is exposed when using React.forwardRef. It lets parent components call methods on child components.
import React, { useRef, useImperativeHandle, forwardRef } from 'react';
// Child component with forwarded ref
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef(null);
// Expose only specific methods to parent
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
reset: () => {
inputRef.current.value = '';
}
}));
return <input ref={inputRef} {...props} />;
});
// Parent component using the custom methods
function Parent() {
const fancyInputRef = useRef(null);
return (
<div>
<FancyInput ref={fancyInputRef} />
<button onClick={() => fancyInputRef.current.focus()}>
Focus Input
</button>
<button onClick={() => fancyInputRef.current.reset()}>
Reset Input
</button>
</div>
);
}
useDebugValue
The useDebugValue Hook can be used to display a label for custom hooks in React DevTools.
import React, { useState, useEffect, useDebugValue } from 'react';
// Custom hook with debug value
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
// Shows "Online: true" or "Online: false" in React DevTools
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}
function StatusIndicator() {
const isOnline = useOnlineStatus();
return <p>You are {isOnline ? 'online' : 'offline'}</p>;
}
Rules of Hooks
To ensure hooks work correctly, you must follow these two rules:
1. Only Call Hooks at the Top Level
Don’t call hooks inside loops, conditions, or nested functions. This ensures the hooks are called in the same order each time a component renders.
// ❌ Wrong: Hook inside a condition
function BadComponent() {
const [count, setCount] = useState(0);
if (count > 0) {
// This breaks the rules of Hooks
const [name, setName] = useState('');
}
// ...
}
// ✅ Correct: All Hooks at the top level
function GoodComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Use the state variables normally
if (count > 0) {
// ...
}
// ...
}
2. Only Call Hooks from React Functions
Only call hooks from React functional components or from custom hooks. Don’t call hooks from regular JavaScript functions.
// ✅ Good: Called from a functional component
function MyComponent() {
const [count, setCount] = useState(0);
// ...
}
// ✅ Good: Called from a custom Hook
function useCustomHook() {
const [value, setValue] = useState(0);
// ...
return value;
}
// ❌ Bad: Called from a regular function
function regularFunction() {
const [value, setValue] = useState(0); // This breaks the rules
// ...
}
Creating Custom Hooks
One of the most powerful features of Hooks is the ability to create your own custom hooks. A custom hook is a JavaScript function that starts with “use” and can call other hooks.
import { useState, useEffect } from 'react';
// Custom hook for fetching data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, { signal: abortController.signal });
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
}
}
fetchData();
// Cleanup function to abort fetch on unmount or URL change
return () => abortController.abort();
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(`https://api.example.com/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<div>
<h1>{data.name}</h1>
<p>Email: {data.email}</p>
</div>
);
}
More Custom Hook Examples
useLocalStorage
A hook to persist state in localStorage.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Initialize state from localStorage or use initialValue
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
// Update localStorage when state changes
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error writing to localStorage:', error);
}
}, [key, value]);
return [value, setValue];
}
// Example usage
function PersistentForm() {
const [name, setName] = useLocalStorage('username', '');
return (
<div>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Your name will persist even after refresh!</p>
</div>
);
}
useMedia
A hook to track media query matches.
import { useState, useEffect } from 'react';
function useMedia(query) {
// Initialize with current match state
const [matches, setMatches] = useState(() =>
window.matchMedia(query).matches
);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
// Update state on change
const updateMatches = () => setMatches(mediaQuery.matches);
// Modern browsers
mediaQuery.addEventListener('change', updateMatches);
// Initial check
updateMatches();
// Cleanup
return () => {
mediaQuery.removeEventListener('change', updateMatches);
};
}, [query]);
return matches;
}
// Example usage
function ResponsiveComponent() {
const isMobile = useMedia('(max-width: 768px)');
return (
<div>
<h1>This website is optimized for {isMobile ? 'mobile' : 'desktop'}</h1>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
</div>
);
}
Hooks vs. Classes
React Hooks offer several advantages over class components:
Feature | Hooks | Classes |
Syntax complexity | Simpler, more concise | More verbose, requires this |
State management | useState or useReducer | this.state and this.setState() |
Side effects | useEffect combines lifecycle methods | Scattered across multiple lifecycle methods |
Code reuse | Custom hooks | Higher-Order Components or render props |
Mental model | Functions and closures | Classes and instances |
Learning curve | Easier for JavaScript developers | Requires understanding of this binding |
TypeScript support | Better type inference | Sometimes requires explicit types |
Bundle size | Generally smaller | Slightly larger |
Advanced Patterns with Hooks
Optimizing Performance
Memoization with useCallback and useMemo
import React, { useState, useCallback, useMemo } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [sortOrder, setSortOrder] = useState('asc');
const [results, setResults] = useState([]);
// Memoized handler - recreated only when query changes
const handleSearch = useCallback(() => {
// Fetch search results
fetchResults(query).then(setResults);
}, [query]);
// Memoized derived data - recalculated only when results or sortOrder changes
const sortedResults = useMemo(() => {
console.log('Sorting results');
return [...results].sort((a, b) => {
if (sortOrder === 'asc') {
return a.name.localeCompare(b.name);
} else {
return b.name.localeCompare(a.name);
}
});
}, [results, sortOrder]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<button onClick={handleSearch}>Search</button>
<select value={sortOrder} onChange={e => setSortOrder(e.target.value)}>
<option value="asc">A-Z</option>
<option value="desc">Z-A</option>
</select>
<ul>
{sortedResults.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
State Initialization Patterns
Lazy Initial State
function ExpensiveInitialState() {
// Bad: This function runs on every render
const [state, setState] = useState(calculateExpensiveInitialState());
// Good: This function runs only once during initialization
const [state, setState] = useState(() => calculateExpensiveInitialState());
// ...
}
Abstracting Complex Logic
Custom Hook for Form Handling
import { useState } from 'react';
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
};
const handleBlur = (e) => {
const { name } = e.target;
setTouched({
...touched,
[name]: true
});
};
const handleSubmit = (onSubmit) => (e) => {
e.preventDefault();
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
setIsSubmitting(true);
onSubmit(values);
}
};
// Example validation function
const validate = (values) => {
const errors = {};
if (!values.email) {
errors.email = 'Required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Invalid email format';
}
if (!values.password) {
errors.password = 'Required';
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
return errors;
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit
};
}
// Example usage
function SignupForm() {
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit
} = useForm({ email: '', password: '' });
const submitForm = (values) => {
console.log('Form submitted with:', values);
// API call here
};
return (
<form onSubmit={handleSubmit(submitForm)}>
<div>
<label>Email</label>
<input
type="email"
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</div>
<div>
<label>Password</label>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Common Hooks Pitfalls and How to Avoid Them
1. Dependency Array Issues
// ❌ Missing dependencies
useEffect(() => {
console.log(`Value changed to: ${value}`);
// value is used but not listed in dependencies
}, []);
// ✅ Correct dependencies
useEffect(() => {
console.log(`Value changed to: ${value}`);
}, [value]);
2. Stale Closures
// ❌ Stale closure problem
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// This always uses the initial value of count (0)
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependency array
return <div>{count}</div>;
}
// ✅ Functional update to solve stale closure
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// This uses the latest state value
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependency array is fine now
return <div>{count}</div>;
}
3. Infinite Loops
// ❌ Infinite loop
function BadComponent() {
const [data, setData] = useState([]);
// This effect runs after every render
useEffect(() => {
fetchData().then(newData => setData(newData));
}); // No dependency array
return <div>{/* render data */}</div>;
}
// ✅ Correct implementation
function GoodComponent() {
const [data, setData] = useState([]);
// This effect runs only once after the initial render
useEffect(() => {
fetchData().then(newData => setData(newData));
}, []); // Empty dependency array
return <div>{/* render data */}</div>;
}
4. Over-optimizing with useMemo and useCallback
// ❌ Unnecessary optimization
function ListItem({ item }) {
// This is overkill for simple values
const displayName = useMemo(() => {
return `${item.firstName} ${item.lastName}`;
}, [item.firstName, item.lastName]);
return <li>{displayName}</li>;
}
// ✅ Better approach: only memoize expensive operations
function ExpensiveList({ items, threshold }) {
// This is appropriate for expensive filtering operations
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
complexExpensiveFilter(item, threshold)
);
}, [items, threshold]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
Best Practices and Tips
- Start with useState and useEffect: Master these basic hooks before moving to more complex ones.
- Extract complex logic into custom hooks: If you find yourself repeating similar hook patterns, consider creating a custom hook.
- Use the ESLint plugin: The eslint-plugin-react-hooks will catch many common mistakes.
- Keep components focused: If a component uses too many hooks, it might be doing too much and should be split.
- Name custom hooks meaningfully: Start with “use” and choose a name that clearly indicates what the hook does.
- Test hooks in isolation: Use testing libraries like @testing-library/react-hooks to test custom hooks.
- Set realistic dependency arrays: Don’t just add dependencies to silence ESLint warnings. Understand why each dependency is needed.
- Consider useReducer for complex state: When state updates depend on previous state or multiple state variables depend on each other.
- Limit hook nesting: Avoid deeply nested custom hooks calling other custom hooks – it makes debugging difficult.
- Profile before optimizing: Use React DevTools Performance tab to identify actual performance issues before adding memoization.
Summary
React Hooks transform how we write React components by enabling functional components to use state, side effects, and other React features. The core built-in hooks (useState, useEffect, useContext, useReducer, useCallback, useMemo, useRef) provide the building blocks for creating clean, reusable, and efficient React applications.
Custom hooks allow developers to extract and share stateful logic between components, leading to more maintainable code. Following the rules of hooks and best practices ensures your components behave correctly and perform well.
As you gain experience with hooks, you’ll discover they simplify many complex patterns that were cumbersome with class components while encouraging better practices like co-locating related logic.