reactjs
/

React State Management – useState, useReducer & More

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactjs

React State Management – useState, useReducer & More

What is State in React?

State is data that changes over time and affects what gets rendered on the screen. Unlike props which are passed from parent to child, state is internal to a component and can be updated using setter functions.

useState – Local State

React JSXRead-only
1
import { useState } from 'react';

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

  // State with initializer function (for expensive computations)
  const [expensive, setExpensive] = useState(() => {
    return computeExpensiveValue();
  });

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

State with Objects

React JSXRead-only
1
function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    age: 25,
    email: 'john@example.com'
  });

  // Updating object state (must create new object)
  const updateName = (newName) => {
    setUser({ ...user, name: newName });
  };

  const updateAge = (newAge) => {
    setUser(prev => ({ ...prev, age: newAge }));
  };

  return (
    <div>
      <input value={user.name} onChange={(e) => updateName(e.target.value)} />
      <input value={user.age} onChange={(e) => updateAge(Number(e.target.value))} />
    </div>
  );
}

State with Arrays

React JSXRead-only
1
function TodoList() {
  const [todos, setTodos] = useState([]);

  // Add item
  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, completed: false }]);
  };

  // Remove item
  const removeTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // Update item
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // Update using functional update
  const updateWithPrev = () => {
    setTodos(prevTodos => [...prevTodos, newTodo]);
  };

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            {todo.text}
          </span>
          <button onClick={() => toggleTodo(todo.id)}>Toggle</button>
          <button onClick={() => removeTodo(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

useReducer – Complex State Logic

React JSXRead-only
1
import { useReducer } from 'react';

// Reducer function
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 };
    case 'SET':
      return { count: action.payload };
    default:
      return state;
  }
};

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set to 10</button>
    </div>
  );
}

Complex useReducer Example

React JSXRead-only
1
// Todo reducer
const todoReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    
    case 'DELETE_TODO':
      return state.filter(todo => todo.id !== action.payload);
    
    case 'EDIT_TODO':
      return state.map(todo =>
        todo.id === action.payload.id ? { ...todo, text: action.payload.text } : todo
      );
    
    case 'CLEAR_COMPLETED':
      return state.filter(todo => !todo.completed);
    
    default:
      return state;
  }
};

function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  const [input, setInput] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (input.trim()) {
      dispatch({ type: 'ADD_TODO', payload: input });
      setInput('');
    }
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={(e) => setInput(e.target.value)} />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}
            />
            {todo.text}
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
      <button onClick={() => dispatch({ type: 'CLEAR_COMPLETED' })}>
        Clear Completed
      </button>
    </div>
  );
}

Context API – Global State

React JSXRead-only
1
import { createContext, useContext, useState } from 'react';

// Create context
const ThemeContext = createContext();

// Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Custom hook for consuming context
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Child component
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button onClick={toggleTheme} style={{
      background: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      Current theme: {theme}
    </button>
  );
}

// App wrapper
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

State Lifting

React JSXRead-only
1
// Shared state lifted to parent
function ParentComponent() {
  const [sharedState, setSharedState] = useState('');

  return (
    <div>
      <ChildA setSharedState={setSharedState} />
      <ChildB sharedState={sharedState} />
    </div>
  );
}

function ChildA({ setSharedState }) {
  return (
    <input onChange={(e) => setSharedState(e.target.value)} />
  );
}

function ChildB({ sharedState }) {
  return (
    <p>Shared state value: {sharedState}</p>
  );
}

State Management Libraries Comparison

SolutionBest ForComplexityBundle Size
useStateSimple local stateLow0 KB (built-in)
useReducerComplex local state logicLow0 KB (built-in)
Context APIGlobal state (low frequency)Low0 KB (built-in)
ReduxLarge apps with complex stateHigh~12 KB
ZustandSimple global stateMedium~3 KB
JotaiAtomic state managementMedium~3 KB
RecoilReact-specific stateMedium~15 KB

State Update Best Practices

  • Never mutate state directly – Always use setter functions
  • Use functional updates when new state depends on previous state
  • Batch related state into a single object when updates are related
  • Keep state minimal – Derive values when possible
  • Lift state up when multiple components need access
  • Use useReducer for complex state transitions
  • Memoize selectors with useMemo to prevent unnecessary recalculations

Common Pitfalls

React JSXRead-only
1
// ❌ Wrong - Direct mutation
const [user, setUser] = useState({ name: 'John' });
user.name = 'Jane'; // Mutates directly
setUser(user); // Won't trigger re-render

// ✅ Correct - Create new object
setUser({ ...user, name: 'Jane' });

// ❌ Wrong - Stale closure
for (let i = 0; i < 5; i++) {
  setTimeout(() => setCount(count + 1), 1000); // Uses stale count
}

// ✅ Correct - Functional update
for (let i = 0; i < 5; i++) {
  setTimeout(() => setCount(prev => prev + 1), 1000);
}

// ❌ Wrong - Using index as key
{todos.map((todo, index) => <li key={index}>{todo.text}</li>)}

// ✅ Correct - Using stable id
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}

Derived State

React JSXRead-only
1
function ShoppingCart({ items }) {
  // Derived state - computed from props/state
  const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
  const hasItems = items.length > 0;
  
  // Only store what's necessary
  const [discountCode, setDiscountCode] = useState('');
  
  // Expensive derived values should be memoized
  const discountedPrice = useMemo(() => {
    return applyDiscount(totalPrice, discountCode);
  }, [totalPrice, discountCode]);
  
  return (
    <div>
      <p>Items: {itemCount}</p>
      <p>Total: ${totalPrice}</p>
      <p>Discounted: ${discountedPrice}</p>
    </div>
  );
}

Conclusion

React provides multiple tools for state management. Start with useState for local state, useReducer for complex logic, and Context API for global state. For large applications, consider dedicated state management libraries like Zustand or Redux.

Try it yourself

import { useState, useReducer } from 'react';

// Try different state management approaches

// 1. Simple useState
function SimpleCounter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h3>Simple Counter</h3>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

// 2. useReducer for complex logic
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset': return { count: 0 };
    default: return state;
  }
};

function ReducerCounter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <div>
      <h3>Reducer Counter</h3>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

// 3. Object state
function FormState() {
  const [form, setForm] = useState({ name: '', email: '' });
  
  const updateField = (field, value) => {
    setForm(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <div>
      <h3>Form State</h3>
      <input 
        placeholder="Name"
        value={form.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input 
        placeholder="Email"
        value={form.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
      <p>Name: {form.name}</p>
      <p>Email: {form.email}</p>
    </div>
  );
}

// Main app
export default function App() {
  return (
    <div>
      <SimpleCounter />
      <hr />
      <ReducerCounter />
      <hr />
      <FormState />
    </div>
  );
}

Test Your Knowledge

Q1
of 4

How do you correctly update object state?

A
object.property = newValue
B
setState({ ...object, property: newValue })
C
setState(object)
D
object = newObject
Q2
of 4

Which hook is best for complex state logic?

A
useState
B
useEffect
C
useReducer
D
useContext
Q3
of 4

What is the purpose of functional updates?

A
Better performance
B
Avoid stale closures
C
Type safety
D
Async operations
Q4
of 4

When should you lift state up?

A
Always
B
Never
C
When multiple components need same state
D
Only for class components

Frequently Asked Questions

useState vs useReducer?

useState is simpler for independent values; useReducer is better for complex state logic with multiple sub-values.

When to use Context API?

For global state that doesn't change frequently, like theme, user auth, or language settings.

Why not mutate state directly?

React relies on immutable updates to detect changes and trigger re-renders.

What is state lifting?

Moving shared state to the closest common ancestor component.

Previous

react props

Next

react events

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.