A fundamental rule of React Hooks is that they must always be called at the top level of your component, never inside conditions, loops, or nested functions. Violating this rule leads to errors and unpredictable behavior.
The Problem
// ❌ Wrong - Hook inside condition
function BadComponent({ shouldFetch }) {
if (shouldFetch) {
const [data, setData] = useState(null); // Error!
}
// ❌ Wrong - Hook inside loop
for (let i = 0; i < 5; i++) {
useEffect(() => { /*...*/ }, []); // Error!
}
// ❌ Wrong - Hook inside callback
const handleClick = () => {
const [count, setCount] = useState(0); // Error!
};
}
Correct Solutions
1. Move Hooks to Top Level
// ✅ Correct - all hooks at top level
function GoodComponent({ shouldFetch }) {
const [data, setData] = useState(null);
const [count, setCount] = useState(0);
useEffect(() => {
if (shouldFetch) {
fetchData().then(setData);
}
}, [shouldFetch]);
const handleClick = () => {
setCount(c => c + 1);
};
}
2. For Conditional Logic
// ✅ Correct - condition inside hook
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) { // Condition inside hook is fine
fetchUser(userId).then(setUser);
}
}, [userId]);
}
3. For Dynamic Lists
// ✅ Correct - single hook manages array state
function ItemList({ initialItems }) {
const [items, setItems] = useState(initialItems);
// Don't create hooks per item - manage array state
const updateItem = useCallback((id, newValue) => {
setItems(items => items.map(item =>
item.id === id ? { ...item, ...newValue } : item
));
}, []);
}
Why This Rule Exists
React relies on the order of Hook calls to maintain state between re-renders. Changing the order (like by skipping Hooks in conditions) breaks this mechanism.
Common Mistakes
- Early returns before hooks:
function Component({ requiredProp }) {
if (!requiredProp) return null; // ❌ Before hooks
const [state] = useState(); // Error
}
- Hooks in switch cases:
switch (type) {
case 'A':
useEffect(() => {...}, []); // ❌
break;
}
- Hooks in event handlers:
const handleClick = () => {
const [value, setValue] = useState(); // ❌
};
Advanced Patterns
1. Custom Hooks for Logic Reuse
function useFetchConditionally(shouldFetch, url) {
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) {
fetch(url).then(r => r.json()).then(setData);
}
}, [shouldFetch, url]);
return data;
}
// Usage
function Component({ shouldFetch }) {
const data = useFetchConditionally(shouldFetch, '/api');
// ...render
}
2. Dynamic Hook-like Behavior
function useDynamicValues(values) {
// Track all values at top level
const [state, setState] = useState(values);
// Update only needed values
const updateValue = useCallback((key, value) => {
setState(prev => ({ ...prev, [key]: value }));
}, []);
return [state, updateValue];
}
Best Practices
- Always call hooks unconditionally at the top level
- Extract conditional logic inside hooks
- Use custom hooks for reusable conditional behavior
- Follow ESLint rules for hooks (eslint-plugin-react-hooks)
- Memoize values with
useMemoinstead of conditional hooks
Tools to Catch Errors
- ESLint Plugin:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error"
}
}
- Error Messages – React provides clear errors about hook violations
- TypeScript – Can help catch some hook misuses during development
