React Hooks have two fundamental rules that are crucial for maintaining proper component behavior:
The Two Cardinal Rules of Hooks
- Only Call Hooks at the Top Level
- Never call hooks inside loops, conditions, or nested functions
- Always call them in the same order on every render
- Only Call Hooks from React Functions
- Call hooks from React function components
- Call hooks from custom hooks
Why These Rules Exist
React relies on the order of Hook calls to preserve state between multiple useState
and useEffect
calls. Changing the order breaks this association.
Common Violations and Fixes
❌ Conditional Hook Calls (Bad)
function BadComponent({ shouldFetch }) {
if (shouldFetch) {
useEffect(() => { // ❌ Wrong - inside condition
fetchData();
}, []);
}
// ...
}
✅ Correct Conditional Execution (Good)
function GoodComponent({ shouldFetch }) {
useEffect(() => { // ✅ Top-level call
if (shouldFetch) { // Condition inside hook
fetchData();
}
}, [shouldFetch]); // Add dependency
}
❌ Loop Hook Calls (Bad)
function BadList({ items }) {
return items.map(item => {
useEffect(() => { // ❌ Wrong - inside loop
trackItem(item.id);
}, []);
return <div key={item.id}>{item.name}</div>;
});
}
✅ Correct Loop Handling (Good)
function GoodList({ items }) {
// Track all items in one effect
useEffect(() => {
items.forEach(item => {
trackItem(item.id);
});
}, [items]); // Dependency on entire array
return items.map(item => (
<div key={item.id}>{item.name}</div>
));
}
Advanced Patterns
Conditional Hooks with Same Order
function Component({ featureFlag }) {
const [alwaysState] = useState(null); // Always called
const [conditionalState] = featureFlag
? useState(initialValue) // Always called if flag exists
: [null, () => {}]; // Mock to preserve order
// ...
}
Custom Hooks for Conditional Logic
function useConditionalEffect(condition, effect, deps) {
useEffect(() => {
if (condition) {
return effect();
}
}, [condition, ...deps]);
}
// Usage
function Component({ shouldRun }) {
useConditionalEffect(shouldRun, () => {
console.log('Running effect');
}, []);
// ...
}
Why Order Matters
React tracks hooks by their call order. Consider this component:
function Example() {
const [name, setName] = useState('Mary'); // Hook 1
const [surname, setSurname] = useState('Poppins'); // Hook 2
useEffect(() => { console.log(name) }); // Hook 3
}
Between renders, React knows:
- First call is
name
state - Second call is
surname
state - Third call is an effect
If you change the order conditionally, this association breaks.
Tools to Enforce Rules
- ESLint Plugin:
eslint-plugin-react-hooks
- Catches violations during development
- Part of Create React App by default
- React DevTools
- Shows hook call order in component tree
Special Cases
Dynamic Imports with Hooks
function Component() {
const [module, setModule] = useState(null);
useEffect(() => {
import('./dynamicModule').then(setModule);
}, []);
if (!module) return <Loading />;
// Now safe to use module's hooks
return <module.ExportedComponent />;
}
Key Takeaways
- Never call hooks conditionally or in loops
- Move conditions inside hooks instead
- Maintain consistent hook order between renders
- Use the ESLint plugin to catch mistakes
- For complex logic, extract to custom hooks