July 3, 2026
Rules of Hooks - React Development Guide
Comprehensive guide to React Hooks rules, violations, and solutions
Comprehensive guide to React Hooks rules, violations, and solutions
Table of Contents
- Overview
- The Two Rules of Hooks
- Common Violations & Solutions
- Advanced Patterns
- Prevention Guidelines
- Debugging Hook Issues
- Additional Hook Rules & Best Practices
- References
Overview
This document provides a comprehensive guide to the Rules of Hooks, documenting common violations encountered during development, their solutions, and prevention strategies.
The Two Rules of Hooks
Rule 1: Only Call Hooks at the Top Level
- ✅ Do: Call hooks at the top level of React functions
- ❌ Don’t: Call hooks inside loops, conditions, or nested functions
Rule 2: Only Call Hooks from React Functions
- ✅ Do: Call hooks from React function components or custom hooks
- ❌ Don’t: Call hooks from regular JavaScript functions, class components, or event handlers
Case 1: Hook Order Violation
Problem Description
Error Message:
Warning: React has detected a change in the order of Hooks called by Cell.
This will lead to bugs and errors if not fixed. For more information,
read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks
Context: This error occurred in table column rendering where cell components were not being properly rendered as React components.
Root Cause Analysis
The warning occurred because cell components were being called as functions instead of being rendered as React components in the tableColumns function. This violated the Rules of Hooks because:
- Function calls vs Component rendering: Components were called directly like
components.DurationWIPCell({ record })instead of being rendered as JSX - Hook order inconsistency: When components are called as functions, React can’t properly track hook calls, leading to inconsistent hook ordering
- Missing React context: Function calls don’t provide the proper React rendering context needed for hooks
Solution Implementation
1. Proper JSX Rendering
Changed from function calls to proper JSX rendering:
// ❌ Before (incorrect)
columns.render = (_text, record) => components.DurationWIPCell({ record });
// ✅ After (correct)
columns.render = (_text, record) => (
<components.DurationWIPCell record={record} />
);
Result: Cell components now render correctly as React components, maintaining consistent hook call order and eliminating the warning.
Case 2: Conditional Hook Calls
Problem Description
Error Message:
Warning: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order every time the component renders.
Context: This error occurs when hooks are called inside conditional statements, causing React to lose track of hook order.
Root Cause Analysis
Conditional hook calls violate Rule 1 because:
- Inconsistent hook order: Different code paths lead to different hook call sequences
- React’s internal tracking: React relies on consistent hook call order to maintain state
- Rendering unpredictability: Conditional hooks make component behavior unpredictable
Solution Implementation
❌ Before (incorrect):
function MyComponent({ shouldUseEffect }) {
if (shouldUseEffect) {
const [state, setState] = useState(0); // ❌ Conditional hook
}
return <div>Content</div>;
}
✅ After (correct):
function MyComponent({ shouldUseEffect }) {
const [state, setState] = useState(0); // ✅ Always called
useEffect(() => {
if (shouldUseEffect) {
// Conditional logic inside hook
setState((prev) => prev + 1);
}
}, [shouldUseEffect]);
return <div>Content</div>;
}
Case 3: Hook Calls in Loops
Problem Description
Error Message:
Warning: React Hook "useState" is called in a loop. React Hooks must be called in the exact same order every time the component renders.
Root Cause Analysis
Loop-based hook calls violate Rule 1 because:
- Variable hook count: Number of hook calls depends on loop iterations
- Unpredictable order: Hook order changes based on data length
- State management issues: React can’t properly track state for dynamic hook counts
Solution Implementation
❌ Before (incorrect):
function ItemList({ items }) {
return (
<div>
{items.map((item) => {
const [isSelected, setIsSelected] = useState(false); // ❌ Hook in loop
return <Item key={item.id} item={item} />;
})}
</div>
);
}
✅ After (correct):
function ItemList({ items }) {
const [selectedItems, setSelectedItems] = useState(new Set());
const toggleItem = (itemId) => {
setSelectedItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
newSet.add(itemId);
}
return newSet;
});
};
return (
<div>
{items.map((item) => (
<Item
key={item.id}
item={item}
isSelected={selectedItems.has(item.id)}
onToggle={() => toggleItem(item.id)}
/>
))}
</div>
);
}
Case 4: Hook Calls in Nested Functions
Problem Description
Error Message:
Warning: React Hook "useEffect" is called in a function that is neither a React function component nor a custom React Hook function.
Root Cause Analysis
Nested function hook calls violate Rule 2 because:
- Wrong execution context: Hooks need React’s component context
- Missing lifecycle tracking: Nested functions don’t participate in React’s lifecycle
- State management failure: React can’t properly manage hook state
Solution Implementation
❌ Before (incorrect):
function MyComponent() {
const handleClick = () => {
useEffect(() => {
// ❌ Hook in nested function
console.log("Effect in handler");
}, []);
};
return <button onClick={handleClick}>Click me</button>;
}
✅ After (correct):
function MyComponent() {
const [clickCount, setClickCount] = useState(0);
useEffect(() => {
console.log("Effect runs on click count change");
}, [clickCount]);
const handleClick = () => {
setClickCount((prev) => prev + 1);
};
return <button onClick={handleClick}>Click me</button>;
}
Case 5: Hook Calls in Event Handlers
Problem Description
Error Message:
Warning: React Hook "useState" is called in a function that is neither a React function component nor a custom React Hook function.
Root Cause Analysis
Event handler hook calls violate Rule 2 because:
- Event context: Event handlers run outside React’s component lifecycle
- Timing issues: Hooks need to be called during render phase
- State synchronization: React can’t properly synchronize state updates
Solution Implementation
❌ Before (incorrect):
function MyComponent() {
const handleSubmit = (event) => {
event.preventDefault();
const [data, setData] = useState(null); // ❌ Hook in event handler
// Process form data
};
return <form onSubmit={handleSubmit}>...</form>;
}
✅ After (correct):
function MyComponent() {
const [data, setData] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsSubmitting(true);
try {
const result = await processFormData();
setData(result);
} finally {
setIsSubmitting(false);
}
};
return <form onSubmit={handleSubmit}>...</form>;
}
Advanced Patterns
Custom Hooks for Complex Logic
✅ Extract complex logic into custom hooks:
// Custom hook
function useFormValidation(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isValid, setIsValid] = useState(false);
useEffect(() => {
const hasErrors = Object.values(errors).some((error) => error);
setIsValid(!hasErrors);
}, [errors]);
const validate = (name, value) => {
// Validation logic
};
const handleChange = (name, value) => {
setValues((prev) => ({ ...prev, [name]: value }));
validate(name, value);
};
return { values, errors, isValid, handleChange };
}
// Component usage
function MyForm() {
const { values, errors, isValid, handleChange } = useFormValidation({
email: "",
password: "",
});
return (
<form>
<input
value={values.email}
onChange={(e) => handleChange("email", e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
</form>
);
}
Conditional Logic with Hooks
✅ Use hooks with conditional logic inside:
function ConditionalComponent({ shouldFetch, userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (shouldFetch && userId) {
setLoading(true);
fetchUserData(userId)
.then(setData)
.finally(() => setLoading(false));
}
}, [shouldFetch, userId]);
if (loading) return <div>Loading...</div>;
if (!data) return <div>No data</div>;
return <div>{data.name}</div>;
}
Prevention Guidelines
✅ Do’s
- Always call hooks at the top level of React function components
- Use custom hooks for complex logic and state management
- Keep hook calls consistent - same order every render
- Use conditional logic inside hooks rather than conditional hook calls
- Extract state logic into custom hooks for reusability
- Use proper dependency arrays in useEffect and other hooks
- Always render components as JSX:
<Component /> - Import React when using JSX in files
❌ Don’ts
- Don’t call hooks conditionally - no if statements around hooks
- Don’t call hooks in loops - use state management patterns instead
- Don’t call hooks in nested functions - keep them at component level
- Don’t call hooks in event handlers - use state updates instead
- Don’t call components as functions:
Component(props) - Don’t mix function calls and JSX rendering for the same components
- Don’t forget dependency arrays in useEffect
- Don’t call hooks from regular JavaScript functions
Debugging Hook Issues
Common Error Messages
- “React Hook is called conditionally”
- Cause: Hook called inside if statement
- Fix: Move conditional logic inside the hook
- “React Hook is called in a loop”
- Cause: Hook called inside map/forEach
- Fix: Use state management patterns (Set, Map, or object)
- “React Hook is called in a function that is neither a React function component nor a custom React Hook function”
- Cause: Hook called in event handler or nested function
- Fix: Move hook to component level, use state updates in handlers
- “React has detected a change in the order of Hooks”
- Cause: Inconsistent hook call order between renders
- Fix: Ensure hooks are always called in the same order
Debugging Tools
- React Developer Tools: Use the hooks tab to inspect hook state
- ESLint Plugin: Install
eslint-plugin-react-hooksfor automatic detection - Console Warnings: React provides detailed warnings in development mode
Best Practices for Debugging
- Use consistent naming for hooks and state variables
- Add console.log statements to track hook execution order
- Use React DevTools to inspect component state
- Test with different data to ensure hook order consistency
- Use custom hooks to isolate complex logic
Additional Hook Rules & Best Practices
Rule 3: Always Use the Same Hook Names
✅ Do:
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Always use the same variable names
}
❌ Don’t:
function MyComponent() {
const [count, setCount] = useState(0);
const [userName, setUserName] = useState(''); // Inconsistent naming
}
Rule 4: Don’t Mutate Hook Dependencies
❌ Don’t mutate dependencies:
function MyComponent({ items }) {
useEffect(() => {
items.push('new item'); // ❌ Mutating props
console.log(items);
}, [items]);
}
✅ Create new references:
function MyComponent({ items }) {
const [localItems, setLocalItems] = useState(items);
useEffect(() => {
setLocalItems(prev => [...prev, 'new item']); // ✅ New reference
}, [items]);
}
Rule 5: Use useCallback and useMemo Wisely
✅ Optimize expensive operations:
function ExpensiveComponent({ items, onItemClick }) {
const memoizedItems = useMemo(() => {
return items.map(item => ({
...item,
processed: expensiveCalculation(item)
}));
}, [items]);
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
{memoizedItems.map(item => (
<Item key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}
Rule 6: Clean Up Effects Properly
✅ Always clean up subscriptions:
function DataComponent({ userId }) {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
const result = await api.getUser(userId);
if (!cancelled) {
setData(result);
}
} catch (error) {
if (!cancelled) {
console.error('Failed to fetch data:', error);
}
}
};
fetchData();
return () => {
cancelled = true; // ✅ Cleanup
};
}, [userId]);
return <div>{data?.name}</div>;
}
Rule 7: Avoid Stale Closures
❌ Stale closure problem:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // ❌ Stale closure
}, 1000);
return () => clearInterval(interval);
}, []); // ❌ Missing dependency
}
✅ Fix with functional updates:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // ✅ Functional update
}, 1000);
return () => clearInterval(interval);
}, []); // ✅ No dependencies needed
}
Rule 8: Use Custom Hooks for Reusable Logic
✅ Extract reusable logic:
// Custom hook
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = useCallback((value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key]);
return [storedValue, setValue];
}
// Usage
function MyComponent() {
const [name, setName] = useLocalStorage('name', '');
return (
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
Rule 9: Handle Async Operations Correctly
✅ Proper async handling:
function AsyncComponent({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
if (userId) {
fetchUser();
}
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return <div>{user.name}</div>;
}
Rule 10: Use Proper Dependency Arrays
✅ Correct dependency arrays:
function Component({ userId, filters }) {
const [data, setData] = useState(null);
// ✅ Include all dependencies
useEffect(() => {
fetchData(userId, filters).then(setData);
}, [userId, filters]); // ✅ All dependencies included
// ✅ Use useCallback for stable references
const handleSubmit = useCallback((formData) => {
submitForm(userId, formData);
}, [userId]); // ✅ userId dependency
return <form onSubmit={handleSubmit}>...</form>;
}
References
- React Rules of Hooks - Official React documentation
- React Hooks FAQ - Common questions and answers
- React Hook Order - Detailed explanation of hook order requirements
- ESLint Plugin React Hooks - Automatic hook rule detection
- React DevTools - Browser extension for debugging
- Custom Hooks Guide - Creating and using custom hooks
- React Hook Form - Performant forms with hooks
- SWR - Data fetching with hooks
- React Query - Powerful data synchronization for React