Ojasa Mirai

Ojasa Mirai

ReactJS

Loading...

Learning Level

🟒 BeginnerπŸ”΅ Advanced
πŸ’Ύ Introduction to Stateβš›οΈ Using useState HookπŸ”„ Updating State Correctly🎯 Initial State Values🚫 Common State MistakesπŸ“Š Multiple State VariablesπŸ”— State & RenderingπŸ“ Forms with StateπŸ—οΈ State Structure Best Practices
Reactjs/State/Usestate Basics

πŸͺ useState Hooks Deep Dive β€” Mastering Hook Rules, Closures, and Updates

Beyond basic useState usage lies a deep understanding of Hook Rules, closure behavior, and functional updates. These concepts are essential for writing correct, predictable state code at scale.


🎯 Hook Rules and Why They Matter

React Hooks have strict rules that must be followed. Violating them causes bugs that are hard to track. These rules exist because Hooks rely on call order to maintain state associations.

Hooks depend on call order to track state. If you call hooks conditionally or in different orders between renders, React can't match state values to their updater functions, leading to silent state corruption.

import { useState } from 'react';

// ❌ WRONG: Conditional hook calls
function BadCounter({ showCount }: { showCount: boolean }) {
  if (showCount) {
    const [count, setCount] = useState(0); // Call order changes!
  }
  // This breaks React's state tracking
  return null;
}

// ❌ WRONG: Hooks in loops
function BadList({ items }: { items: string[] }) {
  for (let i = 0; i < items.length; i++) {
    const [state, setState] = useState(i); // Call order changes with items!
  }
  return null;
}

// βœ… CORRECT: Hooks at top level
function GoodCounter({ showCount }: { showCount: boolean }) {
  const [count, setCount] = useState(0);
  const [isVisible, setIsVisible] = useState(showCount);

  // Logic based on state, not hook calls
  if (showCount) {
    return <p>Count: {count}</p>;
  }
  return null;
}

// βœ… CORRECT: Extract to custom hook instead of looping
function useListItem(initialValue: string) {
  const [value, setValue] = useState(initialValue);
  return [value, setValue] as const;
}

function GoodList({ items }: { items: string[] }) {
  // Each item's state is in a separate component
  return (
    <div>
      {items.map((item) => (
        <ListItem key={item} initial={item} />
      ))}
    </div>
  );
}

function ListItem({ initial }: { initial: string }) {
  const [value, setValue] = useListItem(initial);
  return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Common mistakes with hook rules

// ❌ WRONG: Inside callbacks or event handlers
function BadClick() {
  const handleClick = () => {
    const [count, setCount] = useState(0); // Wrong!
  };
  return <button onClick={handleClick}>Click</button>;
}

// βœ… CORRECT: Hooks only at component level
function GoodClick() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1); // Use state from component level
  };
  return <button onClick={handleClick}>Count: {count}</button>;
}

// ❌ WRONG: Hooks in try-catch or async logic
async function BadAsync() {
  try {
    const [data, setData] = useState(null); // Wrong!
  } catch (e) {
    // Error handling
  }
}

// βœ… CORRECT: Move async logic to useEffect
function GoodAsync() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const result = await fetch('/api/data');
        setData(await result.json());
      } catch (e) {
        console.error(e);
      }
    };
    fetchData();
  }, []);

  return <p>{data ? 'Loaded' : 'Loading'}</p>;
}

</details>

πŸ’‘ Closures and State in React

State exists in closures. Each render captures its own version of state. Understanding this prevents common bugs with stale state and outdated references.

When a component renders, it creates a closure over current state values. Event handlers, callbacks, and effects inside that render capture those values. If you use the same function later, it still has the old valuesβ€”this is closure behavior, not a React bug.

import { useState, useRef } from 'react';

function ClosureDemonstration() {
  const [count, setCount] = useState(0);
  const countRef = useRef(0);

  // This button uses current count at click time (closure)
  const handleClickImmediate = () => {
    alert(`Count is: ${count}`); // Uses closure from render
  };

  // This function delays 3 seconds, count has changed by then
  const handleClickDelayed = () => {
    setTimeout(() => {
      alert(`Count is: ${count}`); // Still has old value from closure!
    }, 3000);
  };

  // This function uses ref, which is mutable
  const handleClickRef = () => {
    countRef.current = count; // Update ref
    setTimeout(() => {
      alert(`Ref value: ${countRef.current}`); // Gets latest value
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleClickImmediate}>Alert Now</button>
      <button onClick={handleClickDelayed}>Alert in 3s</button>
      <button onClick={handleClickRef}>Alert Ref in 3s</button>
    </div>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Closure problems in event handlers

function FormWithClosure() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  // ❌ BAD: Closure captures stale values
  const handleSubmitBad = async () => {
    // If user changes email after clicking, this still uses old value
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }), // Stale closure!
    });
  };

  // βœ… GOOD: Functional update or useCallback with dependencies
  const handleSubmitGood = async (e: React.FormEvent) => {
    e.preventDefault();
    // Get current values from the form
    const formData = new FormData(e.currentTarget);
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({
        email: formData.get('email'),
        password: formData.get('password'),
      }),
    });
  };

  return (
    <form onSubmit={handleSubmitGood}>
      <input
        name="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        name="password"
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

</details>

πŸ”§ Functional Updates and State Batching

Instead of passing new state directly, pass a function that receives previous state. This prevents closure bugs and enables proper batching in React 18.

Functional updates isolate each state update, making them independent. React can batch these updates safely, even when they depend on each other.

import { useState } from 'react';

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

  // ❌ PROBLEMATIC: Direct updates can cause issues
  const incrementBad = () => {
    setCount(count + 1); // Uses closure value
    setCount(count + 1); // Both use same closure value!
    // Result: +1, not +2
  };

  // βœ… CORRECT: Functional updates
  const incrementGood = () => {
    setCount((prevCount) => prevCount + 1);
    setCount((prevCount) => prevCount + 1);
    // Result: +2 (each sees previous update)
  };

  // Complex example: updating dependent state
  const [items, setItems] = useState<string[]>([]);
  const [totalLength, setTotalLength] = useState(0);

  // ❌ BAD: Total gets stale value
  const addItemBad = (item: string) => {
    setItems([...items, item]);
    setTotalLength(items.length + 1); // Uses closure value!
  };

  // βœ… GOOD: Derive from items or use functional update
  const addItemGood = (item: string) => {
    setItems((prevItems) => {
      const newItems = [...prevItems, item];
      setTotalLength(newItems.length); // Correct length!
      return newItems;
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementGood}>Increment</button>
      <p>Items: {items.length}</p>
      <button onClick={() => addItemGood('new')}>Add Item</button>
    </div>
  );
}

<details>

<summary>πŸ“š More Examples</summary>

// Example: Complex state with functional updates

interface AppState {
  notifications: Array<{ id: string; message: string }>;
  unreadCount: number;
  lastUpdated: Date;
}

function NotificationCenter() {
  const [state, setState] = useState<AppState>({
    notifications: [],
    unreadCount: 0,
    lastUpdated: new Date(),
  });

  // Add and update multiple values atomically
  const addNotification = (message: string) => {
    setState((prevState) => ({
      ...prevState,
      notifications: [
        ...prevState.notifications,
        { id: Date.now().toString(), message },
      ],
      unreadCount: prevState.unreadCount + 1,
      lastUpdated: new Date(),
    }));
  };

  // Conditional functional update
  const dismissNotification = (id: string) => {
    setState((prevState) => {
      const dismissed = prevState.notifications.find((n) => n.id === id);
      return {
        ...prevState,
        notifications: prevState.notifications.filter((n) => n.id !== id),
        unreadCount: Math.max(0, prevState.unreadCount - 1),
        lastUpdated: new Date(),
      };
    });
  };

  return (
    <div>
      <p>Notifications: {state.unreadCount}</p>
      <button onClick={() => addNotification('Hello!')}>Add</button>
      {state.notifications.map((n) => (
        <div key={n.id}>
          {n.message}
          <button onClick={() => dismissNotification(n.id)}>Dismiss</button>
        </div>
      ))}
    </div>
  );
}

</details>

πŸ”‘ Initialization Patterns and Performance

Expensive initialization should happen only once. Pass a function to useState to defer calculation until the first render.

If your initial state requires expensive computation (fetching, calculating, etc.), use a function. React calls this function only on the first render, not on every render.

import { useState, useMemo } from 'react';

// ❌ BAD: Heavy computation runs every render
function BadInitialization() {
  const expensiveValue = computeExpensiveValue(); // Always runs!
  const [value, setValue] = useState(expensiveValue);
  return <p>{value}</p>;
}

// βœ… GOOD: Lazy initialization
function GoodInitialization() {
  const [value, setValue] = useState(() => computeExpensiveValue());
  // Function only called once on mount
  return <p>{value}</p>;
}

// Complex initialization example
interface CachedData {
  items: string[];
  timestamp: number;
  hash: string;
}

function DataComponent() {
  const [cache, setCache] = useState<CachedData>(() => {
    // Load from localStorage, compute hash, etc.
    const stored = localStorage.getItem('cache');
    if (stored) {
      try {
        return JSON.parse(stored);
      } catch (e) {
        console.error('Invalid cache');
      }
    }
    // Compute default
    return {
      items: [],
      timestamp: Date.now(),
      hash: '',
    };
  });

  return (
    <div>
      <p>Cached items: {cache.items.length}</p>
    </div>
  );
}

πŸ“Š State Update Patterns Comparison

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pattern                 β”‚ Best For         β”‚ Pitfalls         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Direct update           β”‚ Independent vars β”‚ Stale closures   β”‚
β”‚ Functional update       β”‚ Dependent values β”‚ More verbose     β”‚
β”‚ Lazy initialization     β”‚ Heavy compute    β”‚ Forgotten func   β”‚
β”‚ Reducer pattern         β”‚ Complex state    β”‚ More boilerplate β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”‘ Key Takeaways

  • βœ… Hook Rules exist because Hooks rely on call order to track state
  • βœ… Always call Hooks at top level, never conditionally
  • βœ… Understand closures: each render captures its own state values
  • βœ… Use functional updates for dependent state changes
  • βœ… Functional updates enable proper React 18 batching
  • βœ… Use initialization functions for expensive computations
  • βœ… Combine state updates atomically to prevent inconsistencies
  • βœ… State is read-only: always create new values, never mutate directly

Ready to practice? Challenges | Next: State Updates and Batching


Resources

Python Docs

Ojasa Mirai

Master AI-powered development skills through structured learning, real projects, and verified credentials. Whether you're upskilling your team or launching your career, we deliver the skills companies actually need.

Learn Deep β€’ Build Real β€’ Verify Skills β€’ Launch Forward

Courses

PythonFastapiReactJSCloud

Β© 2026 Ojasa Mirai. All rights reserved.

TwitterGitHubLinkedIn