Calling hooks inside conditions or loops – Hooks can only be called inside the body of a function component

Loading

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

  1. Always call hooks at the top level of your component
  2. Extract conditional logic into the hook itself or custom hooks
  3. Use eslint-plugin-react-hooks to catch violations
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *