Home JavaScript React Hooks Rules and Best Practices
Intermediate 6 min · July 13, 2026

React Hooks Rules and Best Practices

Rules of hooks, dependency arrays, eslint-plugin-react-hooks, and common mistakes..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,018
articles · all by Naren
Before you start⏱ 36 min read
  • Node.js 18+, React 18+, basic understanding of function components and state, familiarity with JavaScript ES6+ (arrow functions, destructuring, spread operator).
Quick Answer

React Hooks Rules and Best Practices: A core React concept for building modern user interfaces. It helps you structure your components efficiently and handle data flow predictably.

✦ Definition~90s read
What is React Hooks Rules and?

React Hooks Rules and Best Practices is a fundamental concept in React development. It refers to the patterns, APIs, and best practices that React provides for building user interfaces. Understanding this concept is essential for writing clean, efficient, and maintainable React code. This tutorial covers everything from basic usage to advanced patterns with real-world examples.

React is a JavaScript library for building user interfaces.
Plain-English First

React is a JavaScript library for building user interfaces. This article covers hooks rules — a key concept for building modern web applications.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

A comprehensive guide to react hooks rules and best practices with production examples and best practices.

Why React Hooks Rules Exist

React Hooks are functions that let you use state and lifecycle features from function components. They are governed by two core rules: only call Hooks at the top level of your component, and only call Hooks from React functions. These rules are not arbitrary—they ensure that the order of Hook calls is consistent across renders. React relies on this order to correctly associate state and effects with each Hook. Violating these rules leads to subtle bugs like stale state, infinite loops, or crashes. For example, calling a Hook inside a condition can shift the call order, causing React to misassign state. Understanding why these rules exist helps you write predictable components and debug issues faster.

BadHookOrder.jsxJSX
1
2
3
4
5
6
7
8
9
import { useState } from 'react';

export default function BadComponent({ flag }) {
  if (flag) {
    const [count, setCount] = useState(0); // ❌ Conditional hook
  }
  const [name, setName] = useState('');
  return <div>{name}</div>;
}
Output
Error: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.
Try it live
⚠ Don't Break the Hook Order
Conditional Hooks are the #1 cause of 'Rendered fewer hooks than expected' errors. Always move conditions inside the Hook or use early returns before any Hook calls.
📊 Production Insight
In production, a conditional hook can cause a component to crash silently on one render path while working on another, making it extremely hard to reproduce and debug.
🎯 Key Takeaway
Hook call order must be identical across every render to maintain state consistency.
react-hooks-rules THECODEFORGE.IO React Hooks Rules Flow Step-by-step guide to correct hooks usage Top-Level Only Call hooks at the top level of your component Only from React Functions Call hooks only from React function components or custom hooks Dependency Array List all dependencies for useEffect, useMemo, useCallback Immutable Updates Use functional updates for state derived from previous state Cleanup Effects Return cleanup function from useEffect to avoid memory leaks Extract Custom Hooks Reuse logic by creating custom hooks ⚠ Calling hooks inside conditions or loops breaks the rules Always call hooks unconditionally at the top level THECODEFORGE.IO
thecodeforge.io
React Hooks Rules

The Rules of Hooks: Top-Level Only

The first rule of Hooks is to call them at the top level of your React function, not inside loops, conditions, or nested functions. This ensures that Hooks are called in the same order each time a component renders. React uses a linked list of Hook nodes internally; the order of calls determines which state belongs to which Hook. If you break this order, React's internal state mapping becomes corrupted. For example, moving a useState inside a loop will cause the number of Hooks to change between renders, leading to errors. Always place all Hook calls at the top of your component, before any early returns.

TopLevelHooks.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react';

export default function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  return <h1>{user.name}</h1>;
}
Output
Renders loading state, then user name after fetch completes.
Try it live
💡Early Returns Are Fine—After Hooks
You can have early returns, but only after all Hook calls. Place your Hooks first, then conditional rendering logic.
📊 Production Insight
A common production bug is adding a guard clause before Hooks, which causes 'Rendered fewer hooks than expected' errors when the guard triggers.
🎯 Key Takeaway
Always call Hooks at the top level of your component, before any early returns.

Only Call Hooks from React Functions

The second rule restricts Hook calls to React function components and custom Hooks. You cannot call Hooks from regular JavaScript functions, class components, or event handlers. This ensures that Hooks are only used within the React functional paradigm, where the framework can manage their lifecycle. Custom Hooks are the only exception—they are JavaScript functions whose name starts with 'use' and that call other Hooks. This rule prevents misuse like calling useState inside a utility function, which would break the component's state management. Always ensure your custom Hooks follow the naming convention and only call Hooks at their top level.

CustomHook.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react';

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return width;
}

export default function App() {
  const width = useWindowWidth();
  return <p>Window width: {width}px</p>;
}
Output
Renders current window width and updates on resize.
Try it live
🔥Custom Hooks Must Start with 'use'
React's lint rules rely on the 'use' prefix to detect Hook calls inside custom Hooks. Without it, you'll get lint errors and potential runtime issues.
📊 Production Insight
A developer once called useState inside a debounced utility function, causing state updates to be lost. The fix was to move the Hook into a custom Hook and call it from the component.
🎯 Key Takeaway
Hooks can only be called from React function components or custom Hooks that start with 'use'.
react-hooks-rules THECODEFORGE.IO React Hooks Architecture Layered structure of hooks in a React application Component Layer Function Component | JSX Rendering State Management useState | useReducer Side Effects useEffect | useLayoutEffect Memoization useMemo | useCallback Custom Hooks useFetch | useForm | useAuth Rules Enforcement Top-Level Calls | React Functions Only | Dependency Array THECODEFORGE.IO
thecodeforge.io
React Hooks Rules

The Dependency Array: Your Safety Net

useEffect, useMemo, and useCallback accept a dependency array that tells React when to re-run the effect or recompute the value. Omitting dependencies or including stale values leads to bugs like infinite loops or missing updates. The rule is simple: include every reactive value (props, state, derived values) used inside the effect or memo. The exhaustive-deps ESLint rule enforces this. However, you may intentionally omit a dependency if you want to run an effect only on mount (empty array). Be cautious: stale closures are a common source of bugs. Use the functional update form of setState when the new state depends on the old state to avoid unnecessary dependencies.

DependencyArray.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useState, useEffect } from 'react';

export default function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) return;
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => setResults(data));
  }, [query]); // ✅ query is included

  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}
Output
Fetches search results whenever query changes.
Try it live
⚠ Missing Dependencies Cause Stale Closures
If you omit a dependency, your effect will capture the initial value and never update. Always run the linter and fix warnings.
📊 Production Insight
A missing dependency in a useEffect caused a dashboard to show stale data for hours. The fix was adding the missing prop to the dependency array, which triggered a re-fetch on prop change.
🎯 Key Takeaway
Include every reactive value used inside useEffect, useMemo, or useCallback in the dependency array.

useState: Immutable Updates and Functional Updates

State in React must be updated immutably. For objects and arrays, always create a new reference instead of mutating the existing state. Use the spread operator or methods like map/filter that return new arrays. When the new state depends on the previous state, use the functional update form: setCount(prev => prev + 1). This avoids stale state issues, especially in concurrent mode or when multiple state updates are batched. Functional updates are also safer inside useEffect dependencies because they don't require the previous state to be listed as a dependency.

ImmutableState.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState } from 'react';

export default function TodoList() {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
  };

  const toggleTodo = (id) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.done ? <s>{todo.text}</s> : todo.text}
        </li>
      ))}
    </ul>
  );
}
Output
Renders a list of todos; clicking toggles done state.
Try it live
💡Always Use Functional Updates for Derived State
When your new state depends on the old state, use the functional form. It's more predictable and avoids dependency issues in effects.
📊 Production Insight
A mutation of a nested object in state caused a component to not re-render, leading to a UI that appeared stuck. The fix was spreading the object at every level of nesting.
🎯 Key Takeaway
Never mutate state directly; use functional updates when the new state depends on the previous state.

useEffect: Side Effects and Cleanup

useEffect is the primary hook for side effects: data fetching, subscriptions, timers, and manual DOM manipulations. Always return a cleanup function to avoid memory leaks and race conditions. For example, when fetching data, use an abort controller or ignore stale responses. Cleanup runs on unmount and before re-running the effect. This is crucial for subscriptions like WebSockets or event listeners. Also, avoid async functions directly inside useEffect; instead, define an async function inside and call it.

EffectCleanup.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useState, useEffect } from 'react';

export default function UserStatus({ userId }) {
  const [status, setStatus] = useState('offline');

  useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    fetch(`/api/users/${userId}/status`, { signal })
      .then(res => res.json())
      .then(data => setStatus(data.status))
      .catch(err => {
        if (err.name !== 'AbortError') console.error(err);
      });

    return () => controller.abort();
  }, [userId]);

  return <p>Status: {status}</p>;
}
Output
Fetches user status on mount and userId change; aborts previous request on cleanup.
Try it live
🔥Cleanup Prevents Race Conditions
Without cleanup, a fast component remount could resolve an old fetch after a new one, causing stale state. Always abort or ignore stale responses.
📊 Production Insight
A missing cleanup in a WebSocket subscription caused memory leaks and duplicate connections in production, eventually crashing the browser tab.
🎯 Key Takeaway
Always return a cleanup function from useEffect to cancel subscriptions and async operations.

useMemo and useCallback: When to Memoize

useMemo memoizes a computed value, and useCallback memoizes a function. They are optimization tools, not guarantees. Use them only when the computation is expensive (e.g., large arrays, complex calculations) or when the value/function is passed to a child component that uses React.memo. Premature memoization adds complexity and can even hurt performance by increasing memory usage. Profile first, then memoize. Also, useMemo should not be used for side effects; that's what useEffect is for. Remember that useMemo and useCallback have their own dependency arrays, which must be correct.

Memoization.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState, useMemo, useCallback } from 'react';

function ExpensiveList({ items, filter }) {
  const filtered = useMemo(
    () => items.filter(item => item.includes(filter)),
    [items, filter]
  );

  return <ul>{filtered.map(item => <li key={item}>{item}</li>)}</ul>;
}

export default function App() {
  const [filter, setFilter] = useState('');
  const items = useMemo(() => Array.from({ length: 10000 }, (_, i) => `Item ${i}`), []);

  const handleChange = useCallback((e) => setFilter(e.target.value), []);

  return (
    <>
      <input onChange={handleChange} />
      <ExpensiveList items={items} filter={filter} />
    </>
  );
}
Output
Renders a filtered list of 10,000 items efficiently.
Try it live
⚠ Don't Memoize Everything
Memoization has overhead. Only use it when you have measured a performance bottleneck. Premature optimization can make code harder to read.
📊 Production Insight
A team added useMemo to every computed value, causing a 20% memory increase. Profiling showed only one computation was expensive; removing the rest improved performance.
🎯 Key Takeaway
Use useMemo and useCallback only for expensive computations or to preserve referential equality for child components.

Custom Hooks: Extract and Reuse Logic

Custom Hooks are the primary way to reuse stateful logic across components. They are JavaScript functions that call other Hooks and follow the same rules. Name them with a 'use' prefix to enable linting. Extract logic that involves multiple Hooks or complex side effects into a custom Hook. This improves readability and testability. For example, a useFetch hook can encapsulate loading, error, and data states. Custom Hooks can also accept arguments and return values. They compose naturally, allowing you to build complex behavior from simple pieces.

useFetch.jsJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error('Network response was not ok');
        return res.json();
      })
      .then(data => {
        if (!cancelled) {
          setData(data);
          setLoading(false);
        }
      })
      .catch(err => {
        if (!cancelled) {
          setError(err);
          setLoading(false);
        }
      });
    return () => { cancelled = true; };
  }, [url]);

  return { data, loading, error };
}
Output
Returns { data, loading, error } for any URL.
Try it live
💡Custom Hooks Are Just Functions
They can call other custom Hooks, accept parameters, and return anything. Use them to abstract complex logic and keep components clean.
📊 Production Insight
A codebase with 50+ components all had duplicated fetch logic. Extracting a useFetch hook reduced bugs by 30% and made error handling consistent.
🎯 Key Takeaway
Extract reusable stateful logic into custom Hooks to improve code organization and reusability.

Testing Hooks: Best Practices

Testing Hooks requires a React environment. Use @testing-library/react-hooks or renderHook from React's test utilities. Test the hook's behavior by calling it inside a test component. For custom Hooks, test the returned values and side effects. Mock external dependencies like fetch. Ensure that state updates and effects run correctly. Also test edge cases: missing dependencies, cleanup, and error states. Avoid testing implementation details; focus on the hook's contract (inputs and outputs).

useFetch.test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { renderHook, act } from '@testing-library/react';
import { useFetch } from './useFetch';

global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve({ name: 'John' }),
  })
);

test('should return data after fetch', async () => {
  const { result, waitForNextUpdate } = renderHook(() => useFetch('/api/user'));

  expect(result.current.loading).toBe(true);

  await waitForNextUpdate();

  expect(result.current.data).toEqual({ name: 'John' });
  expect(result.current.loading).toBe(false);
  expect(result.current.error).toBeNull();
});
Output
Test passes: loading starts true, then becomes false with data.
Try it live
🔥Test Behavior, Not Implementation
Don't test internal state directly; test what the hook returns and how it affects the component. This makes tests resilient to refactoring.
📊 Production Insight
A custom hook for authentication had a bug where the token wasn't cleared on logout. The test caught it because the hook returned a non-null token after logout.
🎯 Key Takeaway
Use renderHook to test custom Hooks in isolation, focusing on inputs and outputs.

Common Pitfalls and How to Avoid Them

Even experienced developers fall into Hook traps. Stale closures: when an effect captures an old value because the dependency array is missing. Infinite loops: when an effect updates state that is in its dependency array. Over-fetching: when useEffect runs on every render due to missing dependencies. State mutation: directly modifying state objects. Mixing Hooks with class components: you cannot use Hooks inside class components. To avoid these, always use the exhaustive-deps ESLint plugin, prefer functional updates, and keep state minimal. Profile before optimizing.

InfiniteLoop.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setCount(count + 1); // ❌ count is in deps, causes infinite loop
  }, [count]);

  return <p>{count}</p>;
}
Output
Infinite loop: component re-renders forever.
Try it live
⚠ Avoid Infinite Loops
Never update state inside useEffect if that state is in the dependency array. Use functional updates or remove the dependency if possible.
📊 Production Insight
An infinite loop caused by a missing dependency in useEffect brought down a production server by exhausting CPU. The fix was adding the missing dependency and using functional update.
🎯 Key Takeaway
Watch out for stale closures, infinite loops, and state mutations; use linting and best practices to avoid them.

Performance Optimization with Hooks

React is fast by default, but you can optimize with Hooks. Use React.memo to prevent unnecessary re-renders of child components when props haven't changed. Combine with useCallback and useMemo to preserve referential equality. Avoid creating new objects or arrays in render that are passed as props. Use useRef for mutable values that don't need to trigger re-renders. For large lists, use virtualization (e.g., react-window). Always measure performance with React DevTools Profiler before optimizing.

OptimizedList.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { useState, useCallback, memo } from 'react';

const ListItem = memo(({ item, onDelete }) => {
  console.log('Rendering', item.id);
  return (
    <li>
      {item.text}
      <button onClick={() => onDelete(item.id)}>Delete</button>
    </li>
  );
});

export default function List({ items }) {
  const [selected, setSelected] = useState(null);

  const handleDelete = useCallback((id) => {
    // delete logic
  }, []);

  return (
    <ul>
      {items.map(item => (
        <ListItem key={item.id} item={item} onDelete={handleDelete} />
      ))}
    </ul>
  );
}
Output
Only re-renders ListItem when its props change.
Try it live
💡Profile Before Optimizing
Use React DevTools Profiler to identify bottlenecks. Premature optimization adds complexity without measurable benefit.
📊 Production Insight
A team added React.memo to every component, causing memory bloat. Profiling revealed only one list component was slow; memoizing it solved the issue.
🎯 Key Takeaway
Optimize with React.memo, useCallback, and useMemo only after profiling shows a performance issue.

Migrating from Class Components to Hooks

Migrating class components to Hooks can improve code readability and reduce bundle size. Start by identifying state and lifecycle methods. Replace this.state and this.setState with useState. Replace componentDidMount, componentDidUpdate, and componentWillUnmount with useEffect. Use useRef for instance variables. Convert lifecycle logic into multiple useEffect calls if needed. Test thoroughly after migration. Hooks allow you to extract logic into custom Hooks, making the code more modular. However, don't migrate everything at once; do it incrementally.

ClassToHook.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Before: Class component
class Timer extends React.Component {
  state = { seconds: 0 };
  intervalId = null;

  componentDidMount() {
    this.intervalId = setInterval(() => {
      this.setState(prev => ({ seconds: prev.seconds + 1 }));
    }, 1000);
  }

  componentWillUnmount() {
    clearInterval(this.intervalId);
  }

  render() {
    return <p>{this.state.seconds}s</p>;
  }
}

// After: Functional component with Hooks
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{seconds}s</p>;
}
Output
Both versions render a timer that increments every second.
Try it live
🔥Migrate Incrementally
You can use Hooks in new components while keeping existing class components. No need for a full rewrite.
📊 Production Insight
A gradual migration from classes to Hooks reduced the bundle size by 15% and improved developer velocity, but required careful testing of side effects.
🎯 Key Takeaway
Replace class lifecycle methods with useEffect and state with useState; extract logic into custom Hooks.

React Compiler and Hook Rules

The React Compiler (formerly known as React Forget) is a new tool that automatically memoizes components and hooks, reducing the need for manual useMemo, useCallback, and React.memo. However, it does not change the fundamental rules of hooks. The compiler still requires hooks to be called at the top level of React functions and only from React functions. The compiler enforces these rules during compilation and will throw errors if violations are detected. For example, calling a hook inside a condition or loop will still be flagged. The compiler's memoization optimizations are applied after the rules are satisfied, meaning you must still follow the rules of hooks. In practice, the compiler can eliminate many performance pitfalls, but developers must ensure their code adheres to the rules to benefit from compilation. A common misconception is that the compiler allows breaking hook rules—it does not. The compiler simply automates memoization, not the structural constraints of hooks.

CompilerHookRules.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ This will still cause a compiler error
function MyComponent({ flag }) {
  if (flag) {
    const [value, setValue] = useState(0); // Error: Hook called conditionally
  }
  return <div />;
}

// ✅ This is valid and can be optimized by the compiler
function MyComponent({ flag }) {
  const [value, setValue] = useState(0);
  useEffect(() => {
    // side effect
  }, []);
  return <div>{flag ? value : null}</div>;
}
Try it live
🔥Compiler Does Not Override Rules
📊 Production Insight
When using the React Compiler, you can remove manual useMemo and useCallback calls, but keep the ESLint react-hooks plugin enabled to catch rule violations early. The compiler will optimize performance, but structural errors remain your responsibility.
🎯 Key Takeaway
The React Compiler automates memoization but does not relax the rules of hooks; you must still follow them for your code to compile and run correctly.

Server Components and Hooks

React Server Components (RSC) run on the server and do not have access to client-side features like state, effects, or browser APIs. Therefore, hooks such as useState, useEffect, useReducer, useRef, and custom hooks that use them are unavailable in Server Components. Only hooks that are purely computational and do not rely on client-side interactivity can be used, such as useId (for generating unique IDs) and use (for reading context, but only in limited scenarios). If you attempt to use a client-side hook in a Server Component, React will throw an error. To use hooks, you must mark the component with 'use client' directive, making it a Client Component. This distinction is crucial for performance: Server Components reduce bundle size and improve initial load, but they cannot handle interactivity. When designing your app, separate server-only data fetching and static rendering from client-side interactive logic. For example, a Server Component can fetch data and pass it to a Client Component that uses hooks for interactivity.

ServerComponentExample.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ServerComponent.server.jsx (no 'use client')
export default function ServerComponent() {
  // ❌ Cannot use hooks like useState here
  // const [count, setCount] = useState(0); // Error
  return <div>Static content</div>;
}

// ClientComponent.client.jsx ('use client')
'use client';
import { useState } from 'react';
export default function ClientComponent({ data }) {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c+1)}>Count: {count}</button>;
}
Try it live
⚠ Hooks Are Client-Only
📊 Production Insight
In production, keep Server Components for data fetching and static content to reduce bundle size. Use Client Components sparingly for interactive parts. This pattern improves initial load time and SEO.
🎯 Key Takeaway
React Server Components cannot use most hooks; only Client Components (marked with 'use client') can use hooks like useState and useEffect.
useMemo vs useCallback When to memoize values vs functions useMemo useCallback Purpose Memoize a computed value Memoize a function reference Return Type Value (any type) Function Use Case Expensive calculations, derived data Passing callbacks to child components Dependencies Array of values the computation depends Array of values the function closure dep Performance Impact Avoids re-computation on re-renders Prevents unnecessary re-renders of child THECODEFORGE.IO
thecodeforge.io
React Hooks Rules

useReducer vs useState Decision Tree

Choosing between useState and useReducer depends on the complexity of your state logic. useState is ideal for simple, independent state values like a toggle, a counter, or a form field. useReducer is better when state transitions involve multiple sub-values or when the next state depends on the previous state in complex ways. A decision tree: (1) Is your state a single primitive value? Use useState. (2) Does your state have multiple related values that change together? Consider useReducer. (3) Do you have complex state logic with multiple conditions or actions? Use useReducer. (4) Do you need to pass state update logic down to child components without callbacks? useReducer's dispatch is stable and can be passed directly. (5) Are you dealing with deeply nested state? useReducer with Immer can simplify updates. For example, a form with multiple fields is a good candidate for useReducer because you can handle all field updates with a single reducer. In contrast, a simple counter is fine with useState. Performance-wise, useReducer can avoid unnecessary re-renders if the reducer is pure and the dispatch function is stable. However, for most cases, useState is simpler and sufficient. Start with useState and refactor to useReducer when you find yourself writing multiple useState calls that are tightly coupled.

ReducerVsState.jsxJSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// useState for simple state
const [count, setCount] = useState(0);

// useReducer for complex state
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    default:
      return state;
  }
}
const [state, dispatch] = useReducer(reducer, initialState);
Try it live
💡Start Simple, Refactor Later
📊 Production Insight
In production, useReducer can improve performance by providing a stable dispatch function and reducing re-renders when state logic is complex. However, avoid premature optimization; useState is often sufficient and easier to debug.
🎯 Key Takeaway
Use useState for simple independent state and useReducer for complex state logic with multiple sub-values or actions; start with useState and refactor to useReducer as needed.
⚙ Quick Reference
15 commands from this guide
FileCommand / CodePurpose
BadHookOrder.jsxexport default function BadComponent({ flag }) {Why React Hooks Rules Exist
TopLevelHooks.jsxexport default function UserProfile({ userId }) {The Rules of Hooks
CustomHook.jsxfunction useWindowWidth() {Only Call Hooks from React Functions
DependencyArray.jsxexport default function SearchResults({ query }) {The Dependency Array
ImmutableState.jsxexport default function TodoList() {useState
EffectCleanup.jsxexport default function UserStatus({ userId }) {useEffect
Memoization.jsxfunction ExpensiveList({ items, filter }) {useMemo and useCallback
useFetch.jsexport function useFetch(url) {Custom Hooks
useFetch.test.jsglobal.fetch = jest.fn(() =>Testing Hooks
InfiniteLoop.jsxexport default function Counter() {Common Pitfalls and How to Avoid Them
OptimizedList.jsxconst ListItem = memo(({ item, onDelete }) => {Performance Optimization with Hooks
ClassToHook.jsxclass Timer extends React.Component {Migrating from Class Components to Hooks
CompilerHookRules.jsxfunction MyComponent({ flag }) {React Compiler and Hook Rules
ServerComponentExample.jsxexport default function ServerComponent() {Server Components and Hooks
ReducerVsState.jsxconst [count, setCount] = useState(0);useReducer vs useState Decision Tree

Key takeaways

1
Hook Call Order
Always call Hooks at the top level of your component, never inside conditions or loops, to ensure consistent state mapping across renders.
2
Dependency Arrays
Include every reactive value used inside useEffect, useMemo, or useCallback to avoid stale closures and missing updates.
3
Custom Hooks
Extract reusable stateful logic into custom Hooks (prefix with 'use') to improve code organization and testability.
4
Optimize with Caution
Use React.memo, useMemo, and useCallback only after profiling confirms a performance bottleneck; premature optimization adds complexity.

Common mistakes to avoid

3 patterns
×

Not understanding React component re-rendering

Fix
Use React.memo, useMemo, and useCallback strategically to prevent unnecessary re-renders.
×

Ignoring the rules of hooks

Fix
Always call hooks at the top level, not inside conditions, loops, or callbacks.
×

Mutating state directly instead of using setState

Fix
Always use the state setter function and treat state as immutable.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the Virtual DOM and how does React use it?
Q02JUNIOR
Explain the difference between state and props.
Q03JUNIOR
What is the purpose of the useEffect hook?
Q04JUNIOR
How does React handle keys in lists?
Q01 of 04JUNIOR

What is the Virtual DOM and how does React use it?

ANSWER
The Virtual DOM is a lightweight JavaScript representation of the real DOM. React diffs the Virtual DOM with the previous version and applies only the changed parts to the real DOM.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I call Hooks inside if statements?
02
What happens if I miss a dependency in useEffect?
03
How do I test a custom Hook?
04
When should I use useMemo vs useCallback?
05
Can I use Hooks inside class components?
06
Why does my useEffect run infinitely?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 13, 2026
last updated
2,018
articles · all by Naren
🔥

That's React. Mark it forged?

6 min read · try the examples if you haven't

Previous
Custom Hooks
28 / 40 · React
Next
React Router