React Hooks were introduced in React 16.8 as a way to use state and other React features without writing class components. They revolutionized React development by enabling function components to manage state, handle side effects, and tap into React's lifecycle features.
This guide covers:
By the end of this guide, you'll understand the fundamentals of React Hooks and be able to implement them following best practices.
Hooks are functions that let you "hook into" React state and lifecycle features from function components. Before hooks, developers had to use class components for state management and lifecycle methods, which often led to complex components with duplicated logic across lifecycle methods.
You may find documentation or dive into a legacy codebase and see something like this. This is a class component. We are only providing this as a reference, as we do not recommend using class components.
// Before hooks (Class component)
class Counter extends React.Component {
// the class components way of initializing state
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
{/* this.setState() is a class method, which is not recommended */}
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
While Class Components work, we should prefer using React Hooks, since they are more intuitive, easier to understand and maintain, and all newer React codebases are using hooks.
// After hooks (Function component)
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
this
binding, requiring understanding of JavaScript's prototype inheritance🎬 Watch: React Today and Tomorrow - The original presentation introducing Hooks by React team members
Hooks should be used when:
The useState hook allows functional components to manage state. It returns a stateful value and a function to update it.
import React, { useState } from 'react';
function Example() {
// Declare a state variable "count" with initial value 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
For more details, see React useState Hook.
The useEffect hook performs side effects in function components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount in React classes.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
// Optional return function for cleanup (like componentWillUnmount)
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>
);
}
For more details, see React useEffect Hook.
React relies on the order in which Hooks are called to correctly preserve state between multiple useState
and useEffect
calls. This requires following two critical rules:
Never call hooks inside loops, conditions, or nested functions. React needs to call hooks in the same order each time a component renders to correctly preserve state between renders.
// ✅ Good: Hooks at the top level
function Form() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
// ...
}
// ❌ Bad: Hook inside a condition
function Form() {
const [name, setName] = useState('');
if (name !== '') {
// This breaks the rule!
useEffect(() => {
// ...
});
}
// ...
}
Call hooks only from:
Don't call hooks from regular JavaScript functions or class components.
// ✅ Good: Called from a React function component
function Example() {
const [count, setCount] = useState(0);
// ...
}
// ✅ Good: Called from a custom Hook
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
// ...
return size;
}
// ❌ Bad: Called from a regular function
function regularFunction() {
const [count, setCount] = useState(0); // This is not allowed!
// ...
}
🛠️ Tool: eslint-plugin-react-hooks automatically enforces these rules
Extract common stateful logic into custom hooks that can be shared across components.
// Custom hook for managing form fields
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
function handleChange(e) {
setValue(e.target.value);
}
return {
value,
onChange: handleChange
};
}
// Using the custom hook in multiple components
function LoginForm() {
const username = useFormInput('');
const password = useFormInput('');
return (
<form>
<input type="text" {...username} />
<input type="password" {...password} />
</form>
);
}
Split complex logic into multiple hooks rather than creating one giant hook. This improves readability and makes testing easier.
function ProductPage({ productId }) {
// Separate concerns into different hooks
const product = useProductData(productId);
const cart = useShoppingCart();
const theme = useTheme();
// ...
}
Always include all values from the component scope that change over time and are used by the effect in the dependencies array.
function SearchResults({ query }) {
const [results, setResults] = useState([]);
// ✅ Good: All dependencies included
useEffect(() => {
fetchResults(query).then(data => setResults(data));
}, [query]); // query is included as a dependency
// ...
}
When updating state based on previous state, use the functional form of the state setter.
// ❌ May lead to stale state issues
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(count + 1); // Uses the captured count value
}
// ...
}
// ✅ Safer approach using functional updates
function Counter() {
const [count, setCount] = useState(0);
function increment() {
setCount(prevCount => prevCount + 1); // Always uses latest state
}
// ...
}
A common mistake is creating an infinite loop by updating state in an effect without a proper dependency array.
// ❌ Infinite loop!
function BadComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Updates state, triggers re-render, runs effect again...
}); // Missing dependency array
return <div>{count}</div>;
}
// ✅ Fixed version
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
// Only run once on mount
setCount(prevCount => prevCount + 1);
}, []); // Empty dependency array
return <div>{count}</div>;
}
A common misconception is confusing when to use hooks versus when to use event handlers.
function Form() {
const [name, setName] = useState('');
// ❌ Don't use hooks for event handling
const handleClick = () => {
// This is wrong!
const [data, setData] = useState(null);
// ...
};
// ✅ Do define event handlers as regular functions
const handleSubmit = async (e) => {
e.preventDefault();
const response = await submitForm(name);
// ...
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
Functions defined in your components capture the props and state from the render they were created in. This can lead to unexpected behaviors.
function Timer() {
const [count, setCount] = useState(0);
// ❌ Problematic: This closure captures the initial count value (0)
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count);
setCount(count + 1); // Will always use the initial value of count
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array means this only runs on mount
// ✅ Fixed: Use functional update and don't depend on count in closure
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1); // Always uses the latest count
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency is fine now since we don't use count inside
return <div>{count}</div>;
}