🎣 useState Deep Dive

Master the Most Important React Hook

Why useState?

useState is the most fundamental React hook. It allows functional components to have state - making them interactive and dynamic. Understanding useState deeply is crucial for React mastery.

What You'll Learn:

  • Lazy initialization for expensive computations
  • Functional updates to avoid stale state
  • Multiple state variables vs single object
  • State batching and performance
  • Common patterns and best practices

📚 Basic Syntax Review

import { useState } from 'react';

function Counter() {
  // [currentValue, updateFunction] = useState(initialValue)
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

⚡ Lazy Initialization

When initial state requires expensive computation, use lazy initialization:

// ❌ Bad - runs expensive calculation every render
function Component() {
  const [data, setData] = useState(expensiveCalculation());
  // expensiveCalculation() runs on EVERY render!
}

// ✅ Good - calculation runs only once
function Component() {
  const [data, setData] = useState(() => expensiveCalculation());
  // Arrow function ensures it only runs on initial render
}

// Real example
function TodoList() {
  // Only parse localStorage once on mount
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });
  
  return <div>{/* render todos */}</div>;
}

When to Use Lazy Init:

  • Reading from localStorage/sessionStorage
  • Complex calculations (array processing)
  • Date/time operations
  • Any operation that takes time

🔄 Functional Updates

Avoid stale state by using the functional update form:

function Counter() {
  const [count, setCount] = useState(0);
  
  // ❌ Problem - can miss updates
  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);  // Still uses old count!
    // Result: count increases by 1, not 2
  };
  
  // ✅ Solution - functional update
  const handleClickCorrect = () => {
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
    // Result: count increases by 2
  };
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClickCorrect}>+2</button>
    </div>
  );
}

When Functional Updates Matter:

// Multiple updates in quick succession
function AsyncCounter() {
  const [count, setCount] = useState(0);
  
  const handleMultipleUpdates = () => {
    // These all execute with same count value
    setTimeout(() => setCount(count + 1), 1000);
    setTimeout(() => setCount(count + 1), 2000);
    setTimeout(() => setCount(count + 1), 3000);
    // Result: count will be 1, not 3!
  };
  
  const handleMultipleUpdatesCorrect = () => {
    // Each gets previous state
    setTimeout(() => setCount(prev => prev + 1), 1000);
    setTimeout(() => setCount(prev => prev + 1), 2000);
    setTimeout(() => setCount(prev => prev + 1), 3000);
    // Result: count will be 3 ✓
  };
  
  return (
    <button onClick={handleMultipleUpdatesCorrect}>
      Increment 3 times: {count}
    </button>
  );
}

🎨 Multiple State Variables

Approach 1: Separate State Variables

function UserProfile() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);
  const [isActive, setIsActive] = useState(false);
  
  // Pros: Simple, clear updates
  // Cons: More code, hard to reset all
  
  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="number" value={age} onChange={e => setAge(+e.target.value)} />
      <input type="checkbox" checked={isActive} onChange={e => setIsActive(e.target.checked)} />
    </div>
  );
}

Approach 2: Single State Object

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
    isActive: false
  });
  
  // Update single field (must spread existing state!)
  const updateField = (field, value) => {
    setUser(prevUser => ({
      ...prevUser,    // Keep existing fields
      [field]: value  // Update specific field
    }));
  };
  
  // Reset all at once
  const reset = () => {
    setUser({ name: '', email: '', age: 0, isActive: false });
  };
  
  // Pros: Easy to reset, related data grouped
  // Cons: Must remember to spread, more complex
  
  return (
    <div>
      <input 
        value={user.name} 
        onChange={e => updateField('name', e.target.value)} 
      />
      <input 
        value={user.email} 
        onChange={e => updateField('email', e.target.value)} 
      />
      <button onClick={reset}>Reset All</button>
    </div>
  );
}

Which Approach to Use?

  • Separate variables: Unrelated data, simple components
  • Single object: Related data (form fields), need bulk updates
  • Rule of thumb: If data changes together, group it

⚙️ State Batching

React 18+ automatically batches state updates for performance:

function BatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  console.log('Render');  // Check how many times this runs
  
  const handleClick = () => {
    setCount(c => c + 1);  // Update 1
    setFlag(f => !f);       // Update 2
    setCount(c => c + 1);  // Update 3
    
    // In React 18: All 3 updates batched → renders ONCE
    // In React 17: Would render multiple times
  };
  
  const handleAsyncClick = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // React 18: Batched even in setTimeout!
      // React 17: Would render twice
    }, 1000);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>Sync Update</button>
      <button onClick={handleAsyncClick}>Async Update</button>
    </div>
  );
}

🎯 Array State Patterns

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false },
    { id: 2, text: 'Build App', done: false }
  ]);
  
  // Add item
  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(),
      text,
      done: false
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };
  
  // Remove item
  const removeTodo = (id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  };
  
  // Update item
  const toggleTodo = (id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  };
  
  // Update specific field
  const updateTodoText = (id, newText) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, text: newText } : todo
      )
    );
  };
  
  // Sort items
  const sortTodos = () => {
    setTodos(prevTodos => [...prevTodos].sort((a, b) => 
      a.text.localeCompare(b.text)
    ));
  };
  
  // Clear completed
  const clearCompleted = () => {
    setTodos(prevTodos => prevTodos.filter(todo => !todo.done));
  };
  
  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <input 
            type="checkbox"
            checked={todo.done}
            onChange={() => toggleTodo(todo.id)}
          />
          <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </div>
      ))}
      <button onClick={sortTodos}>Sort</button>
      <button onClick={clearCompleted}>Clear Completed</button>
    </div>
  );
}

🔄 Derived State

Don't store values you can calculate - compute them during render:

// ❌ Bad - duplicating state
function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [total, setTotal] = useState(0);  // Duplicate!
  
  const addItem = (item) => {
    const newItems = [...items, item];
    setItems(newItems);
    // Must remember to update total
    setTotal(newItems.reduce((sum, i) => sum + i.price, 0));
  };
  // Problem: Easy to forget, state can get out of sync
}

// ✅ Good - derive during render
function ShoppingCart() {
  const [items, setItems] = useState([]);
  
  // Calculate on every render (cheap operation)
  const total = items.reduce((sum, item) => sum + item.price, 0);
  
  const addItem = (item) => {
    setItems(prevItems => [...prevItems, item]);
    // Total automatically updates!
  };
  
  return (
    <div>
      <p>Total: ${total}</p>
      {/* items list */}
    </div>
  );
}

More Derived State Examples:

function UserList() {
  const [users, setUsers] = useState([...]);
  const [searchTerm, setSearchTerm] = useState('');
  
  // Derived - no need to store separately
  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );
  
  const activeUserCount = users.filter(u => u.isActive).length;
  const averageAge = users.reduce((sum, u) => sum + u.age, 0) / users.length;
  
  return (
    <div>
      <input 
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
        placeholder="Search users"
      />
      <p>Active users: {activeUserCount}</p>
      <p>Average age: {averageAge.toFixed(1)}</p>
      <ul>
        {filteredUsers.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

🎯 Complete Example: Form with Validation

function RegistrationForm() {
  // Form state
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  // UI state
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  // Validation (derived)
  const errors = {};
  if (formData.username.length < 3) {
    errors.username = 'Username must be at least 3 characters';
  }
  if (!formData.email.includes('@')) {
    errors.email = 'Invalid email';
  }
  if (formData.password.length < 6) {
    errors.password = 'Password must be at least 6 characters';
  }
  if (formData.password !== formData.confirmPassword) {
    errors.confirmPassword = 'Passwords do not match';
  }
  
  // Derived state
  const isValid = Object.keys(errors).length === 0;
  const showError = (field) => touched[field] && errors[field];
  
  // Update field
  const handleChange = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };
  
  // Mark field as touched
  const handleBlur = (field) => {
    setTouched(prev => ({ ...prev, [field]: true }));
  };
  
  // Submit
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    // Mark all fields as touched
    setTouched({
      username: true,
      email: true,
      password: true,
      confirmPassword: true
    });
    
    if (!isValid) return;
    
    setIsSubmitting(true);
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      alert('Registration successful!');
      
      // Reset form
      setFormData({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
      });
      setTouched({});
    } catch (error) {
      alert('Registration failed');
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          type="text"
          value={formData.username}
          onChange={e => handleChange('username', e.target.value)}
          onBlur={() => handleBlur('username')}
          placeholder="Username"
        />
        {showError('username') && (
          <p className="error">{errors.username}</p>
        )}
      </div>
      
      <div>
        <input
          type="email"
          value={formData.email}
          onChange={e => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
          placeholder="Email"
        />
        {showError('email') && (
          <p className="error">{errors.email}</p>
        )}
      </div>
      
      <div>
        <input
          type="password"
          value={formData.password}
          onChange={e => handleChange('password', e.target.value)}
          onBlur={() => handleBlur('password')}
          placeholder="Password"
        />
        {showError('password') && (
          <p className="error">{errors.password}</p>
        )}
      </div>
      
      <div>
        <input
          type="password"
          value={formData.confirmPassword}
          onChange={e => handleChange('confirmPassword', e.target.value)}
          onBlur={() => handleBlur('confirmPassword')}
          placeholder="Confirm Password"
        />
        {showError('confirmPassword') && (
          <p className="error">{errors.confirmPassword}</p>
        )}
      </div>
      
      <button 
        type="submit" 
        disabled={isSubmitting || !isValid}
      >
        {isSubmitting ? 'Registering...' : 'Register'}
      </button>
    </form>
  );
}

⚠️ Common Mistakes

1. Mutating State Directly

// ❌ Wrong
const [user, setUser] = useState({ name: 'John', age: 30 });
user.age = 31;  // Mutates state directly!
setUser(user);  // React won't detect change

// ✅ Correct
setUser({ ...user, age: 31 });

2. Not Using Functional Updates

// ❌ Can miss updates
setCount(count + 1);

// ✅ Always gets latest
setCount(prev => prev + 1);

3. Too Many State Variables

// ❌ Too fragmented
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [street, setStreet] = useState('');
const [city, setCity] = useState('');
const [zip, setZip] = useState('');

// ✅ Better - group related data
const [user, setUser] = useState({
  name: { first: '', last: '' },
  address: { street: '', city: '', zip: '' }
});

🎯 Key Takeaways