All writing

July 3, 2026

Rules of Hooks - React Development Guide

Comprehensive guide to React Hooks rules, violations, and solutions

React Frontend

Comprehensive guide to React Hooks rules, violations, and solutions

Table of Contents


Overview

This document provides a comprehensive guide to the Rules of Hooks, documenting common violations encountered during development, their solutions, and prevention strategies.

The Two Rules of Hooks

Rule 1: Only Call Hooks at the Top Level

  • Do: Call hooks at the top level of React functions
  • Don’t: Call hooks inside loops, conditions, or nested functions

Rule 2: Only Call Hooks from React Functions

  • Do: Call hooks from React function components or custom hooks
  • Don’t: Call hooks from regular JavaScript functions, class components, or event handlers

Case 1: Hook Order Violation

Problem Description

Error Message:

Warning: React has detected a change in the order of Hooks called by Cell.
This will lead to bugs and errors if not fixed. For more information,
read the Rules of Hooks: https://reactjs.org/link/rules-of-hooks

Context: This error occurred in table column rendering where cell components were not being properly rendered as React components.

Root Cause Analysis

The warning occurred because cell components were being called as functions instead of being rendered as React components in the tableColumns function. This violated the Rules of Hooks because:

  1. Function calls vs Component rendering: Components were called directly like components.DurationWIPCell({ record }) instead of being rendered as JSX
  2. Hook order inconsistency: When components are called as functions, React can’t properly track hook calls, leading to inconsistent hook ordering
  3. Missing React context: Function calls don’t provide the proper React rendering context needed for hooks

Solution Implementation

1. Proper JSX Rendering
Changed from function calls to proper JSX rendering:

// ❌ Before (incorrect)
columns.render = (_text, record) => components.DurationWIPCell({ record });

// ✅ After (correct)
columns.render = (_text, record) => (
  <components.DurationWIPCell record={record} />
);

Result: Cell components now render correctly as React components, maintaining consistent hook call order and eliminating the warning.

Case 2: Conditional Hook Calls

Problem Description

Error Message:

Warning: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order every time the component renders.

Context: This error occurs when hooks are called inside conditional statements, causing React to lose track of hook order.

Root Cause Analysis

Conditional hook calls violate Rule 1 because:

  1. Inconsistent hook order: Different code paths lead to different hook call sequences
  2. React’s internal tracking: React relies on consistent hook call order to maintain state
  3. Rendering unpredictability: Conditional hooks make component behavior unpredictable

Solution Implementation

❌ Before (incorrect):

function MyComponent({ shouldUseEffect }) {
  if (shouldUseEffect) {
    const [state, setState] = useState(0); // ❌ Conditional hook
  }

  return <div>Content</div>;
}

✅ After (correct):

function MyComponent({ shouldUseEffect }) {
  const [state, setState] = useState(0); // ✅ Always called

  useEffect(() => {
    if (shouldUseEffect) {
      // Conditional logic inside hook
      setState((prev) => prev + 1);
    }
  }, [shouldUseEffect]);

  return <div>Content</div>;
}

Case 3: Hook Calls in Loops

Problem Description

Error Message:

Warning: React Hook "useState" is called in a loop. React Hooks must be called in the exact same order every time the component renders.

Root Cause Analysis

Loop-based hook calls violate Rule 1 because:

  1. Variable hook count: Number of hook calls depends on loop iterations
  2. Unpredictable order: Hook order changes based on data length
  3. State management issues: React can’t properly track state for dynamic hook counts

Solution Implementation

❌ Before (incorrect):

function ItemList({ items }) {
  return (
    <div>
      {items.map((item) => {
        const [isSelected, setIsSelected] = useState(false); // ❌ Hook in loop
        return <Item key={item.id} item={item} />;
      })}
    </div>
  );
}

✅ After (correct):

function ItemList({ items }) {
  const [selectedItems, setSelectedItems] = useState(new Set());

  const toggleItem = (itemId) => {
    setSelectedItems((prev) => {
      const newSet = new Set(prev);
      if (newSet.has(itemId)) {
        newSet.delete(itemId);
      } else {
        newSet.add(itemId);
      }
      return newSet;
    });
  };

  return (
    <div>
      {items.map((item) => (
        <Item
          key={item.id}
          item={item}
          isSelected={selectedItems.has(item.id)}
          onToggle={() => toggleItem(item.id)}
        />
      ))}
    </div>
  );
}

Case 4: Hook Calls in Nested Functions

Problem Description

Error Message:

Warning: React Hook "useEffect" is called in a function that is neither a React function component nor a custom React Hook function.

Root Cause Analysis

Nested function hook calls violate Rule 2 because:

  1. Wrong execution context: Hooks need React’s component context
  2. Missing lifecycle tracking: Nested functions don’t participate in React’s lifecycle
  3. State management failure: React can’t properly manage hook state

Solution Implementation

❌ Before (incorrect):

function MyComponent() {
  const handleClick = () => {
    useEffect(() => {
      // ❌ Hook in nested function
      console.log("Effect in handler");
    }, []);
  };

  return <button onClick={handleClick}>Click me</button>;
}

✅ After (correct):

function MyComponent() {
  const [clickCount, setClickCount] = useState(0);

  useEffect(() => {
    console.log("Effect runs on click count change");
  }, [clickCount]);

  const handleClick = () => {
    setClickCount((prev) => prev + 1);
  };

  return <button onClick={handleClick}>Click me</button>;
}

Case 5: Hook Calls in Event Handlers

Problem Description

Error Message:

Warning: React Hook "useState" is called in a function that is neither a React function component nor a custom React Hook function.

Root Cause Analysis

Event handler hook calls violate Rule 2 because:

  1. Event context: Event handlers run outside React’s component lifecycle
  2. Timing issues: Hooks need to be called during render phase
  3. State synchronization: React can’t properly synchronize state updates

Solution Implementation

❌ Before (incorrect):

function MyComponent() {
  const handleSubmit = (event) => {
    event.preventDefault();
    const [data, setData] = useState(null); // ❌ Hook in event handler
    // Process form data
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

✅ After (correct):

function MyComponent() {
  const [data, setData] = useState(null);
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setIsSubmitting(true);

    try {
      const result = await processFormData();
      setData(result);
    } finally {
      setIsSubmitting(false);
    }
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

Advanced Patterns

Custom Hooks for Complex Logic

✅ Extract complex logic into custom hooks:

// Custom hook
function useFormValidation(initialValues) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isValid, setIsValid] = useState(false);

  useEffect(() => {
    const hasErrors = Object.values(errors).some((error) => error);
    setIsValid(!hasErrors);
  }, [errors]);

  const validate = (name, value) => {
    // Validation logic
  };

  const handleChange = (name, value) => {
    setValues((prev) => ({ ...prev, [name]: value }));
    validate(name, value);
  };

  return { values, errors, isValid, handleChange };
}

// Component usage
function MyForm() {
  const { values, errors, isValid, handleChange } = useFormValidation({
    email: "",
    password: "",
  });

  return (
    <form>
      <input
        value={values.email}
        onChange={(e) => handleChange("email", e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}
    </form>
  );
}

Conditional Logic with Hooks

✅ Use hooks with conditional logic inside:

function ConditionalComponent({ shouldFetch, userId }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (shouldFetch && userId) {
      setLoading(true);
      fetchUserData(userId)
        .then(setData)
        .finally(() => setLoading(false));
    }
  }, [shouldFetch, userId]);

  if (loading) return <div>Loading...</div>;
  if (!data) return <div>No data</div>;

  return <div>{data.name}</div>;
}

Prevention Guidelines

✅ Do’s

  • Always call hooks at the top level of React function components
  • Use custom hooks for complex logic and state management
  • Keep hook calls consistent - same order every render
  • Use conditional logic inside hooks rather than conditional hook calls
  • Extract state logic into custom hooks for reusability
  • Use proper dependency arrays in useEffect and other hooks
  • Always render components as JSX: <Component />
  • Import React when using JSX in files

❌ Don’ts

  • Don’t call hooks conditionally - no if statements around hooks
  • Don’t call hooks in loops - use state management patterns instead
  • Don’t call hooks in nested functions - keep them at component level
  • Don’t call hooks in event handlers - use state updates instead
  • Don’t call components as functions: Component(props)
  • Don’t mix function calls and JSX rendering for the same components
  • Don’t forget dependency arrays in useEffect
  • Don’t call hooks from regular JavaScript functions

Debugging Hook Issues

Common Error Messages

  1. “React Hook is called conditionally”
  • Cause: Hook called inside if statement
  • Fix: Move conditional logic inside the hook
  1. “React Hook is called in a loop”
  • Cause: Hook called inside map/forEach
  • Fix: Use state management patterns (Set, Map, or object)
  1. “React Hook is called in a function that is neither a React function component nor a custom React Hook function”
  • Cause: Hook called in event handler or nested function
  • Fix: Move hook to component level, use state updates in handlers
  1. “React has detected a change in the order of Hooks”
  • Cause: Inconsistent hook call order between renders
  • Fix: Ensure hooks are always called in the same order

Debugging Tools

  • React Developer Tools: Use the hooks tab to inspect hook state
  • ESLint Plugin: Install eslint-plugin-react-hooks for automatic detection
  • Console Warnings: React provides detailed warnings in development mode

Best Practices for Debugging

  1. Use consistent naming for hooks and state variables
  2. Add console.log statements to track hook execution order
  3. Use React DevTools to inspect component state
  4. Test with different data to ensure hook order consistency
  5. Use custom hooks to isolate complex logic

Additional Hook Rules & Best Practices

Rule 3: Always Use the Same Hook Names

✅ Do:

function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  // Always use the same variable names
}

❌ Don’t:

function MyComponent() {
  const [count, setCount] = useState(0);
  const [userName, setUserName] = useState(''); // Inconsistent naming
}

Rule 4: Don’t Mutate Hook Dependencies

❌ Don’t mutate dependencies:

function MyComponent({ items }) {
  useEffect(() => {
    items.push('new item'); // ❌ Mutating props
    console.log(items);
  }, [items]);
}

✅ Create new references:

function MyComponent({ items }) {
  const [localItems, setLocalItems] = useState(items);
  
  useEffect(() => {
    setLocalItems(prev => [...prev, 'new item']); // ✅ New reference
  }, [items]);
}

Rule 5: Use useCallback and useMemo Wisely

✅ Optimize expensive operations:

function ExpensiveComponent({ items, onItemClick }) {
  const memoizedItems = useMemo(() => {
    return items.map(item => ({
      ...item,
      processed: expensiveCalculation(item)
    }));
  }, [items]);

  const handleClick = useCallback((id) => {
    onItemClick(id);
  }, [onItemClick]);

  return (
    <div>
      {memoizedItems.map(item => (
        <Item key={item.id} item={item} onClick={handleClick} />
      ))}
    </div>
  );
}

Rule 6: Clean Up Effects Properly

✅ Always clean up subscriptions:

function DataComponent({ userId }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    let cancelled = false;
    
    const fetchData = async () => {
      try {
        const result = await api.getUser(userId);
        if (!cancelled) {
          setData(result);
        }
      } catch (error) {
        if (!cancelled) {
          console.error('Failed to fetch data:', error);
        }
      }
    };

    fetchData();

    return () => {
      cancelled = true; // ✅ Cleanup
    };
  }, [userId]);

  return <div>{data?.name}</div>;
}

Rule 7: Avoid Stale Closures

❌ Stale closure problem:

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

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1); // ❌ Stale closure
    }, 1000);

    return () => clearInterval(interval);
  }, []); // ❌ Missing dependency
}

✅ Fix with functional updates:

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

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prev => prev + 1); // ✅ Functional update
    }, 1000);

    return () => clearInterval(interval);
  }, []); // ✅ No dependencies needed
}

Rule 8: Use Custom Hooks for Reusable Logic

✅ Extract reusable logic:

// Custom 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 = useCallback((value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key]);

  return [storedValue, setValue];
}

// Usage
function MyComponent() {
  const [name, setName] = useLocalStorage('name', '');
  
  return (
    <input 
      value={name} 
      onChange={(e) => setName(e.target.value)} 
    />
  );
}

Rule 9: Handle Async Operations Correctly

✅ Proper async handling:

function AsyncComponent({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const userData = await api.getUser(userId);
        if (!cancelled) {
          setUser(userData);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    if (userId) {
      fetchUser();
    }

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return <div>{user.name}</div>;
}

Rule 10: Use Proper Dependency Arrays

✅ Correct dependency arrays:

function Component({ userId, filters }) {
  const [data, setData] = useState(null);

  // ✅ Include all dependencies
  useEffect(() => {
    fetchData(userId, filters).then(setData);
  }, [userId, filters]); // ✅ All dependencies included

  // ✅ Use useCallback for stable references
  const handleSubmit = useCallback((formData) => {
    submitForm(userId, formData);
  }, [userId]); // ✅ userId dependency

  return <form onSubmit={handleSubmit}>...</form>;
}

References