Calling hooks conditionally or inside loops

A fundamental rule of React Hooks is that they must always be called at the top level of your component, never inside conditions, loops, or nested functions. Violating this rule leads to errors and unpredictable behavior.

The Problem

// ❌ Wrong - Hook inside condition
function BadComponent({ shouldFetch }) {
  if (shouldFetch) {
    const [data, setData] = useState(null); // Error!
  }

  // ❌ Wrong - Hook inside loop
  for (let i = 0; i < 5; i++) {
    useEffect(() => { /*...*/ }, []); // Error!
  }

  // ❌ Wrong - Hook inside callback
  const handleClick = () => {
    const [count, setCount] = useState(0); // Error!
  };
}

Correct Solutions

1. Move Hooks to Top Level

// ✅ Correct - all hooks at top level
function GoodComponent({ shouldFetch }) {
  const [data, setData] = useState(null);
  const [count, setCount] = useState(0);

  useEffect(() => {
    if (shouldFetch) {
      fetchData().then(setData);
    }
  }, [shouldFetch]);

  const handleClick = () => {
    setCount(c => c + 1);
  };
}

2. For Conditional Logic

// ✅ Correct - condition inside hook
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (userId) { // Condition inside hook is fine
      fetchUser(userId).then(setUser);
    }
  }, [userId]);
}

3. For Dynamic Lists

// ✅ Correct - single hook manages array state
function ItemList({ initialItems }) {
  const [items, setItems] = useState(initialItems);

  // Don't create hooks per item - manage array state
  const updateItem = useCallback((id, newValue) => {
    setItems(items => items.map(item => 
      item.id === id ? { ...item, ...newValue } : item
    ));
  }, []);
}

Why This Rule Exists

React relies on the order of Hook calls to maintain state between re-renders. Changing the order (like by skipping Hooks in conditions) breaks this mechanism.

Common Mistakes

  1. Early returns before hooks:
   function Component({ requiredProp }) {
     if (!requiredProp) return null; // ❌ Before hooks
     const [state] = useState(); // Error
   }
  1. Hooks in switch cases:
   switch (type) {
     case 'A':
       useEffect(() => {...}, []); // ❌
       break;
   }
  1. Hooks in event handlers:
   const handleClick = () => {
     const [value, setValue] = useState(); // ❌
   };

Advanced Patterns

1. Custom Hooks for Logic Reuse

function useFetchConditionally(shouldFetch, url) {
  const [data, setData] = useState(null);

  useEffect(() => {
    if (shouldFetch) {
      fetch(url).then(r => r.json()).then(setData);
    }
  }, [shouldFetch, url]);

  return data;
}

// Usage
function Component({ shouldFetch }) {
  const data = useFetchConditionally(shouldFetch, '/api');
  // ...render
}

2. Dynamic Hook-like Behavior

function useDynamicValues(values) {
  // Track all values at top level
  const [state, setState] = useState(values);

  // Update only needed values
  const updateValue = useCallback((key, value) => {
    setState(prev => ({ ...prev, [key]: value }));
  }, []);

  return [state, updateValue];
}

Best Practices

  1. Always call hooks unconditionally at the top level
  2. Extract conditional logic inside hooks
  3. Use custom hooks for reusable conditional behavior
  4. Follow ESLint rules for hooks (eslint-plugin-react-hooks)
  5. Memoize values with useMemo instead of conditional hooks

Tools to Catch Errors

  1. ESLint Plugin:
   {
     "plugins": ["react-hooks"],
     "rules": {
       "react-hooks/rules-of-hooks": "error"
     }
   }
  1. Error Messages – React provides clear errors about hook violations
  2. TypeScript – Can help catch some hook misuses during development

Leave a Reply

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