There are two main rules for using hooks in React:
✅ Rule 1: Only call hooks at the top level
✅ Rule 2: Only call hooks in React functional components or custom hooks
Key rule: Maintain hook call order and don’t call them conditionally! 🎯
Imagine hooks are like ingredients in a recipe. If you add them in a different order each time or skip some, the dish will be unpredictable! 🍳
React relies on hook call order for proper functioning:
// ✅ Correct - hooks at top level
function MyComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
// Effect
}, []);
return (
<div>
<p>{count} - {name}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
// ❌ Incorrect - hooks in conditions
function BadComponent({ condition }) {
if (condition) {
const [state, setState] = useState(0); // ERROR!
}
return <div>Component</div>;
}
React uses an internal list to track hooks:
// How React sees hooks:
// First render:
// 1. useState(0) -> [0, setter]
// 2. useState('') -> ['', setter]
// 3. useEffect(() => {...}) -> undefined
// Second render:
// 1. useState(0) -> [0, setter] // Same hook!
// 2. useState('') -> ['', setter] // Same hook!
// 3. useEffect(() => {...}) -> undefined // Same hook!
// ❌ Incorrect - conditional call
function BadComponent({ isLoggedIn }) {
const [user, setUser] = useState(null);
if (isLoggedIn) {
// This breaks hook order!
const [token, setToken] = useState(''); // ERROR!
}
return <div>Component</div>;
}
// ✅ Correct - always call hooks
function GoodComponent({ isLoggedIn }) {
const [user, setUser] = useState(null);
const [token, setToken] = useState(''); // Always called
// Use conditional logic inside hooks
useEffect(() => {
if (isLoggedIn) {
// Logic for authenticated users
}
}, [isLoggedIn]);
return <div>Component</div>;
}
// ❌ Incorrect - hooks in loops
function BadComponent({ items }) {
const [state, setState] = useState(null);
items.forEach(item => {
// Each render might have different number of items!
const [itemState, setItemState] = useState(item); // ERROR!
});
return <div>Component</div>;
}
// ✅ Correct - hooks outside loops
function GoodComponent({ items }) {
const [state, setState] = useState(null);
// Use array of states
const [itemStates, setItemStates] = useState(
items.map(item => item)
);
return (
<div>
{items.map((item, index) => (
<div key={index}>
<input
value={itemStates[index]}
onChange={e => {
const newStates = [...itemStates];
newStates[index] = e.target.value;
setItemStates(newStates);
}}
/>
</div>
))}
</div>
);
}
// ❌ Incorrect - hooks in regular functions
function regularFunction() {
const [state, setState] = useState(0); // ERROR!
return state;
}
// ✅ Correct - hooks in components or custom hooks
function Component() {
const value = useCustomHook(); // Correct
return <div>{value}</div>;
}
function useCustomHook() {
const [state, setState] = useState(0); // Correct
return state;
}
// ✅ Custom hooks start with "use"
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// ❌ Incorrect naming
function counterHook() { // Should start with "use"
const [count, setCount] = useState(0);
return [count, setCount];
}
// ✅ Proper usage
function UserProfile() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>Counter: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
// ✅ Custom hook for API
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// ❌ Too many separate states
function BadForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
// ... more states
return (
<form>
{/* Form fields */}
</form>
);
}
// ✅ Grouping in object
function GoodForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
confirmPassword: ''
});
const updateField = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
value={formData.name}
onChange={e => updateField('name', e.target.value)}
/>
<input
value={formData.email}
onChange={e => updateField('email', e.target.value)}
/>
{/* Other fields */}
</form>
);
}
// ❌ Suboptimal useEffect
function BadComponent({ userId, postId }) {
const [user, setUser] = useState(null);
const [post, setPost] = useState(null);
// Runs when any prop changes
useEffect(() => {
fetchUser(userId).then(setUser);
fetchPost(postId).then(setPost);
}, [userId, postId]); // Both props in dependencies
return <div>Component</div>;
}
// ✅ Separated effects
function GoodComponent({ userId, postId }) {
const [user, setUser] = useState(null);
const [post, setPost] = useState(null);
// Separate effects for different data
useEffect(() => {
if (userId) {
fetchUser(userId).then(setUser);
}
}, [userId]); // Only userId
useEffect(() => {
if (postId) {
fetchPost(postId).then(setPost);
}
}, [postId]); // Only postId
return <div>Component</div>;
}
// ✅ Simple state
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Counter: {count}
</button>
);
}
// ✅ Subscriptions and cleanup
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup subscription
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>
Window size: {size.width} x {size.height}
</div>
);
}
// ✅ Reusable 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 = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'ru');
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value="ru">Русский</option>
<option value="en">English</option>
</select>
</div>
);
}
Hook rules are like traffic rules for React! 🚦
Never break these rules:
Practical tips:
Following hook rules is the key to stable and predictable React applications! 💪
Want more useful React articles? Subscribe to EasyAdvice, bookmark the site and level up every day! 🚀