How to update state in React

Updating state properly in React is fundamental for creating responsive and predictable user interfaces that trigger re-renders when data changes. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented state updates in thousands of React components for form handling, data manipulation, user interactions, and complex state management across enterprise applications. From my expertise, the most effective approach is to use the setState function with immutability patterns. This method ensures proper React re-renders, maintains state history for debugging, and prevents common bugs related to state mutations while optimizing component performance.

Use the setState function with immutability patterns to properly update React state and trigger re-renders.

import { useState } from 'react'

function StateUpdateExamples() {
  // Primitive state updates
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')
  const [isVisible, setIsVisible] = useState(false)

  // Object state updates
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
    preferences: {
      theme: 'light',
      notifications: true
    }
  })

  // Array state updates
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1', completed: false },
    { id: 2, text: 'Item 2', completed: true }
  ])

  // 1. Simple state updates
  const incrementCount = () => {
    setCount(count + 1) // Direct update
  }

  const incrementCountSafely = () => {
    setCount(prevCount => prevCount + 1) // Functional update (safer)
  }

  const toggleVisibility = () => {
    setIsVisible(!isVisible)
  }

  // 2. Object state updates (immutable)
  const updateUserName = (newName) => {
    setUser({
      ...user,
      name: newName
    })
  }

  const updateUserEmail = (newEmail) => {
    setUser(prevUser => ({
      ...prevUser,
      email: newEmail
    }))
  }

  // 3. Nested object updates
  const updateUserTheme = (newTheme) => {
    setUser(prevUser => ({
      ...prevUser,
      preferences: {
        ...prevUser.preferences,
        theme: newTheme
      }
    }))
  }

  // 4. Array state updates
  const addItem = (text) => {
    const newItem = {
      id: Date.now(),
      text,
      completed: false
    }
    setItems([...items, newItem]) // Add to end
    // setItems([newItem, ...items]) // Add to beginning
  }

  const removeItem = (id) => {
    setItems(items.filter(item => item.id !== id))
  }

  const updateItem = (id, updates) => {
    setItems(items.map(item =>
      item.id === id
        ? { ...item, ...updates }
        : item
    ))
  }

  const toggleItemCompleted = (id) => {
    setItems(prevItems =>
      prevItems.map(item =>
        item.id === id
          ? { ...item, completed: !item.completed }
          : item
      )
    )
  }

  // 5. Complex state updates with reducer pattern
  const updateUserComplex = (field, value) => {
    setUser(prevUser => {
      if (field.includes('.')) {
        // Handle nested fields like 'preferences.theme'
        const [parentField, childField] = field.split('.')
        return {
          ...prevUser,
          [parentField]: {
            ...prevUser[parentField],
            [childField]: value
          }
        }
      }

      return {
        ...prevUser,
        [field]: value
      }
    })
  }

  // 6. Batch state updates
  const resetAll = () => {
    setCount(0)
    setName('')
    setIsVisible(false)
    setUser({
      name: '',
      email: '',
      age: 0,
      preferences: {
        theme: 'light',
        notifications: true
      }
    })
    setItems([])
  }

  // 7. Conditional state updates
  const conditionalUpdate = () => {
    if (count < 10) {
      setCount(prevCount => prevCount + 1)
    } else {
      setCount(0) // Reset if over limit
    }
  }

  return (
    <div>
      <h2>State Update Examples</h2>

      {/* Primitive state */}
      <div>
        <h3>Count: {count}</h3>
        <button onClick={incrementCount}>Increment</button>
        <button onClick={incrementCountSafely}>Increment Safely</button>
        <button onClick={conditionalUpdate}>Conditional Update</button>
      </div>

      {/* Object state */}
      <div>
        <h3>User Info</h3>
        <input
          type="text"
          placeholder="Name"
          value={user.name}
          onChange={(e) => updateUserName(e.target.value)}
        />
        <input
          type="email"
          placeholder="Email"
          value={user.email}
          onChange={(e) => updateUserEmail(e.target.value)}
        />
        <select
          value={user.preferences.theme}
          onChange={(e) => updateUserTheme(e.target.value)}
        >
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
        <button onClick={() => updateUserComplex('preferences.notifications', !user.preferences.notifications)}>
          Toggle Notifications: {user.preferences.notifications ? 'On' : 'Off'}
        </button>
      </div>

      {/* Array state */}
      <div>
        <h3>Todo Items</h3>
        <button onClick={() => addItem(`Item ${items.length + 1}`)}>
          Add Item
        </button>
        <ul>
          {items.map(item => (
            <li key={item.id}>
              <span style={{ textDecoration: item.completed ? 'line-through' : 'none' }}>
                {item.text}
              </span>
              <button onClick={() => toggleItemCompleted(item.id)}>
                {item.completed ? 'Undo' : 'Complete'}
              </button>
              <button onClick={() => removeItem(item.id)}>Delete</button>
            </li>
          ))}
        </ul>
      </div>

      {/* Reset all */}
      <button onClick={resetAll} style={{ marginTop: '20px' }}>
        Reset All State
      </button>
    </div>
  )
}

// Common patterns and utilities
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)

  const toggle = () => setValue(prev => !prev)
  const setTrue = () => setValue(true)
  const setFalse = () => setValue(false)

  return [value, { toggle, setTrue, setFalse }]
}

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue)

  const increment = () => setCount(prev => prev + 1)
  const decrement = () => setCount(prev => prev - 1)
  const reset = () => setCount(initialValue)

  return [count, { increment, decrement, reset }]
}

export default StateUpdateExamples

State updates in React must be immutable - never modify the existing state directly. Use the spread operator (...) to create new objects and arrays. For functional updates, pass a function to setState that receives the previous state, which is safer for updates that depend on the current state. When updating nested objects, spread each level of nesting. For arrays, use methods like map, filter, and spread operator instead of mutating methods like push or splice. React batches multiple setState calls in event handlers for performance.

Best Practice Note:

This is the same approach we use in CoreUI React components for reliable state management and optimal re-rendering performance. Always use immutable updates, prefer functional updates when the new state depends on the previous state, use custom hooks for complex state logic, avoid directly mutating state objects or arrays, and consider useReducer for complex state scenarios with multiple related updates.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author