A Comprehensive Guide to React Hooks
Introduction
React Hooks revolutionized how we write React components by introducing a more intuitive way to handle state and side effects. Introduced in React 16.8, Hooks allow us to use state and other React features without writing class components (in functional components). This article will help you understand and master React Hooks through practical examples and best practices.
Hooks are functions that let you 'hook into' React state and lifecycle features from function components.
Table of Contents
- What are React Hooks?
- Core React Hooks
- Custom Hooks
- Best Practices
- Common Mistakes to Avoid
- Conclusion
What are React Hooks?
React Hooks are functions that let you use state and other React features in function components. They solve several problems that developers faced with class components:
- Complex Logic: Class components often required splitting related logic across different lifecycle methods
- Wrapper Hell: Higher-order components and render props patterns led to deeply nested components
- Confusing Classes: The
thiskeyword and class syntax could be confusing for many developers
Core React Hooks
useState Hook
The useState Hook is the most fundamental Hook that lets you add state to function components and update states that triggers re-renders.
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState("");
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<p>Hello, {name}!</p>
</div>
);
}
Important Points about useState
- useState allows you to add state to functional components.
- The setter function returned by useState updates the state and triggers a re-render.
- Always use the functional update form when the new state depends on the previous state:
setCount(prev => prev + 1). - State updates are asynchronous and may be batched for performance.
- Array destructuring makes it easy to access the state value and its setter.
useEffect Hook
The useEffect Hook lets you perform side effects in function components. It's similar to lifecycle methods in class components.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This effect runs after every render where userId changes
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error("Error fetching user:", error);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Important Points about useEffect
- useEffect runs after every render by default, but you can control when it runs using the dependency array.
- An empty dependency array (
[]) makes the effect run only once on mount (like componentDidMount). - Always include all variables used inside the effect in the dependency array to avoid bugs.
- Clean up side effects (like subscriptions or timers) by returning a cleanup function from useEffect.
- Avoid causing infinite loops by not updating state unconditionally inside useEffect.
useRef Hook
The useRef Hook creates a mutable reference that persists across renders.
It returns a object with .current property
import { useRef, useEffect } from "react";
function TextInputWithFocus() {
const inputRef = useRef(null);
useEffect(() => {
// Focus the input element on mount
inputRef.current?.focus();
}, []);
return (
<div>
<input
ref={inputRef}
type="text"
placeholder="This input will be focused"
/>
</div>
);
}
Important Points about useRef
- useRef is used to persist values across renders without causing re-renders.
- It is commonly used to access DOM elements directly.
- useRef is useful for storing mutable values that do not trigger a re-render when changed.
- Unlike useState, updating a ref does not cause the component to re-render.
- Great for keeping track of previous values or for storing interval IDs, etc.
useContext Hook
The useContext Hook lets you consume React context in function components.
import { createContext, useContext, useState } from "react";
// Create a context
const ThemeContext = createContext("light");
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className={theme}
>
Toggle Theme
</button>
);
}
Important Points about useContext
- useContext allows you to access context values directly in functional components.
- Use context to avoid prop drilling and share data across many components.
- Combine useContext with useState or useReducer to create updatable global state.
- You can use multiple contexts in a single component by calling useContext multiple times.
- Context is best for global data like themes, user info, or settings.
Custom Hooks
Custom Hooks let you extract component logic into reusable functions.
import { useState, useEffect } from "react";
// Custom Hook for window size
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return size;
}
// Using the custom Hook
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
Window size: {width} x {height}
</div>
);
}
Custom Hooks are perfect when:
- You want to reuse logic across multiple components (like form handling, animation, fetching).
- You want to keep components clean and separate concerns.
- You’re dealing with multiple effects or states that can be grouped logically.
Naming Rule: Custom Hooks must start with use (e.g., useForm, useDebounce) so React knows they follow the Rules of Hooks.
useMemo Hook
The useMemo Hook lets you memoize expensive computations so that they're only recalculated when their dependencies change.
import { useState, useMemo } from "react";
function ExpensiveComponent({ items }) {
const [filter, setFilter] = useState("");
const filteredItems = useMemo(() => {
// Simulate an expensive operation
return items.filter(item => item.toLowerCase().includes(filter.toLowerCase()));
}, [items, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Search"
/>
<ul>
{filteredItems.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
}
Key Points:
- Only use useMemo when the computation is expensive or the result is passed as a prop to children.
- It avoids recomputation unless dependencies change.
- Overuse can lead to unnecessary complexity.
useCallback Hook
The useCallback Hook returns a memoized version of a function, preventing it from being recreated on every render.
import { useState, useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<Child onClick={handleClick} />
<p>Count: {count}</p>
</div>
);
}
function Child({ onClick }) {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
}
Without useCallback, onClick would be a new function on each render, causing the child component to re-render unnecessarily.
Key Points: Use useCallback when passing callbacks to child components that rely on referential equality (React.memo, etc.).
Prevents unnecessary re-renders of memoized components.
Best Practices
-
Only call Hooks at the top level
- Don't call Hooks inside loops, conditions, or nested functions
- This ensures Hooks are called in the same order every render
-
Only call Hooks from React function components
- Don't call Hooks from regular JavaScript functions
- Custom Hooks must start with "use"
-
Use the dependency array correctly in useEffect
- Include all values from the component scope that the effect uses
- Empty array
[]means the effect runs only once on mount
-
Clean up side effects
- Return a cleanup function from useEffect when necessary
- This prevents memory leaks and ensures proper cleanup
-
Split complex logic into multiple Hooks
- Each Hook should have a single responsibility
- This makes the code more maintainable and testable
Common Mistakes to Avoid
- Infinite loops in useEffect
// ❌ Wrong: Missing dependency array
useEffect(() => {
setCount(count + 1);
});
// ✅ Correct: Empty dependency array for mount-only effect
useEffect(() => {
setCount(count + 1);
}, []);
- Stale closures
// ❌ Wrong: Using stale state in callback
function handleClick() {
setTimeout(() => {
console.log(count); // Might be stale
}, 1000);
}
// ✅ Correct: Using functional updates
function handleClick() {
setTimeout(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
}
- Unnecessary re-renders
// ❌ Wrong: Creating new object on every render
const value = { id: 1, name: "John" };
useEffect(() => {
// Effect runs on every render
}, [value]);
// ✅ Correct: Using useMemo for stable references
const value = useMemo(() => ({ id: 1, name: "John" }), []);
useEffect(() => {
// Effect runs only when dependencies change
}, [value]);
Conclusion
React Hooks have transformed how we write React components, making them more functional and easier to understand. By mastering useState, useEffect, useRef, and useContext, you can build powerful and maintainable React applications. Remember to follow the Rules of Hooks and implement the best practices discussed in this guide. Keep experimenting and building with Hooks to become a more efficient React developer!
By understanding and properly implementing React Hooks, you'll be able to write cleaner, more maintainable code and build better React applications.
