Comprehensive React Guide & Lab

React State Management

Master the complete spectrum of managing state in React: from local useState and Lifting State Up, to useReducer, React Context, Redux Toolkit (RTK), and Server Cache boundaries.

13 Deep Dive Sections Interactive Decision Lab 8 Knowledge Check Questions Production Best Practices
INTRO

Introduction to State in React

In React, State is the memory of your user interface. It represents any piece of data that can change over time based on user interactions, network responses, timers, or form inputs.

When state changes, React automatically evaluates component functions, performs virtual DOM diffing, and reconciles the real browser DOM to reflect the new state. This declarative model is summarized by the formula:

Declarative UI Formula

UI = f(State)— Your user interface is simply a pure visual projection of your application’s underlying state at any given moment.

01

What Is State Management?

State Management refers to the architectural design of how state is initialized, stored, modified, and broadcasted across components in a software application.

In a small React component, state management is trivial: a simple useState(0) handles a counter. But in real-world applications, data must flow across dozens of deeply nested components, sidebar navigation drawers, checkout screens, and user session bars.

Ephemeral

UI State

Temporary values tied strictly to visual representation (e.g. dropdown open/close, active tab, hovered button, modal visibility).

Best kept in local component useState.
Transactional

Domain / App State

Core business entities shared across screens (e.g. authenticated user profile, shopping cart items, notification queues).

Best kept in Lifted State, Context, or Store.
02

Why State Management Becomes Difficult

As applications scale from single pages to complex web applications, managing state encounters four classic friction points:

Friction #1

Prop Drilling

Passing props down 5 to 10 component levels through intermediate components that don't even need the data themselves, solely to reach a deeply nested child.

Friction #2

State Synchronization Bugs

Storing duplicate copies of data in multiple sibling components, causing them to drift out of sync when one updates and the other doesn't.

Friction #3

Interdependent State

When changing one state variable requires calculating changes in 3 other state variables (e.g. item added → subtotal recalculation → tax re-eval → shipping eligibility).

Friction #4

Over-Engineering

Introducing heavy global state managers (like raw Redux with 15 boilerplate files) for simple apps that only needed simple lifted state.

03

Where Should State Live?

The most crucial question in React design is: “Which component should own this state?”

1. Local State

If a piece of state is only used by a single component and none of its ancestors or siblings need it (e.g. an accordion open toggle or text input value), keep it local with useState.

2. Shared State & Lifting State Up

When two or more sibling components need access to the same state (for example, a ProductList adding items and a Header displaying the cart item badge), lift the state up to their closest common parent component.

App.tsx (Lifting State Up Example)
// 1. Common Parent owns the state (Single Source of Truth)
function App() {
  const [cartCount, setCartCount] = useState(0);

  return (
    <div>
      {/* 2. Pass read-only state to Header */}
      <Header count={cartCount} />
      
      {/* 3. Pass updater callback down to ProductList */}
      <ProductList onAddToCart={() => setCartCount(c => c + 1)} />
    </div>
  );
}
04

Managing Complex State With useReducer

When component state involves complex logic, multiple sub-values, or when the next state depends heavily on the previous state, useReducer provides a clean, predictable architecture inspired by Redux.

The Reducer Pattern

A Reducer is a pure function: (state, action) => newState. It takes the current state and an action object (e.g. { type: 'ADD_ITEM', payload: item }) and returns the brand-new immutable state.

cartReducer.ts
type CartAction =
  | { type: 'ADD'; payload: Product }
  | { type: 'REMOVE'; payload: { id: string } }
  | { type: 'CLEAR' };

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE':
      return { ...state, items: state.items.filter(i => i.id !== action.payload.id) };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      return state;
  }
}
05

Sharing State With Context

React Context allows you to broadcast values (like user authentication, dark/light themes, or locale preferences) to all descendants without explicitly passing props through intermediate components.

ThemeContext.tsx
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext<{ theme: string; toggle: () => void } | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState('dark');
  const toggle = () => setTheme(t => t === 'dark' ? 'light' : 'dark');

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

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) throw new Error('useTheme must be used within ThemeProvider');
  return context;
};
06

Combining Context and useReducer

Combining Context with useReducer creates a powerful, lightweight global state store with zero external dependencies.

Architectural Optimization: Separate State & Dispatch Contexts

By creating two separate contexts — CartStateContext and CartDispatchContext — components that only dispatch actions (like an "Add to Cart" button) will not re-render when the cart items change!

07

When Should You Use Global State?

Use this 3-point test before making any state global:

Test 1

Disparate Subtrees?

Is this state accessed by components located in completely different branches of the component tree (e.g. Navbar profile vs Settings page)?

Test 2

Significant Prop Drilling?

Does passing this state require threading it through more than 3-4 intermediate components that have no business knowing about it?

Test 3

Broad Lifetime?

Does the data need to survive across route transitions and page navigations (e.g. user authentication tokens, active shopping cart)?

08

Choosing the Right State Management Approach

Follow the State Decision Ladder. Move up a level only when your requirements exceed the lower level's capabilities:

LevelApproachComplexityBest Used For
1useStateLowestSelf-contained UI states (toggles, form inputs, modals)
2Lifted StateLow2-3 sibling components with a shared parent
3React ContextMediumLow-frequency global data (Theme, Auth Session, Locale)
4Context + useReducerMedium-HighMedium apps with complex state & business logic (Shopping Cart)
5Redux Toolkit / ZustandHighLarge enterprise apps with heavy cross-slice state & middleware
09

Redux and Redux Toolkit (RTK)

Redux is a predictable state container based on the Flux pattern. It enforces strict unidirectional data flow:

The Redux Lifecycle

UI Component → dispatch(action) → Reducer (pure) → Store → useSelector() → UI Re-renders

Modern Standard: Redux Toolkit (RTK)

Legacy Redux required hand-writing action types, action creators, and immutable switch statements. Redux Toolkit (@reduxjs/toolkit) simplifies this dramatically with createSlice and built-in Immer support.

cartSlice.ts (Redux Toolkit)
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface CartState {
  items: { id: string; name: string; qty: number }[];
}

const cartSlice = createSlice({
  name: 'cart',
  initialState: { items: [] } as CartState,
  reducers: {
    addItem: (state, action: PayloadAction<{ id: string; name: string }>) => {
      const item = state.items.find(i => i.id === action.payload.id);
      if (item) {
        item.qty += 1; // Immer allows direct 'mutation' syntax safely!
      } else {
        state.items.push({ ...action.payload, qty: 1 });
      }
    },
    removeItem: (state, action: PayloadAction<string>) => {
      state.items = state.items.filter(i => i.id !== action.payload);
    }
  }
});

export const { addItem, removeItem } = cartSlice.actions;
export default cartSlice.reducer;
10

State Management vs Server Data

One of the biggest shifts in modern React is recognizing that Server Data is NOT Client State.

Server Cache

Server Data / API Queries

Data that lives in the remote database. Needs caching, deduplication, polling, and background revalidation.

Use TanStack Query, SWR, or RTK Query.
Client State

Client UI State

Ephemeral data that only lives in browser memory (modal open status, dark theme, multi-step form draft).

Use useState, Context, or Zustand.
11

Common State Management Mistakes

Mistake #1

Storing Derived State

Creating const [fullName, setFullName] = useState('') when you already have firstName and lastName. Compute derived values on the fly!

Mistake #2

Direct State Mutation

Executing state.items.push(newItem) without returning a fresh array copy. React depends on reference inequality (prev !== next) to detect updates.

Mistake #3

Putting Everything in Global State

Putting every single text input and hover state into Redux/Context, causing unnecessary re-renders across the entire component tree.

12

Building a Small State-Managed React App

Below is the architectural layout of our complete Mini E-Commerce application. Notice how App orchestrates the state, allowing Header and Cart to read from the single source of truth while ProductList dispatches additions.

13

State Management Best Practices

Architectural Rules of Thumb
  • Keep state as local as possible. Only lift it when sibling components genuinely require it.
  • Never duplicate state. Derive values (e.g. total = items.reduce(...)) during render.
  • Always treat state as immutable. Use spread operators ([...prev, item]) or Immer.
  • Separate Server Cache (TanStack Query) from ephemeral Client UI State.
  • Split Context into State and Dispatch contexts to avoid re-render cascades.

🔥 Live Interactive — State Management Decision Lab

Walk through real-world architectural scenarios and test your decision-making on our interactive component tree.

Interactive Simulator
Target Component Tree Hierarchy:
App (Root Container)
Header (Badge: 1)
ProductList → Product
Cart Drawer (1 Items)

Scenario 1: Shared State Across Siblings

“Cart count Header aur Cart dono mein chahiye. State kahan rakhoge?”

Pathubs Tech Store
Cart: 1 item
Available Products (Catalog Component):
Mechanical Keyboard (RGB)$120
Wireless Ergonomic Mouse$65
4K UltraHD Monitor 27"$340
Cart Component (useReducer State):
Mechanical Keyboard x1
$120
Total :$120.00
Live useReducer Action Dispatch Stream:
> INIT: Cart initialized with 1 item
Knowledge AssessmentQuestion 1 of 8

When two sibling components (e.g., Header and Cart) both need access to cart items, what is the standard React solution?