What are the rules for using hooks in React?

👨‍💻 Frontend Developer 🟡 Often Asked 🎚️ Medium
#React #Hooks

Brief Answer

There are two main rules for using hooks in React:

Rule 1: Only call hooks at the top level

  • Don’t call hooks inside loops, conditions, or nested functions
  • Hooks must be called in the same order on every render

Rule 2: Only call hooks in React functional components or custom hooks

  • Don’t call hooks in regular JavaScript functions
  • Don’t call hooks in class components

Key rule: Maintain hook call order and don’t call them conditionally! 🎯


Full Answer

Imagine hooks are like ingredients in a recipe. If you add them in a different order each time or skip some, the dish will be unpredictable! 🍳

Main hook rules

React relies on hook call order for proper functioning:

// ✅ Correct - hooks at top level
function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  useEffect(() => {
    // Effect
  }, []);
  
  return (
    <div>
      <p>{count} - {name}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
 
// ❌ Incorrect - hooks in&nbsp;conditions
function BadComponent({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // ERROR!
  }
  
  return <div>Component</div>;
}

Why hook order matters

React uses an internal list to track hooks:

// How React sees hooks:
// First render:
// 1. useState(0) -> [0, setter]
// 2. useState('') -> ['', setter]
// 3. useEffect(() => {...}) -> undefined
 
// Second render:
// 1. useState(0) -> [0, setter] // Same hook!
// 2. useState('') -> ['', setter] // Same hook!
// 3. useEffect(() => {...}) -> undefined // Same hook!

Common mistakes

1. Conditional hook calls

// ❌ Incorrect - conditional call
function BadComponent({ isLoggedIn }) {
  const [user, setUser] = useState(null);
  
  if (isLoggedIn) {
    // This breaks hook order!
    const [token, setToken] = useState(''); // ERROR!
  }
  
  return <div>Component</div>;
}
 
// ✅ Correct - always call hooks
function GoodComponent({ isLoggedIn }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(''); // Always called
  
  // Use conditional logic inside hooks
  useEffect(() => {
    if (isLoggedIn) {
      // Logic for authenticated users
    }
  }, [isLoggedIn]);
  
  return <div>Component</div>;
}

2. Loops and hooks

// ❌ Incorrect - hooks in&nbsp;loops
function BadComponent({ items }) {
  const [state, setState] = useState(null);
  
  items.forEach(item => {
    // Each render might have different number of&nbsp;items!
    const [itemState, setItemState] = useState(item); // ERROR!
  });
  
  return <div>Component</div>;
}
 
// ✅ Correct - hooks outside loops
function GoodComponent({ items }) {
  const [state, setState] = useState(null);
  
  // Use array of&nbsp;states
  const [itemStates, setItemStates] = useState(
    items.map(item => item)
  );
  
  return (
    <div>
      {items.map((item, index) => (
        <div key={index}>
          <input 
            value={itemStates[index]}
            onChange={e => {
              const newStates = [...itemStates];
              newStates[index] = e.target.value;
              setItemStates(newStates);
            }}
          />
        </div>
      ))}
    </div>
  );
}

3. Calling hooks in regular functions

// ❌ Incorrect - hooks in&nbsp;regular functions
function regularFunction() {
  const [state, setState] = useState(0); // ERROR!
  return state;
}
 
// ✅ Correct - hooks in&nbsp;components or&nbsp;custom hooks
function Component() {
  const value = useCustomHook(); // Correct
  return <div>{value}</div>;
}
 
function useCustomHook() {
  const [state, setState] = useState(0); // Correct
  return state;
}

Rules for custom hooks

Naming

// ✅ Custom hooks start with "use"
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}
 
// ❌ Incorrect naming
function counterHook() { // Should start with "use"
  const [count, setCount] = useState(0);
  return [count, setCount];
}

Proper custom hook usage

// ✅ Proper usage
function UserProfile() {
  const { count, increment, decrement, reset } = useCounter(0);
  
  return (
    <div>
      <p>Counter: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}
 
// ✅ Custom hook for API
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [url]);
  
  return { data, loading, error };
}

Best practices

// ❌ Too many separate states
function BadForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  // ... more states
  
  return (
    <form>
      {/* Form fields */}
    </form>
  );
}
 
// ✅ Grouping in&nbsp;object
function GoodForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const updateField = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  return (
    <form>
      <input 
        value={formData.name}
        onChange={e => updateField('name', e.target.value)}
      />
      <input 
        value={formData.email}
        onChange={e => updateField('email', e.target.value)}
      />
      {/* Other fields */}
    </form>
  );
}

2. useEffect optimization

// ❌ Suboptimal useEffect
function BadComponent({ userId, postId }) {
  const [user, setUser] = useState(null);
  const [post, setPost] = useState(null);
  
  // Runs when any prop changes
  useEffect(() => {
    fetchUser(userId).then(setUser);
    fetchPost(postId).then(setPost);
  }, [userId, postId]); // Both props in dependencies
  
  return <div>Component</div>;
}
 
// ✅ Separated effects
function GoodComponent({ userId, postId }) {
  const [user, setUser] = useState(null);
  const [post, setPost] = useState(null);
  
  // Separate effects for different data
  useEffect(() => {
    if (userId) {
      fetchUser(userId).then(setUser);
    }
  }, [userId]); // Only userId
  
  useEffect(() => {
    if (postId) {
      fetchPost(postId).then(setPost);
    }
  }, [postId]); // Only postId
  
  return <div>Component</div>;
}

When to use hooks

1. useState for local state

// ✅ Simple state
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Counter: {count}
    </button>
  );
}

2. useEffect for side effects

// ✅ Subscriptions and cleanup
function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // Cleanup subscription
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return (
    <div>
      Window size: {size.width} x {size.height}
    </div>
  );
}

3. Custom hooks for reusability

// ✅ Reusable 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 = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}
 
// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'ru');
  
  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      
      <select value={language} onChange={e => setLanguage(e.target.value)}>
        <option value="ru">Русский</option>
        <option value="en">English</option>
      </select>
    </div>
  );
}

Summary

Hook rules are like traffic rules for React! 🚦

  • Rule 1: Only call hooks at top level ✅
  • Rule 2: Only call hooks in functional components or custom hooks ✅

Never break these rules:

  • Never call hooks conditionally ❌
  • Never call hooks in loops ❌
  • Never call hooks in regular functions ❌

Practical tips:

  1. Use ESLint plugin for React Hooks
  2. Name custom hooks with “use” prefix
  3. Group related states
  4. Split complex effects into multiple useEffect

Following hook rules is the key to stable and predictable React applications! 💪


Want more useful React articles? Subscribe to EasyAdvice, bookmark the site and level up every day! 🚀