Understanding State Management

As React applications grow, managing state becomes more complex. You need to decide where to store state, how to share it between components, and how to keep it synchronized. This page covers various approaches to state management in React applications.

🤔 When Do You Need State Management?

  • Prop Drilling: Passing props through many component levels
  • Shared State: Multiple components need the same data
  • Complex Updates: State changes affect multiple parts of the app
  • Global Features: User authentication, themes, language settings

Local vs Global State

Local State

✅ Use Local State For:

  • Form input values
  • Toggle states (show/hide)
  • Component-specific data
  • Temporary UI states
Local State Example
function LoginForm() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [showPassword, setShowPassword] = useState(false);
  const [errors, setErrors] = useState({});

  const handleSubmit = (e) => {
    e.preventDefault();
    // Validate and submit
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type={showPassword ? 'text' : 'password'}
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button 
        type="button"
        onClick={() => setShowPassword(!showPassword)}
      >
        {showPassword ? 'Hide' : 'Show'} Password
      </button>
      <button type="submit">Login</button>
    </form>
  );
}
Global State

🌐 Use Global State For:

  • User authentication
  • Theme preferences
  • Shopping cart contents
  • App-wide settings
Global State Example
// Context for global state
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [cart, setCart] = useState([]);

  const value = {
    user,
    setUser,
    theme,
    setTheme,
    cart,
    setCart
  };

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// Any component can access global state
function Header() {
  const { user, theme, cart } = useContext(AppContext);
  
  return (
    <header className={theme}>
      {user ? `Welcome ${user.name}` : 'Please login'}
      <CartIcon count={cart.length} />
    </header>
  );
}

Context API - React's Built-in Solution

React's Context API provides a way to pass data through the component tree without having to pass props down manually at every level. It's perfect for global state like themes, user authentication, and app settings.

🏗️ Building a Complete Context System

Complete Context Example
// 1. Create Context and Custom Hook
import { createContext, useContext, useReducer } from 'react';

const AppContext = createContext();

// Custom hook to use the context
export const useApp = () => {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
};

// 2. Reducer for complex state logic
const appReducer = (state, action) => {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    
    case 'ADD_TO_CART':
      return {
        ...state,
        cart: [...state.cart, action.payload]
      };
    
    case 'REMOVE_FROM_CART':
      return {
        ...state,
        cart: state.cart.filter(item => item.id !== action.payload)
      };
    
    case 'CLEAR_CART':
      return { ...state, cart: [] };
    
    case 'SET_LOADING':
      return { ...state, loading: action.payload };
    
    case 'SET_ERROR':
      return { ...state, error: action.payload };
    
    default:
      return state;
  }
};

// 3. Provider Component
export function AppProvider({ children }) {
  const initialState = {
    user: null,
    theme: 'light',
    cart: [],
    loading: false,
    error: null
  };

  const [state, dispatch] = useReducer(appReducer, initialState);

  // Action creators
  const actions = {
    setUser: (user) => dispatch({ type: 'SET_USER', payload: user }),
    setTheme: (theme) => dispatch({ type: 'SET_THEME', payload: theme }),
    addToCart: (item) => dispatch({ type: 'ADD_TO_CART', payload: item }),
    removeFromCart: (id) => dispatch({ type: 'REMOVE_FROM_CART', payload: id }),
    clearCart: () => dispatch({ type: 'CLEAR_CART' }),
    setLoading: (loading) => dispatch({ type: 'SET_LOADING', payload: loading }),
    setError: (error) => dispatch({ type: 'SET_ERROR', payload: error })
  };

  const value = {
    ...state,
    ...actions
  };

  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// 4. Using the Context in Components
function Header() {
  const { user, theme, cart, setTheme } = useApp();

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

  return (
    <header className={`header ${theme}`}>
      <h1>My Store</h1>
      
      {user ? (
        <div>Welcome, {user.name}!</div>
      ) : (
        <LoginButton />
      )}
      
      <button onClick={toggleTheme}>
        Switch to {theme === 'light' ? 'dark' : 'light'} mode
      </button>
      
      <CartIcon count={cart.length} />
    </header>
  );
}

function ProductCard({ product }) {
  const { addToCart } = useApp();

  const handleAddToCart = () => {
    addToCart({
      id: product.id,
      name: product.name,
      price: product.price,
      quantity: 1
    });
  };

  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={handleAddToCart}>
        Add to Cart
      </button>
    </div>
  );
}

// 5. App Setup
function App() {
  return (
    <AppProvider>
      <Header />
      <ProductList />
      <Cart />
    </AppProvider>
  );
}

Redux - Predictable State Container

Redux is a popular state management library that implements a unidirectional data flow pattern. While not always necessary, Redux shines in large applications with complex state interactions.

🔄 Redux Core Concepts

🏪 Store

Single source of truth that holds the entire state of your application.

📋 Actions

Plain objects that describe what happened. They have a type and optional payload.

🔧 Reducers

Pure functions that specify how state changes in response to actions.

📡 Dispatch

Function used to send actions to the store to trigger state changes.

Redux Pattern (Conceptual)
// 1. Actions - What happened
const actions = {
  increment: () => ({ type: 'INCREMENT' }),
  decrement: () => ({ type: 'DECREMENT' }),
  setCount: (count) => ({ type: 'SET_COUNT', payload: count }),
  addTodo: (text) => ({ 
    type: 'ADD_TODO', 
    payload: { id: Date.now(), text, completed: false }
  })
};

// 2. Reducers - How state changes
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'SET_COUNT':
      return { ...state, count: action.payload };
    default:
      return state;
  }
};

const todosReducer = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, action.payload];
    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);
    default:
      return state;
  }
};

// 3. Combine Reducers
const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todosReducer
});

// 4. Create Store
const store = createStore(rootReducer);

// 5. Usage in Components (with React-Redux)
function Counter() {
  const count = useSelector(state => state.counter.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(actions.increment())}>
        +
      </button>
      <button onClick={() => dispatch(actions.decrement())}>
        -
      </button>
    </div>
  );
}

Modern State Management Alternatives

🔮 Zustand

Small, fast, and scalable state management solution.

Zustand Example
import { create } from 'zustand'

const useStore = create((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
}))

function Counter() {
  const { count, increment, decrement } = useStore()
  return (
    <div>
      <p>{count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  )
}

⚛️ Jotai

Atomic approach to React state management.

Jotai Example
import { atom, useAtom } from 'jotai'

const countAtom = atom(0)

function Counter() {
  const [count, setCount] = useAtom(countAtom)
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>
        +
      </button>
    </div>
  )
}

🌊 Valtio

Proxy-based state management for React.

Valtio Example
import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0 })

function Counter() {
  const snap = useSnapshot(state)
  
  return (
    <div>
      <p>{snap.count}</p>
      <button onClick={() => ++state.count}>
        +
      </button>
    </div>
  )
}

📊 Redux Toolkit

Modern Redux with less boilerplate.

Redux Toolkit
import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    }
  }
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer

Choosing the Right State Management Solution

🤔 Decision Matrix

Simple Apps

✅ Use:

  • useState for local component state
  • useContext for 2-3 global values
  • Props for parent-child communication

📝 Characteristics:

  • Few shared state values
  • Simple data flow
  • Small development team
Complex Apps

✅ Consider:

  • Redux Toolkit for predictable updates
  • Zustand for simplicity + power
  • Jotai for atomic state

📝 Characteristics:

  • Complex state interactions
  • Time-travel debugging needs
  • Large development team

State Management Best Practices

✅ General Guidelines

  • Start with local state
  • Lift state up when needed
  • Use Context for truly global state
  • Keep state as close to usage as possible
  • Normalize complex state structures

🏗️ Architecture Tips

  • Separate UI state from server state
  • Use custom hooks for state logic
  • Implement error boundaries
  • Consider using React Query for server state
  • Keep actions and reducers pure

⚡ Performance

  • Avoid large Context values
  • Split Context by update frequency
  • Use selectors in Redux
  • Memoize expensive calculations
  • Implement proper loading states

🐛 Testing

  • Test reducers in isolation
  • Mock Context providers
  • Test integration with components
  • Use testing utilities like @testing-library
  • Test error scenarios

What's Next?

You now understand the various approaches to state management in React applications. Next, you'll learn about routing to build single-page applications with multiple views.

🚀 Continue Learning

  1. Routing - Building single-page applications with React Router
  2. Development Tools - Essential tools for React development
  3. Next.js - Full-stack React framework
  4. Transition Guide - Complete migration strategies