Rules of Hooks: Best Practices and ESLint Enforcement
React hooks only work correctly if you follow two fundamental rules: call them at the top level (never in loops or conditions), and only from React components or other custom hooks. The eslint-plugin-react-hooks ESLint rule automatically enforces these rules and checks your dependency arrays, catching bugs before runtime. Following these rules and best practices ensures your custom hooks are reusable, maintainable, and predictable.
Key Takeaways
- Rule 1: Call hooks at top level — never inside loops, conditions, or nested functions; React relies on consistent call order
- Rule 2: Call hooks from React functions only — only from functional components or custom hooks; never from regular JavaScript functions
- The
eslint-plugin-react-hooksESLint plugin automatically enforces both rules and checks the dependency arrays ofuseEffectanduseCallback - Custom hook best practices: use the
useprefix, keep single purpose, return values intuitively, and memoize expensive operations - Breaking the Rules of Hooks causes subtle state corruption and bugs that are hard to trace
Why Are the Rules of Hooks Essential?
How React Tracks Hook State
React relies on a simple mechanism to track which state belongs to which hook call. When you use useState multiple times in a component, React doesn't store state in a map keyed by variable name. Instead, it relies on the call order of hooks. The first useState call gets the first piece of state, the second useState call gets the second piece, and so on:
function MyComponent() {
const [name, setName] = useState('Alice'); // 1st state
const [age, setAge] = useState(30); // 2nd state
const [email, setEmail] = useState('[email protected]'); // 3rd state
}
React internally maintains a list: [state1, state2, state3]. If you conditionally call useState, that order breaks:
function Broken({ showAge }) {
const [name, setName] = useState('Alice'); // Always 1st
if (showAge) {
const [age, setAge] = useState(30); // Sometimes 2nd, sometimes skipped!
}
const [email, setEmail] = useState('[email protected]'); // 2nd or 3rd?
}
When showAge is false, the second and third states shift. React gets confused about which state belongs to which hook, and your component breaks silently—state appears to be lost or swapped between renders.
What Is Rule 1: Only Call Hooks at the Top Level?
Correct: Hooks at Top Level
Always call hooks directly at the function body's top level, in the same order every time:
function UserProfile({ userId }) {
const [user, setUser] = useState(null); // Always 1st
const [posts, setPosts] = useState([]); // Always 2nd
useEffect(() => {
// Fetch user...
}, [userId]); // Always 1st effect
useEffect(() => {
// Fetch posts...
}, [userId]); // Always 2nd effect
return <div>{user?.name}</div>;
}
Incorrect: Hooks in Conditions
Do not call hooks inside conditionals:
function BadComponent({ isLoggedIn }) {
if (isLoggedIn) {
const [profile, setProfile] = useState(null); // WRONG!
}
// When isLoggedIn is false, this hook is skipped.
// On next render with isLoggedIn true, React's hook indices are wrong.
}
Incorrect: Hooks in Loops
Do not call hooks inside loops:
function BadList({ items }) {
items.forEach(item => {
const [count, setCount] = useState(0); // WRONG!
});
// The number of hooks changes with the array length!
}
The Correct Pattern: Move Logic
If you need conditional logic with hooks, move the logic inside the hook callback:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) { // Conditional logic inside the hook
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}
}, [userId]);
return user ? <div>{user.name}</div> : <p>Loading...</p>;
}
What Is Rule 2: Only Call Hooks from React Functions?
Correct: Hooks from Components
Call hooks only from functional React components:
function MyComponent() {
const [count, setCount] = useState(0); // OK
return <div>Count: {count}</div>;
}
Correct: Hooks from Custom Hooks
Call hooks from other custom hooks (which themselves are called from components):
function useCustom() {
const [data, setData] = useState(null); // OK
useEffect(() => {
// ...
}, []);
return data;
}
function MyComponent() {
const data = useCustom(); // Calling custom hook from component
return <div>{data}</div>;
}
Incorrect: Hooks from Regular Functions
Do not call hooks from regular JavaScript functions, even if they are called from a component:
function MyComponent() {
function handleClick() {
const [value, setValue] = useState(0); // WRONG!
}
// This is a regular function, not a hook or component.
return <button onClick={handleClick}>Click</button>;
}
Incorrect: Hooks from Class Components
Do not call hooks from class components (hooks are for functional components only):
class MyClass extends React.Component {
render() {
const [count, setCount] = useState(0); // WRONG!
return <div>{count}</div>;
}
}
Best Practices for Writing Custom Hooks
Naming Convention: Start with "use"
Always name your custom hook with a use prefix:
function useFetch(url) {
const [data, setData] = useState(null);
// ...
return data;
}
function useLocalStorage(key, initialValue) {
// ...
}
function useWindowSize() {
const [size, setSize] = useState(null);
// ...
return size;
}
The use prefix is a convention that tells other developers (and ESLint) that this is a hook. React enforces the Rules of Hooks only on functions starting with use.
Keep Hooks Focused: Single Responsibility
Each custom hook should have one clear purpose. If your hook is doing too much, break it into smaller hooks:
// BAD: Too many responsibilities
function useUser(userId) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [comments, setComments] = useState([]);
// Fetches user, posts, and comments — too broad
// ...
return { user, posts, comments };
}
// GOOD: Focused, single purpose
function useFetchUser(userId) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
return user;
}
function useFetchUserPosts(userId) {
const [posts, setPosts] = useState([]);
// ...
return posts;
}
Return Values: Array vs. Object
-
Return an array for a single value (like
useState):function useToggle(initialValue) {
const [value, setValue] = useState(initialValue);
return [value, () => setValue(!value)];
} -
Return an object for multiple related values (more flexible):
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
// ...
return { data, error, loading };
}
Memoization: useCallback and useMemo
If your hook returns functions or objects, memoize them to prevent unnecessary re-renders in components that depend on reference equality:
function useFetch(url) {
const [data, setData] = useState(null);
// Memoize the fetch function
const refetch = useCallback(async () => {
const res = await fetch(url);
const json = await res.json();
setData(json);
}, [url]);
useEffect(() => {
refetch();
}, [refetch]);
return { data, refetch };
}
How to Enforce the Rules with eslint-plugin-react-hooks
Installation
If you are using Create React App, the plugin is already configured. Otherwise, install it:
npm install --save-dev eslint-plugin-react-hooks
Configuration
Add the plugin to your .eslintrc.json or .eslintrc.js:
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
The Two Rules
-
react-hooks/rules-of-hooks— Enforces that hooks are called at the top level and only from React functions. ESLint will error if you:- Call a hook inside a conditional or loop
- Call a hook from a non-hook function
- Call a hook from a class component
-
react-hooks/exhaustive-deps— Checks that dependency arrays inuseEffect,useCallback, anduseMemoinclude all necessary dependencies. ESLint will warn if you omit a variable that the hook uses.
Example: ESLint Catches Violations
function MyComponent() {
const [count, setCount] = useState(0);
if (count > 5) {
const [name, setName] = useState('Alice'); // ESLint error: hook in conditional
}
useEffect(() => {
console.log(count);
}, []); // ESLint warning: 'count' missing from dependency array
}
Frequently Asked Questions
Can I call a hook conditionally if I check before calling it?
No. This is still wrong:
function MyComponent({ shouldUseState }) {
if (shouldUseState) {
const [value, setValue] = useState(0); // Still WRONG, even with the check
}
}
The rule is absolute: never conditionally call hooks. Move the conditional logic inside the hook.
What if I want to skip a hook based on props?
Do not skip the hook call. Instead, pass the conditional logic as a parameter or use it inside the hook:
function useData(shouldFetch, url) {
const [data, setData] = useState(null);
useEffect(() => {
if (shouldFetch) { // Logic inside the hook
fetch(url).then(r => r.json()).then(setData);
}
}, [shouldFetch, url]);
return data;
}
Can I disable the ESLint rule if I know better?
Don't. If you think the rule is wrong, you are almost certainly missing something. The rule is battle-tested. Only disable it with a detailed comment explaining a very rare edge case, and have it reviewed by a senior developer.
How do I update dependencies without triggering infinite loops?
Use useCallback and useMemo to stabilize dependencies:
function MyComponent({ userId }) {
const [data, setData] = useState(null);
const fetchData = useCallback(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setData);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // Stable because useCallback controls when it changes
}
Further Reading
- React Official Docs: Rules of Hooks
- eslint-plugin-react-hooks on npm
- YouTube: How React Hooks Really Work (Fun Fun Function)
Last updated: June 2, 2026 by Dr. Alex Turner