React Basics of Hooks and Rules of Hooks
Overview
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:
- What React Hooks are and why they were created
- When and how to use hooks
- Overview of the most common built-in hooks
- Rules of Hooks and best practices
- Common issues and pitfalls to avoid
By the end of this guide, you’ll understand the fundamentals of React Hooks and be able to implement them following best practices.
What are React Hooks?
React Hooks are functions that allow you to “hook into” (use) React’s state and lifecycle features in function components. Before Hooks, managing state and handling lifecycle methods was done using class components, which often led to complex code.
The “old way” of doing things (Class components)
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>
);
}
}
What’s happening here?
- Class components use
this.stateto manage state. - The
setState()method is used to update the state, which triggers a re-render of the component. - This setup requires you to handle
thiskeyword binding and often leads to repetitive code in larger applications.
The “new way” of doing things (Functional components with hooks)
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>
);
}
What’s happening here?
- useState manages the
countvariable, which is the state of the counter. - The
setCountfunction updates the state when the button is clicked. React automatically rerenders the component with the updated state.
Why Hooks Were Created
- Reuse stateful logic - Hooks allow you to extract and share stateful logic between components without changing your component hierarchy
- Split complex components - Break down large components into smaller functions based on related pieces
- Use more React features without classes - Classes can be confusing with
thisbinding, requiring understanding of JavaScript’s prototype inheritance - More intuitive lifecycle management - Class component lifecycle methods often contained unrelated logic, making them hard to understand and maintain
🎬 Watch: React Today and Tomorrow - The original presentation introducing Hooks by React team members
When to Use Hooks
Hooks should be used when:
- You need to manage local component state - Use useState to track values that change over time
- You need to perform side effects - Use useEffect for data fetching, subscriptions, or DOM manipulations
- You want to share logic between components - Create custom hooks to extract and reuse component logic
- You need to access context - Use useContext to consume context without nesting
- You want to optimize performance - Use useMemo and useCallback to avoid unnecessary calculations and re-renders
Common Built-in Hooks
useState
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>
);
}
What’s happening here?
-
useState(0)initializescountwith a value of0. It returns two things:- The current state (
count). - A function (
setCount) to update the state.
- The current state (
-
The button’s
onClickhandler callssetCountto increase the count each time it’s clicked. React re-renders the component whenever the state changes.
For more details, see React useState Hook.
useEffect
The useEffect hook handles side effects in functional components, such as data fetching, subscribing to external APIs, or interacting with the DOM.
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>
);
}
What’s happening here?
useEffectis used to perform a side effect when the component mounts or updates.- The
document.titleis updated to reflect the current count. - The cleanup function is returned to run when the component unmounts.
For more details, see React useEffect Hook.
Rules of Hooks
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:
1. Only Call Hooks at the Top Level
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(() => {
// ...
});
}
// ...
}
What’s happening here?
- The
useStatehook is called at the top level of the function. - The
useEffecthook is called inside a condition, which breaks the rule.
2. Only Call Hooks from React Functions
Call hooks only from:
- React function components
- Custom hooks (functions starting with “use”)
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!
// ...
}
What’s happening here?
- The
useStatehook is called from a regular function, which breaks the rule. - The
useStatehook is called from a regular function, which breaks the rule.
🛠️ Tool: eslint-plugin-react-hooks automatically enforces these rules
Best Practices for Using Hooks
1. Create Custom Hooks for Reusable Logic
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>
);
}
What’s happening here?
- The
useFormInputhook is a custom hook that can be reused across multiple components. - The
useFormInputhook is a custom hook that can be reused across multiple components.
2. Keep Logic Organized in Multiple Hooks
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();
// ...
}
What’s happening here?
- The
useProductDatahook is a custom hook that can be reused across multiple components. - The
useShoppingCarthook is a custom hook that can be reused across multiple components. - The
useThemehook is a custom hook that can be reused across multiple components.
3. Be Careful with Dependencies Arrays
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
// ...
}
What’s happening here?
- The
useEffecthook is used to fetch results when thequerychanges. - The
queryis included as a dependency. Dependencies are the values that are used inside theuseEffecthook that should trigger the effect to run again.
4. Use Functional Updates for State
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
}
// ...
}
What’s happening here?
- In the second version, the
setCountfunction is called with the previous state value. This is the recommended way to update state.
Common Issues & Misconceptions
1. Infinite Render Loops
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>;
}
What’s happening here?
- In the first example, the dependency array is missing, so the effect will run on every render. This will cause an infinite loop.
- In the second example, the dependency array is provided, but empty, so the effect will only run once on mount. This is the recommended way to handle this.
2. Hooks vs Event Handlers
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>
);
}
What’s happening here?
- The
useStatehook is invoked inside an event handler, which breaks the rule.
3. Stale Closures
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>;
}
What’s happening here?
- In the first example, the
countvariable is captured in the closure of theuseEffecthook. This will cause a stale closure problem. If thecountvariable changes, theuseEffecthook will not be aware of the change. - In the second example, the
countvariable is not captured in the closure of theuseEffecthook. Instead, thesetCountfunction is called with the previous state value. This is the recommended way to update state, since we are not using closures to capture thecountvariable.