React Hooks Rules: Your Essential Guide to Clean, Stable Components


React Hooks have revolutionized how we write React components, allowing us to leverage state and lifecycle features directly within functional components. This makes our code cleaner, more concise, and easier to understand. However, to truly harness their power and avoid unexpected bugs, it's crucial to understand and adhere to the "Rules of Hooks."
These rules aren't arbitrary; they are fundamental to how React internally manages the state and side effects associated with your components. Breaking them can lead to unpredictable behavior, difficult-to-debug issues, and a less stable application.
Let's dive into the two core rules of React Hooks with clear explanations and examples.
The Two Golden Rules of React Hooks
There are two primary rules you must always follow when using React Hooks:
Only Call Hooks at the Top Level.
Only Call Hooks from React Functions.
Let's break down each rule.
Rule 1: Only Call Hooks at the Top Level.
This rule means you should never call Hooks inside:
Loops
Conditional statements (like if statements)
Nested functions
Event handlers
Instead, always declare your Hooks at the very top of your functional component, before any early returns or other logic.
Why is this rule important?
React relies on the order in which Hooks are called to associate internal state and effects with the correct Hook. When your component re-renders, React expects the Hooks to be called in the exact same sequence as they were in the initial render.
If you call a Hook conditionally or within a loop, the order of Hook calls might change between renders. This would confuse React, causing it to lose track of which state belongs to which useState call, or which effect belongs to which useEffect. This leads to unpredictable behavior, stale data, and often, crashes.
Incorrect Usage (Violating Rule 1):
//Example 1:
import React, { useState, useEffect } from 'react';
function ComponentA({ someCondition }) {
if (someCondition) {
// Incorrect: Calling useState inside a conditional statement
const [count, setCount] = useState(0);
}
return (
<div>
<p>Count: {/* count is not accessible here if someCondition is false */}</p>
</div>
);
}
Explanation: In ComponentA, if “someCondition” changes from true to false (or vice-versa), the useState Hook might not be called, or its order relative to other Hooks could shift, causing React to mismanage state.
//Example 2:
function ComponentB() {
for (let i = 0; i < 5; i++) {
// Incorrect: Calling useState inside a loop
const [value, setValue] = useState(i);
}
return <div>Some Text</div>;
}
Explanation: Similarly, in ComponentB, the number of useState calls would depend on the loop's iteration, leading to complete chaos for React's internal state management.
Correct Usage (Adhering to Rule 1):
If you need conditional logic related to a Hook, place the condition inside the Hook's callback or after the Hook declaration.
//Example 1:
import React, { useState, useEffect } from 'react';
function ComponentA({ someCondition }) {
const [count, setCount] = useState(0); // Correct: Call useState at the top level
useEffect(() => {
// correct: Conditional logic inside useEffect's callback
if (someCondition) {
//executes some task
}
}, [count, someCondition]); // Dependencies ensure effect re-runs when needed
const handleClick = () => {
// correct: You can use the state update function conditionally
if (count < 10) {
setCount(prevCount => prevCount + 1);
}
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
Here, useState is always called, ensuring its consistent position. The conditional logic for updating the document title is handled within the useEffect callback, which is perfectly fine.
Rule 2: Only Call Hooks from React Functions.
This rule means you should only call Hooks from:
React functional components (e.g; function MyComponent(){…})
Custom Hooks (functions whose names start with use, like useFetchData())
You should not call Hooks from:
Regular JavaScript functions (that are not React components or custom Hooks)
Class components (Hooks are designed for functional components)
Why is this rule important?
Hooks are deeply integrated with React's rendering cycle and its internal mechanisms for managing state and side effects. When you call a Hook, React needs to know which component it belongs to so it can associate the Hook's state or effect with that specific component instance.
Calling Hooks outside of a React function context breaks this connection. React won't have the necessary context to manage the Hook, leading to errors.
Incorrect Usage (Violating Rule 2):
//Example 1:
import React, { useState } from 'react';
// Incorrect: Not a React functional component or a custom Hook becuase it is normal JS function
function myUtilityFunction() {
const [data, setData] = useState(null); // This will throw an error
}
//Example 2:
class MyClassComponent extends React.Component {
render() {
const [count, setCount] = useState(0); // Incorrect: Cannot use Hooks in class components
return <div>Hello Class!</div>;
}
}
Correct Usage (Adhering to Rule 2):
import React, { useState, useEffect } from 'react';
// Correct: Called within a React functional component
function MyFunctionalComponent() {
const [name, setName] = useState('Alice');
useEffect(() => {
console.log(`Name changed to: ${name}`);
}, [name]);
return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}!</p>
</div>
);
}
In the above correct example, Hooks are exclusively used within MyFunctionalComponent (a functional component) that is a good practice.
// Correct: Called within a custom Hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
return { count, increment, decrement };
}
function CountComponent() {
const { count, increment, decrement } = useCounter(10); // Using the custom Hook
return (
<div>
<p>Current count from custom hook: {count}</p>
<button onClick={increment}>Add</button>
<button onClick={decrement}>Subtract</button>
</div>
);
}
In the above example, useCounter (a custom Hook) also adhere to the hooks rule ensuring React can properly manage their state and effects.
Additional:
There is another third rule that is:
"Call Hooks in the Same Order on Every Render,"
It is arguably the most important underlying principle that the first two rules enforce. Let's delve into it more deeply.
Why is this rule absolutely critical?
When React renders a component that uses Hooks, it maintains an internal "memory" for that component. This memory is essentially an array or a linked list of Hook states and effects, where each entry corresponds to a specific Hook call within your component.
Consider this simplified mental model:
// Internal React memory for Below Component:
[
// 0: state for first useState()
// 1: effect for first useEffect()
// 2: state for second useState()
// ...and so on
]
When your component re-renders, React iterates through this internal memory structure, matching the Hook calls in your code with the corresponding entries in its memory, based purely on their order.
If the order of Hook calls changes between renders (e.g., due to a conditional Hook call, a Hook inside a loop, or an early return), React gets confused. It will incorrectly associate a Hook's state or effect with a different entry in its internal memory, leading to:
Incorrect state values: Your useState might return the value that was meant for another useState call.
Effects firing at the wrong time or not at all: Your useEffect might not run when expected, or it might run when it shouldn't.
Runtime errors: React might throw errors like "Rendered more hooks than during the previous render."
Incorrect Usage (Violating Rule 3, often caused by violating Rule 1):
import React, { useState, useEffect } from 'react';
function SomeBadComponent({ showExtraFeature }) {
const [count, setCount] = useState(0); // This is Hook #1
if (showExtraFeature) {
// Incorrect: This Hook (Hook #2) is called conditionally.
// Its position in the call order changes based on `showExtraFeature`.
const [extraData, setExtraData] = useState('initial');
}
// If showExtraFeature changes from true to false,
// the order of Hooks changes from (useState, useState) to (useState).
// React will lose track of `extraData`.
useEffect(() => { // This is Hook #2 or #3 depending on the condition
console.log('Count changed:', count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* If showExtraFeature is true, extraData might be used here */}
</div>
);
}
In SomeBadComponent above:
When showExtraFeature is true: The Hooks called are useState(0), useState('initial'), and useEffect(). Their internal indices might be 0, 1, 2.
When showExtraFeature is false: The Hooks called are useState(0), and useEffect(). Their internal indices might be 0, 1.
Notice how useEffect() which was at index 2 is now at index 1. This causes useEffect() to potentially be associated with the state of extraData from a previous render (if showExtraFeature was true before), or simply cause an error because React expects a Hook at that position that isn't there.
Correct Usage (Adhering to Rule 3 by adhering to Rule 1):
To ensure Hooks are called in the same order every time, you must declare them unconditionally at the top level of your component. If you need conditional logic, perform it inside the Hook's callback or after the Hook declaration.
import React, { useState, useEffect } from 'react';
function ClearComponent({ showExtraFeature }) {
const [count, setCount] = useState(0);
const [extraData, setExtraData] = useState('initial');
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
useEffect(() => {
if (showExtraFeature) {
console.log('Extra feature is active. Extra Data:', extraData);
}
}, [showExtraFeature, extraData]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{showExtraFeature && (
<p>Extra Data: {extraData}</p>
)}
</div>
);
}
In ClearComponent, regardless of showExtraFeature, the three useState and useEffect calls are always executed in the exact same order. The conditional logic about showExtraFeature is handled within the second useEffect or directly in the JSX, ensuring the Hook call order remains fixed.
Why are these rules so crucial?
State Consistency: The primary reason for these rules is to ensure that React can consistently manage and associate state with the correct Hooks across multiple renders. Without a fixed order, React wouldn't know which useState call corresponds to which piece of state.
Predictable Behavior: By enforcing a strict structure, React ensures that your components behave predictably. You can rely on your state and effects being applied correctly and consistently.
Optimized Performance: React often optimizes rendering by comparing the previous and current state of your components. A consistent Hook order is vital for this optimization process.
Maintainability and Debugging: Adhering to these rules makes your code more readable and easier to debug. When you know where Hooks should be, it's simpler to trace data flow and identify issues.
Tooling Support: Linters like ESLint (specifically eslint-plugin-react-hook) are designed to automatically enforce these rules, catching violations as you code. This is an invaluable tool for maintaining code quality.
Wrap Up:
React Hooks are incredibly powerful, but their strength comes from a well-defined internal mechanism. By understanding and diligently following the two rules: "Only Call Hooks at the Top Level" and "Only Call Hooks from React Functions," you'll write more robust, predictable, and maintainable React applications. Embrace these rules, and your journey with React Hooks will be much smoother and more enjoyable.
Happy coding! If you have any questions or suggestions you'd like to explore further, feel free to drop a comment below.
See you in the next blog. Please don’t forget to follow me:
Subscribe to my newsletter
Read articles from Nitin Saini directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Nitin Saini
Nitin Saini
A Full Stack Web Developer, possessing a strong command of React.js, Node.js, Express.js, MongoDB, and AWS, alongside Next.js, Redux, and modern JavaScript (ES6+)