Proper Usage of React Hooks: Avoiding Conditional and Loop Calls
A fundamental rule of React Hooks is that they must be called unconditionally at the top level of your function component. This ensures hooks are called in the same order each time a component renders.
The Core Rule
Never call hooks inside:
- Conditional statements (
if
,switch
, etc.) - Loops (
for
,while
, etc.) - Nested functions
- Event handlers
Common Mistakes
1. Conditional Hook Calls
// ❌ Wrong - hook inside condition
function MyComponent({ shouldFetch }) {
if (shouldFetch) {
const [data, setData] = useState(null); // Bad
}
// ...
}
2. Loop-Based Hook Calls
// ❌ Wrong - hook inside loop
function List({ items }) {
for (let i = 0; i < items.length; i++) {
const [expanded, setExpanded] = useState(false); // Bad
}
// ...
}
3. Early Returns Before Hooks
// ❌ Wrong - hook after early return
function UserProfile({ user }) {
if (!user) return null;
const [profile, setProfile] = useState(null); // Bad
// ...
}
Correct Patterns
1. Unconditional Hook Calls
// ✅ Correct - hooks at top level
function MyComponent({ shouldFetch }) {
const [data, setData] = useState(null); // Good
useEffect(() => {
if (shouldFetch) {
fetchData().then(setData);
}
}, [shouldFetch]);
// ...
}
2. Dynamic State with Conditions
// ✅ Correct - condition inside hook
function ToggleComponent({ initialOn }) {
const [isOn, setIsOn] = useState(initialOn); // Good
// Conditionally apply effect
useEffect(() => {
if (isOn) {
// Setup effect
return () => {
// Cleanup
};
}
}, [isOn]);
// ...
}
3. Multiple Items with Custom Hooks
// ✅ Correct - use custom hook for each item
function List({ items }) {
return items.map(item => (
<ListItem key={item.id} item={item} />
));
}
function ListItem({ item }) {
const [expanded, setExpanded] = useState(false); // Good
// ...
}
Advanced Solutions
1. Custom Hooks for Conditional Logic
function useConditionalFetch(shouldFetch) {
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) {
fetchData().then(setData);
}
}, [shouldFetch]);
return data;
}
function MyComponent({ shouldFetch }) {
const data = useConditionalFetch(shouldFetch); // Good
// ...
}
2. Early Returns with Hooks
function UserProfile({ user }) {
// ✅ All hooks before any returns
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
if (!user) return null; // After hooks
useEffect(() => {
fetchProfile(user.id).then(p => {
setProfile(p);
setLoading(false);
});
}, [user.id]);
// ...
}
Why This Rule Exists
React relies on the order of hook calls to preserve state between renders. Conditional or loop-based hook calls would make this order unpredictable, leading to bugs.
Best Practices
- Always call hooks at the top level of your component
- Extract conditional logic into the hook itself or custom hooks
- Use eslint-plugin-react-hooks to catch violations
- For lists, either:
- Create separate components for each item
- Use a custom hook that manages a collection of state
Remember that this rule applies to all React hooks (useState, useEffect, useContext, etc.), not just the built-in ones, but also any custom hooks you create.