The Rules of Hooks: Avoiding Conditional and Looped Calls
React Hooks have strict rules about where and how they can be called. Violating these rules leads to bugs and unpredictable behavior. Here’s what you need to know:
The Fundamental Rules
- Only Call Hooks at the Top Level
- Never inside loops, conditions, or nested functions
- Always in the same order each render
- Only Call Hooks from React Functions
- Inside functional components
- Inside other custom hooks
The Problems
1. Conditional Hook Calls (Wrong)
function MyComponent({ shouldFetch }) {
if (shouldFetch) {
// ❌ Dangerous - hook called conditionally
const [data, setData] = useState(null);
}
// ...
}
2. Hooks in Loops (Wrong)
function MyList({ items }) {
return items.map(item => {
// ❌ Dangerous - hook called in loop
const [isSelected, setIsSelected] = useState(false);
return <div key={item.id}>{item.name}</div>;
});
}
3. Hooks in Callbacks (Wrong)
function MyComponent() {
const handleClick = () => {
// ❌ Dangerous - hook in nested function
const [count, setCount] = useState(0);
};
// ...
}
Correct Patterns
1. Move Hooks to Top Level
function MyComponent({ shouldFetch }) {
// ✅ Correct - unconditional top-level call
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) {
fetchData().then(setData);
}
}, [shouldFetch]);
}
2. For Conditional Logic – Use Flags
function MyComponent({ isAdmin }) {
// ✅ All hooks called unconditionally
const [userData, setUserData] = useState(null);
const [adminData, setAdminData] = useState(null);
useEffect(() => {
isAdmin ? loadAdminData() : loadUserData();
}, [isAdmin]);
}
3. For Lists – Lift State Up
function ParentComponent({ items }) {
// ✅ Manage state at parent level
const [selectedItems, setSelectedItems] = useState([]);
return items.map(item => (
<Child
key={item.id}
item={item}
isSelected={selectedItems.includes(item.id)}
onSelect={() => toggleSelection(item.id)}
/>
));
}
Why These Rules Exist
React relies on:
- Consistent Hook Order to maintain state between renders
- Predictable Component Lifecycle for proper updates
- Stable State References across re-renders
Advanced Solutions
Custom Hooks for Complex Logic
function useUserData(isAdmin) {
const [data, setData] = useState(null);
useEffect(() => {
const loadData = isAdmin ? loadAdminData : loadUserData;
loadData().then(setData);
}, [isAdmin]);
return data;
}
function MyComponent({ isAdmin }) {
// ✅ Clean usage with custom hook
const userData = useUserData(isAdmin);
// ...
}
Dynamic Hook-like Behavior
function MyComponent({ features }) {
// Initialize all possible states
const [hasA, setHasA] = useState(false);
const [hasB, setHasB] = useState(false);
// Then conditionally use them
return (
<div>
{features.includes('A') && <FeatureA enabled={hasA} />}
{features.includes('B') && <FeatureB enabled={hasB} />}
</div>
);
}
Debugging Hook Errors
React will throw errors if you violate Hook rules. Common messages:
- “React Hook ‘useState’ is called conditionally”
- “Hooks must be called in the exact same order”
Use the ESLint plugin for Hooks to catch these issues early.
Key Takeaways
- Always call Hooks at the top level of your component
- Never call Hooks conditionally – use conditional logic inside effects instead
- For lists, manage state at a higher level
- Use custom Hooks to encapsulate complex logic
- Follow the linter warnings – they’re there to help
Remember: These rules exist to ensure your components work predictably. While they might seem restrictive at first, they lead to more maintainable code in the long run.