What Are React Hooks?
React Hooks revolutionized how we build React applications when introduced in version 16.8. These functions let you use state and other React features directly in functional components, eliminating the need for class components while making your code more intuitive and maintainable.
Before hooks, developers had to understand complex lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount. Hooks provide a cleaner, more composable approach that aligns with how we naturally think about component logic.
The introduction of hooks marked a fundamental shift in React development. According to the React documentation on custom hooks, hooks enable you to extract component logic into reusable functions that can be shared across multiple components. This architectural change has become the standard approach for building modern React applications.
Understanding hooks is essential for any developer working with React, as they form the foundation of component logic and state management in contemporary React applications.
useState
Manage local component state with a simple, intuitive API that returns the current value and an update function.
useEffect
Handle side effects like data fetching, subscriptions, and DOM manipulation with precise control over when code runs.
useRef
Access DOM elements directly and persist mutable values across renders without triggering re-renders.
useContext
Access global state from any component without prop drilling through your component tree.
useCallback
Memoize callback functions to prevent unnecessary re-renders in child components.
useMemo
Cache expensive computations and derive values efficiently without recalculating on every render.
The useState Hook: Managing Local State
The useState hook is the foundation of state management in functional components. It returns an array with two elements: the current state value and a function to update it.
Key Patterns for Effective State Management
Lazy Initialization: When your initial state requires expensive computation, pass a function to useState to defer the calculation until the initial render. This prevents unnecessary computation on every re-render.
Functional Updates: When updating state based on the previous value, use the functional form of the setter to ensure you're working with the most current state value. This prevents race conditions and stale state issues, especially important when multiple updates may be queued.
As highlighted in modern React hooks tutorials, proper useState patterns form the bedrock of predictable component behavior.
For a deeper dive into state concepts, see our guide on React State which covers state management patterns in detail.
1import React, { useState } from 'react';2 3function Counter() {4 // Basic state with initial value5 const [count, setCount] = useState(0);6 7 // Lazy initialization for expensive initial state8 const [data] = useState(() => {9 return expensiveInitialComputation();10 });11 12 // Functional update ensures accurate state13 const increment = () => {14 setCount(prevCount => prevCount + 1);15 };16 17 return (18 <div>19 <p>Count: {count}</p>20 <button onClick={increment}>21 Increment22 </button>23 </div>24 );25}1function UserProfile({ userId }) {2 const [user, setUser] = useState(null);3 const [loading, setLoading] = useState(true);4 5 useEffect(() => {6 let isMounted = true;7 8 const fetchUser = async () => {9 setLoading(true);10 try {11 const response = await fetch(12 `/api/users/${userId}`13 );14 const userData = await response.json();15 if (isMounted) {16 setUser(userData);17 }18 } catch (error) {19 console.error('Fetch error:', error);20 } finally {21 if (isMounted) {22 setLoading(false);23 }24 }25 };26 27 fetchUser();28 29 // Cleanup prevents memory leaks30 return () => {31 isMounted = false;32 };33 }, [userId]);34 35 if (loading) return <Loading />;36 if (!user) return <NotFound />;37 38 return <div>{user.name}</div>;39}The useEffect Hook: Handling Side Effects
The useEffect hook handles side effects in functional components--operations that need to happen outside the render process like data fetching, subscriptions, or DOM manipulation.
The Dependency Array
The dependency array controls when your effect runs:
- Empty array
[]: Effect runs only once on mount - No array: Effect runs after every render
- With dependencies
[a, b]: Effect runs whenaorbchanges
Cleanup Is Essential
Always return a cleanup function from your effect to prevent memory leaks, stale closures, and unexpected behavior. This cleanup runs before the component unmounts and before each subsequent effect run.
Production-level patterns for useEffect, as documented by DEV Community, emphasize proper cleanup and dependency management as critical for avoiding subtle bugs.
1import { useCallback, useMemo, memo } from 'react';2 3function ProductList({ products, filter }) {4 // Memoize expensive computation5 const filteredProducts = useMemo(() => {6 return products7 .map(p => calculatePrice(p))8 .filter(p => p.stock > 0)9 .filter(p => p.name.includes(filter));10 }, [products, filter]);11 12 // Memoize callback to prevent child re-renders13 const handleAddToCart = useCallback((product) => {14 addToCart(product);15 showNotification(`${product.name} added`);16 }, []);17 18 return (19 <ul>20 {filteredProducts.map(product => (21 <ProductItem22 key={product.id}23 product={product}24 onAdd={handleAddToCart}25 />26 ))}27 </ul>28 );29}30 31// Memoize entire component for extra protection32export default memo(ProductList);Custom Hooks: Extracting Reusable Logic
Custom hooks are the key to building maintainable React applications. They allow you to extract component logic into reusable functions that can be shared across multiple components.
Building Effective Custom Hooks
A well-designed custom hook:
- Starts with "use" prefix (required by React linting rules)
- Focuses on a single responsibility
- Returns useful values or functions for the consuming component
- Handles edge cases and error states appropriately
Real-World Example: LocalStorage Hook
The useLocalStorage hook demonstrates how to create a reusable abstraction around browser storage with proper error handling and type safety. This pattern, as explored in Telerik's React design patterns guide, exemplifies how custom hooks can encapsulate complex logic behind a simple API.
1// Custom hook for persistent local storage2function useLocalStorage(key, initialValue) {3 // Lazy initialization4 const [storedValue, setStoredValue] = useState(() => {5 try {6 const item = window.localStorage.getItem(key);7 return item ? JSON.parse(item) : initialValue;8 } catch (error) {9 console.error('Error reading localStorage:', error);10 return initialValue;11 }12 });13 14 // Set value with error handling15 const setValue = (value) => {16 try {17 // Allow function or direct value18 const valueToStore = 19 value instanceof Function ? value(storedValue) : value;20 setStoredValue(valueToStore);21 window.localStorage.setItem(22 key, 23 JSON.stringify(valueToStore)24 );25 } catch (error) {26 console.error('Error writing localStorage:', error);27 }28 };29 30 return [storedValue, setValue];31}32 33// Usage across multiple components34function SettingsPanel() {35 const [theme, setTheme] = useLocalStorage('theme', 'light');36 const [notifications, setNotifications] = 37 useLocalStorage('notifications', true);38 39 return (40 <div className={theme}>41 <label>42 <input43 type="checkbox"44 checked={notifications}45 onChange={e => setNotifications(e.target.checked)}46 />47 Enable Notifications48 </label>49 </div>50 );51}DRY Principle
Write logic once and reuse it across multiple components without duplication.
Testability
Test custom hooks in isolation from your UI components for more focused unit tests.
Abstraction
Hide complex implementation details behind a simple, focused API.
Consistency
Ensure the same behavior across all components using the hook.
Common Pitfalls and How to Avoid Them
Even experienced React developers encounter these common hook-related issues. Here's how to recognize and fix them.
The Stale Closure Problem
A stale closure occurs when a callback captures outdated state values. This commonly happens when using state inside setInterval or event listeners without proper dependencies. The solution is to use functional updates or include the changing value in your dependency array.
Missing Dependency Arrays
Omitting the dependency array from useEffect is a frequent source of bugs. Without it, your effect runs after every render, which can cause performance issues, infinite loops, and unexpected behavior with state that was meant to be static.
For teams building production React applications, following consistent hooks patterns is essential. Our web development services team specializes in implementing robust React architectures that avoid these common pitfalls.
1// ❌ WRONG: Stale closure with setInterval2function WrongTimer() {3 const [count, setCount] = useState(0);4 5 useEffect(() => {6 const interval = setInterval(() => {7 // count is always 0 here due to stale closure!8 setCount(count + 1);9 }, 1000);10 11 return () => clearInterval(interval);12 }, []); // Missing count dependency13 14 return <div>{count}</div>;15}16 17// ✅ CORRECT: Functional update18function CorrectTimer() {19 const [count, setCount] = useState(0);20 21 useEffect(() => {22 const interval = setInterval(() => {23 // Using functional update prevents stale closure24 setCount(prev => prev + 1);25 }, 1000);26 27 return () => clearInterval(interval);28 }, []); // Empty deps work because we don't use count directly29 30 return <div>{count}</div>;31}1// ❌ WRONG: Hook inside conditional2function WrongComponent({ showData }) {3 if (showData) {4 const [data] = useFetchData(); // BUG!5 // State gets reset if condition changes6 }7 return <div>{/* ... */}</div>;8}9 10// ✅ CORRECT: Always call hooks at top level11function CorrectComponent({ showData }) {12 // Hooks always run in the same order13 const [data] = useFetchData();14 const [count, setCount] = useState(0);15 16 // Use conditional rendering in JSX, not in hook calls17 return (18 <div>19 {showData && <DataDisplay data={data} />}20 <button onClick={() => setCount(count + 1)}>21 Count: {count}22 </button>23 </div>24 );25}