1. useState Hook Fundamentals and Patterns

1.1 useState Syntax and State Declaration

Syntax Description Return Value Use Case
useState(initialValue) Declares state variable with initial value [state, setState] array Basic state management for primitives and objects
const [value, setValue] Array destructuring to extract state and setter State variable and updater function Naming convention: [noun, setNoun]
Multiple useState calls Each call creates independent state variable Separate state slices with own setters Split state by concern for better organization
State Type Declaration Initial Value TypeScript Type
Primitive useState(0) Number, string, boolean, null const [count, setCount] = useState<number>(0)
Object useState({name: ''}) Object literal useState<User>({name: '', age: 0})
Array useState([]) Empty or populated array useState<string[]>([])
Nullable useState(null) null or undefined useState<User | null>(null)

Example: Basic useState declarations

import { useState } from 'react';

function Counter() {
  // Primitive state
  const [count, setCount] = useState(0);
  
  // Object state
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  // Array state
  const [items, setItems] = useState(['a', 'b', 'c']);
  
  // Boolean state
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

1.2 Functional State Updates and Updater Functions

Pattern Syntax When to Use Advantage
Direct Update setState(newValue) New value doesn't depend on previous state Simple, straightforward
Functional Update RECOMMENDED setState(prev => newValue) New value depends on previous state Prevents stale closure issues, guarantees latest state
Updater with Logic setState(prev => prev + delta) Calculations based on current state Safe for async updates and batching
Scenario ❌ Problematic ✅ Correct
Multiple Updates setCount(count + 1);
setCount(count + 1);
setCount(c => c + 1);
setCount(c => c + 1);
Event Handler onClick={() => setCount(count + 1)} onClick={() => setCount(c => c + 1)}
Async Callback setTimeout(() => setCount(count + 1), 1000) setTimeout(() => setCount(c => c + 1), 1000)

Example: Functional updates prevent stale closures

// ❌ Problem: Both clicks only increment once total
function BadCounter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1); // Uses stale 'count'
    setCount(count + 1); // Uses same stale 'count'
  };
  
  return <button onClick={handleClick}>{count}</button>;
}

// ✅ Solution: Functional updates use latest state
function GoodCounter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(c => c + 1); // Uses latest state
    setCount(c => c + 1); // Uses result of previous update
  };
  
  return <button onClick={handleClick}>{count}</button>; // Increments by 2
}
Best Practice: Always use functional updates when the new state depends on the previous state, especially in event handlers, async operations, and when multiple updates may be batched.

1.3 Lazy State Initialization with Functions

Pattern Syntax Execution Performance Impact
Direct Initialization useState(expensiveCalc()) Runs on every render ❌ Wasteful - recalculates unnecessarily
Lazy Initialization RECOMMENDED useState(() => expensiveCalc()) Runs only once on mount ✅ Optimized - calculates only when needed
Lazy with Dependencies useState(() => computeFromProps(props)) Once on mount with prop values ✅ Efficient initial state from props
Use Case Example Why Lazy Init
localStorage Read useState(() => JSON.parse(localStorage.getItem('key'))) Avoid reading storage on every render
Complex Calculation useState(() => expensiveDataProcessing(data)) Defer expensive computation to mount only
Date/Time useState(() => new Date()) Capture exact mount time, not re-render time
Random Value useState(() => Math.random()) Ensure consistent initial random value

Example: Lazy initialization for expensive operations

// ❌ Bad: Reads localStorage on EVERY render (wasteful)
function BadComponent() {
  const [data, setData] = useState(
    JSON.parse(localStorage.getItem('userData') || '{}')
  );
  // This executes on every render, even though result is ignored after mount
}

// ✅ Good: Reads localStorage ONLY on initial mount
function GoodComponent() {
  const [data, setData] = useState(() => {
    const saved = localStorage.getItem('userData');
    return saved ? JSON.parse(saved) : {};
  });
  // Function executes once, subsequent renders skip it
}

// ✅ Good: Expensive calculation done only once
function DataProcessor({ rawData }) {
  const [processed, setProcessed] = useState(() => {
    console.log('Processing...'); // Logs only once
    return expensiveProcessing(rawData);
  });
}
Warning: Lazy initializer function runs only once on mount. It won't re-run if props change. For derived state from props, use useMemo or useEffect instead.

1.4 State Update Batching and React 18 Automatic Batching

React Version Batching Scope Behavior
React 17 Event handlers only Batches updates in synthetic events, but not in setTimeout, promises, native events
React 18+ NEW All updates everywhere Automatic batching in timeouts, promises, native events, and event handlers
Context React 17 Batching React 18 Batching Re-renders
Event Handler ✅ Batched ✅ Batched 1 re-render for multiple setState calls
setTimeout ❌ Not batched ✅ Batched React 18: 1 re-render; React 17: multiple
Promise.then ❌ Not batched ✅ Batched React 18: 1 re-render; React 17: multiple
Native Event ❌ Not batched ✅ Batched React 18: 1 re-render; React 17: multiple
API Purpose Usage
flushSync() Force immediate synchronous update, opt-out of batching flushSync(() => setState(value))
ReactDOM.unstable_batchedUpdates() Manual batching in React 17 (deprecated in React 18) unstable_batchedUpdates(() => { setState1(); setState2(); })

Example: Automatic batching in React 18

import { useState } from 'react';
import { flushSync } from 'react-dom';

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  console.log('Render'); // Track re-renders
  
  // React 18: Both updates batched → 1 render
  // React 17: Both updates batched → 1 render
  function handleClick() {
    setCount(c => c + 1);
    setFlag(f => !f);
    // Single re-render after both updates
  }
  
  // React 18: Both updates batched → 1 render
  // React 17: Each update causes render → 2 renders
  function handleAsync() {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // React 18: Single re-render
    }, 1000);
  }
  
  // React 18: Both updates batched → 1 render
  // React 17: Each update causes render → 2 renders
  function handlePromise() {
    fetch('/api').then(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // React 18: Single re-render
    });
  }
  
  // Opt-out: Force immediate synchronous updates
  function handleFlushSync() {
    flushSync(() => {
      setCount(c => c + 1); // Renders immediately
    });
    flushSync(() => {
      setFlag(f => !f); // Renders immediately (separate)
    });
    // Two separate re-renders
  }
}
React 18 Benefit: Automatic batching improves performance by reducing re-renders. Multiple state updates in any context are batched into a single re-render, leading to better performance and more consistent behavior.

1.5 Complex State Objects and State Merging

Pattern Behavior Syntax Note
useState (Replaces) Replaces entire state object setState(newObject) ❌ No automatic merging - must spread manually
Class setState (Merges) Merges with existing state this.setState({key: value}) ⚠️ Legacy - not available in hooks
Manual Merge Spread existing state + new values setState(prev => ({...prev, key: value})) ✅ Recommended pattern for object state
Update Pattern Code Result
Single Property setUser(prev => ({...prev, name: 'Jane'})) Updates name, preserves other properties
Multiple Properties setUser(prev => ({...prev, name: 'Jane', age: 25})) Updates multiple fields at once
Nested Object setUser(prev => ({...prev, address: {...prev.address, city: 'NYC'}})) Update nested property immutably
Computed Property setUser(prev => ({...prev, [key]: value})) Dynamic key based on variable

Example: Object state updates and merging

function UserForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
    address: { city: '', country: '' }
  });
  
  // ❌ Wrong: Loses all other properties
  const updateNameBad = (name) => {
    setUser({ name }); // user now only has 'name', lost email, age, address
  };
  
  // ✅ Correct: Spreads existing state, updates one field
  const updateName = (name) => {
    setUser(prev => ({ ...prev, name }));
  };
  
  // ✅ Multiple fields at once
  const updateMultiple = () => {
    setUser(prev => ({
      ...prev,
      name: 'John',
      age: 30
    }));
  };
  
  // ✅ Nested object update
  const updateCity = (city) => {
    setUser(prev => ({
      ...prev,
      address: { ...prev.address, city }
    }));
  };
  
  // ✅ Dynamic property name
  const updateField = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };
  
  return (
    <form>
      <input value={user.name} onChange={e => updateName(e.target.value)} />
      <input value={user.email} onChange={e => updateField('email', e.target.value)} />
    </form>
  );
}
Warning: useState does NOT merge objects automatically like class component setState. Always use the spread operator {...prev, ...changes} to preserve unmodified properties.

1.6 State Reset Patterns and Key Changes

Reset Method Implementation Use Case
setState to Initial setState(initialValue) Simple reset to known initial state
Key Prop Change RECOMMENDED <Component key={id} /> Force complete component remount with fresh state
useEffect Reset useEffect(() => setState(initial), [dependency]) Reset state when dependency changes
Reset Function const reset = () => setState(INITIAL_STATE) Reusable reset handler
Pattern Code Example Behavior
Constant Initial State const INITIAL = { name: '', age: 0 };
useState(INITIAL);
Reference constant for resets: setState(INITIAL)
Factory Function const createInitial = () => ({name: '', age: 0});
useState(createInitial);
Reset with: setState(createInitial())
Key-based Reset <Form key={userId} /> Changing userId unmounts and remounts Form with fresh state

Example: State reset patterns

const INITIAL_FORM = { name: '', email: '', age: 0 };

function Form() {
  const [form, setForm] = useState(INITIAL_FORM);
  
  // Method 1: Direct reset to constant
  const handleReset = () => {
    setForm(INITIAL_FORM);
  };
  
  // Method 2: Factory function for reset
  const createInitialState = () => ({
    name: '',
    email: '',
    age: 0,
    timestamp: Date.now() // Fresh timestamp on each reset
  });
  
  const [data, setData] = useState(createInitialState);
  
  const handleResetWithFactory = () => {
    setData(createInitialState());
  };
  
  return (
    <form>
      <input value={form.name} onChange={e => setForm(prev => ({...prev, name: e.target.value}))} />
      <button type="button" onClick={handleReset}>Reset</button>
    </form>
  );
}

// Method 3: Key-based reset (component remounts)
function UserProfile({ userId }) {
  return <ProfileForm key={userId} />; // New userId = fresh state
}

function ProfileForm() {
  const [name, setName] = useState(''); // Resets when key changes
  const [email, setEmail] = useState('');
  
  // State automatically resets when parent's key prop changes
}

Example: Reset state when prop changes

// Pattern: Reset internal state when prop changes
function EditableItem({ item }) {
  const [localValue, setLocalValue] = useState(item.value);
  
  // Reset local state when item prop changes
  useEffect(() => {
    setLocalValue(item.value);
  }, [item.value]);
  
  return (
    <input
      value={localValue}
      onChange={e => setLocalValue(e.target.value)}
    />
  );
}

// Alternative: Use key to force remount
function ItemList({ items }) {
  return items.map(item => (
    <EditableItem key={item.id} item={item} />
  ));
}

useState Best Practices Summary

  • Use functional updates when new state depends on previous state
  • Apply lazy initialization for expensive initial calculations
  • Leverage automatic batching in React 18+ for better performance
  • Always spread objects when updating - useState doesn't merge
  • Use key prop to reset component state completely
  • Store initial state in constants for easy resets

2. useReducer Hook for Complex State Logic

2.1 useReducer Hook Syntax and Dispatch Patterns

Component Syntax Description Usage
useReducer Hook useReducer(reducer, initialState) Returns [state, dispatch] array Alternative to useState for complex state logic
With Init Function useReducer(reducer, initialArg, init) Third argument initializes state lazily Lazy initialization: init(initialArg) called once
Dispatch Function dispatch(action) Triggers state update via reducer function Stable reference - safe for useEffect dependencies
Dispatch Pattern Syntax Action Structure Use Case
Type Only dispatch({ type: 'INCREMENT' }) Action with type, no payload Simple state transitions without data
Type + Payload dispatch({ type: 'SET_VALUE', payload: value }) Action with type and data Most common - pass data to reducer
Spread Payload dispatch({ type: 'UPDATE', ...data }) Flatten payload into action object Multiple values: { type, id, name, age }
FSA (Flux Standard Action) dispatch({ type, payload, error, meta }) Standardized action shape Industry standard for Redux-style actions

Example: Basic useReducer setup and dispatch

import { useReducer } from 'react';

// Reducer function
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'SET':
      return { count: action.payload };
    case 'RESET':
      return { count: 0 };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

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: 'SET', payload: 10 })}>Set 10</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
    </div>
  );
}

Example: Lazy initialization with init function

// Init function for lazy initialization
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function CounterWithHistory({ initialCount }) {
  // Third argument: init function called once with initialCount
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  
  // state = { count: initialCount, history: [] }
}

2.2 Reducer Function Patterns and Action Types

Reducer Pattern Implementation Pros Cons
Switch Statement switch(action.type) { case 'TYPE': return newState; } Clear, explicit, easy to read Verbose for many action types
Object Lookup const handlers = {TYPE: (state) => newState}; return handlers[action.type]?.(state) Concise, functional style Less explicit error handling
If-Else Chain if (type === 'A') return ...; else if (type === 'B') return ...; Simple for few actions Poor scalability, hard to maintain
Action Type Convention Format Example Use Case
UPPER_SNAKE_CASE 'ACTION_NAME' 'FETCH_USER', 'UPDATE_PROFILE' Traditional Redux style, clear constants
Namespaced 'domain/ACTION' 'user/FETCH', 'cart/ADD_ITEM' Organize by feature, prevent collisions
Enum/Constants const ACTIONS = {ADD: 'ADD'} ACTIONS.ADD, ACTIONS.REMOVE Type safety, autocomplete, refactoring

Example: Reducer patterns comparison

// Pattern 1: Switch statement (most common)
function reducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      return state; // Or throw error for unknown actions
  }
}

// Pattern 2: Object lookup (functional style)
const handlers = {
  ADD_ITEM: (state, action) => ({
    ...state,
    items: [...state.items, action.payload]
  }),
  REMOVE_ITEM: (state, action) => ({
    ...state,
    items: state.items.filter(i => i.id !== action.payload)
  }),
  CLEAR: (state) => ({ ...state, items: [] })
};

function reducer(state, action) {
  const handler = handlers[action.type];
  return handler ? handler(state, action) : state;
}

// Pattern 3: Action type constants (type safety)
const ACTIONS = {
  ADD_ITEM: 'ADD_ITEM',
  REMOVE_ITEM: 'REMOVE_ITEM',
  CLEAR: 'CLEAR'
} as const;

function reducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_ITEM:
      return { ...state, items: [...state.items, action.payload] };
    case ACTIONS.REMOVE_ITEM:
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    case ACTIONS.CLEAR:
      return { ...state, items: [] };
    default:
      return state;
  }
}
Best Practice: Always return a new state object. Never mutate the existing state. Use spread operators or Immer for immutable updates.

2.3 Action Creators and Payload Structures

Pattern Implementation Benefit
Inline Action dispatch({ type: 'ADD', payload: item }) Simple, direct - good for one-off actions
Action Creator RECOMMENDED const addItem = (item) => ({ type: 'ADD', payload: item }) Reusable, testable, consistent structure
Typed Action Creator const addItem = (item: Item): Action => ({ type: 'ADD', payload: item }) Type safety with TypeScript
Payload Structure Example Use Case
Single Value { type: 'SET_NAME', payload: 'John' } Simple value updates
Object Payload { type: 'UPDATE_USER', payload: { name, age, email } } Multiple related values
Indexed Payload { type: 'UPDATE_ITEM', payload: { id: 1, changes: {...} } } Update specific item in collection
Normalized Payload { type: 'ADD_MANY', payload: { byId: {...}, allIds: [...] } } Normalized data structures

Example: Action creators for cleaner code

// Action type constants
const ACTIONS = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER'
};

// Action creators
const addTodo = (text) => ({
  type: ACTIONS.ADD_TODO,
  payload: { id: Date.now(), text, completed: false }
});

const toggleTodo = (id) => ({
  type: ACTIONS.TOGGLE_TODO,
  payload: id
});

const deleteTodo = (id) => ({
  type: ACTIONS.DELETE_TODO,
  payload: id
});

const setFilter = (filter) => ({
  type: ACTIONS.SET_FILTER,
  payload: filter
});

// Usage in component
function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  const handleAdd = (text) => {
    dispatch(addTodo(text)); // Clean, readable
  };
  
  const handleToggle = (id) => {
    dispatch(toggleTodo(id)); // Clear intent
  };
  
  return (
    <div>
      <button onClick={() => handleAdd('New task')}>Add</button>
      <button onClick={() => handleToggle(1)}>Toggle</button>
    </div>
  );
}

Example: TypeScript action creators with discriminated unions

// Define action types with discriminated union
type Action =
  | { type: 'ADD_TODO'; payload: { text: string } }
  | { type: 'TOGGLE_TODO'; payload: number }
  | { type: 'DELETE_TODO'; payload: number }
  | { type: 'SET_FILTER'; payload: 'all' | 'active' | 'completed' };

// Type-safe action creators
const actions = {
  addTodo: (text: string): Action => ({
    type: 'ADD_TODO',
    payload: { text }
  }),
  toggleTodo: (id: number): Action => ({
    type: 'TOGGLE_TODO',
    payload: id
  }),
  deleteTodo: (id: number): Action => ({
    type: 'DELETE_TODO',
    payload: id
  }),
  setFilter: (filter: 'all' | 'active' | 'completed'): Action => ({
    type: 'SET_FILTER',
    payload: filter
  })
};

// Reducer with full type inference
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD_TODO':
      // TypeScript knows action.payload is { text: string }
      return { ...state, todos: [...state.todos, { id: Date.now(), ...action.payload, completed: false }] };
    case 'TOGGLE_TODO':
      // TypeScript knows action.payload is number
      return { ...state, todos: state.todos.map(t => t.id === action.payload ? { ...t, completed: !t.completed } : t) };
    default:
      return state;
  }
}

2.4 State Machine Patterns with useReducer

State Machine Concept Description Benefit
Finite States Limited set of possible states (idle, loading, success, error) Prevents impossible states, clearer logic
Transitions Explicit rules for moving between states Predictable state changes, easier debugging
Guards Conditions that must be met for transitions Validation logic, prevent invalid transitions
State Machine Pattern State Structure Use Case
Status Enum { status: 'idle' | 'loading' | 'success' | 'error', data?, error? } Async operations, API calls
Tagged Union { type: 'loading' } | { type: 'success', data } | { type: 'error', error } Type-safe states with TypeScript
Nested States { phase: 'editing', editMode: { step: 1 | 2 | 3 } } Multi-step processes, wizards

Example: State machine for async data fetching

// State type with discriminated union
type State =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; payload: string }
  | { type: 'RESET' };

function reducer(state: State, action: Action): State {
  switch (state.status) {
    case 'idle':
      if (action.type === 'FETCH_START') {
        return { status: 'loading' };
      }
      break;
    
    case 'loading':
      if (action.type === 'FETCH_SUCCESS') {
        return { status: 'success', data: action.payload };
      }
      if (action.type === 'FETCH_ERROR') {
        return { status: 'error', error: action.payload };
      }
      break;
    
    case 'success':
    case 'error':
      if (action.type === 'FETCH_START') {
        return { status: 'loading' };
      }
      if (action.type === 'RESET') {
        return { status: 'idle' };
      }
      break;
  }
  
  return state; // No valid transition
}

function UserList() {
  const [state, dispatch] = useReducer(reducer, { status: 'idle' });
  
  const fetchUsers = async () => {
    dispatch({ type: 'FETCH_START' });
    try {
      const data = await fetch('/api/users').then(r => r.json());
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };
  
  if (state.status === 'loading') return <div>Loading...</div>;
  if (state.status === 'error') return <div>Error: {state.error}</div>;
  if (state.status === 'success') return <div>{state.data.length} users</div>;
  return <button onClick={fetchUsers}>Load Users</button>;
}
Impossible States: State machines prevent scenarios like { loading: true, error: 'Failed', data: [...] } which are logically impossible but possible with boolean flags.

2.5 useReducer vs useState Decision Matrix

Criterion Use useState Use useReducer
State Complexity Simple primitives or shallow objects Complex nested objects, multiple related values
Update Logic Simple assignments: setState(value) Complex logic, conditionals, multiple steps
Number of Updates Few state update locations Many different update operations
Predictability Simple, local state changes Need centralized, traceable state transitions
Testing Simple component tests sufficient Want to test reducer logic in isolation
Future Changes State unlikely to grow in complexity Anticipate adding more actions/transitions
Scenario Best Choice Reason
Toggle boolean flag ✅ useState Simple on/off state, no complex logic
Form with validation ✅ useReducer Multiple fields, validation rules, error states
Shopping cart ✅ useReducer Add/remove/update/clear operations, quantities
Modal open/close ✅ useState Binary state, simple toggle
Multi-step wizard ✅ useReducer State machine with steps, navigation, data
Counter ✅ useState Single value, simple increment/decrement
Async data fetching ✅ useReducer Loading/success/error states, state machine

Example: When to migrate from useState to useReducer

// ❌ useState becomes unwieldy with complex state
function ComplexForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState(0);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [submitError, setSubmitError] = useState(null);
  
  // Hard to coordinate updates across multiple setState calls
  const handleSubmit = async () => {
    setIsSubmitting(true);
    setSubmitError(null);
    // ... complex logic
  };
}

// ✅ useReducer centralizes complex state management
function ComplexForm() {
  const [state, dispatch] = useReducer(formReducer, {
    values: { name: '', email: '', age: 0 },
    errors: {},
    touched: {},
    isSubmitting: false,
    submitError: null
  });
  
  // Single dispatch for coordinated updates
  const handleSubmit = async () => {
    dispatch({ type: 'SUBMIT_START' });
    try {
      await submitForm(state.values);
      dispatch({ type: 'SUBMIT_SUCCESS' });
    } catch (error) {
      dispatch({ type: 'SUBMIT_ERROR', payload: error.message });
    }
  };
}

Migration Checklist: useState → useReducer

2.6 Async Actions and useReducer Integration

Pattern Implementation Use Case
Dispatch in Async Handler Call dispatch before/after async operations in event handlers Simple async flows in components
Thunk Pattern Function that receives dispatch and executes async logic Reusable async operations, middleware-like
useEffect Integration useEffect with dispatch for side effects Fetch on mount, sync with external systems
Custom Hook Wrap useReducer + async logic in custom hook Encapsulate complex async state management
Async State Pattern State Structure Actions
Loading/Error/Data { loading: boolean, error: Error?, data: T? } START, SUCCESS, ERROR
Status Enum { status: 'idle' | 'loading' | 'success' | 'error', ... } FETCH_START, FETCH_SUCCESS, FETCH_ERROR, RESET
Request Tracking { requestId: string, pending: boolean, ... } Track request IDs to prevent race conditions

Example: Async actions with useReducer

type State = {
  data: User[] | null;
  loading: boolean;
  error: string | null;
};

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: User[] }
  | { type: 'FETCH_ERROR'; payload: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'FETCH_START':
      return { data: null, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { data: action.payload, loading: false, error: null };
    case 'FETCH_ERROR':
      return { data: null, loading: false, error: action.payload };
    default:
      return state;
  }
}

function UserList() {
  const [state, dispatch] = useReducer(reducer, {
    data: null,
    loading: false,
    error: null
  });
  
  // Async handler with dispatch
  const fetchUsers = async () => {
    dispatch({ type: 'FETCH_START' });
    
    try {
      const response = await fetch('/api/users');
      const data = await response.json();
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
    }
  };
  
  // Fetch on mount
  useEffect(() => {
    fetchUsers();
  }, []);
  
  if (state.loading) return <div>Loading...</div>;
  if (state.error) return <div>Error: {state.error}</div>;
  if (state.data) return <ul>{state.data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
  return null;
}

Example: Custom async hook with useReducer

// Reusable async hook
function useAsync<T>(asyncFunction: () => Promise<T>) {
  const [state, dispatch] = useReducer(
    (state: AsyncState<T>, action: AsyncAction<T>) => {
      switch (action.type) {
        case 'LOADING': return { status: 'loading' };
        case 'SUCCESS': return { status: 'success', data: action.payload };
        case 'ERROR': return { status: 'error', error: action.payload };
        default: return state;
      }
    },
    { status: 'idle' }
  );
  
  const execute = useCallback(async () => {
    dispatch({ type: 'LOADING' });
    try {
      const data = await asyncFunction();
      dispatch({ type: 'SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'ERROR', payload: error.message });
    }
  }, [asyncFunction]);
  
  return { ...state, execute };
}

// Usage
function UserProfile({ userId }) {
  const { status, data, error, execute } = useAsync(() =>
    fetch(`/api/users/${userId}`).then(r => r.json())
  );
  
  useEffect(() => {
    execute();
  }, [userId, execute]);
  
  if (status === 'loading') return <div>Loading...</div>;
  if (status === 'error') return <div>Error: {error}</div>;
  if (status === 'success') return <div>{data.name}</div>;
  return null;
}
Race Conditions: Multiple async operations can complete out of order. Use request IDs or abort controllers to cancel stale requests. React 18's useTransition can also help manage async state updates.

useReducer Best Practices Summary

  • Use switch statements for clear, explicit action handling
  • Create action creators for consistent action structure
  • Implement state machines for complex async flows
  • Choose useReducer when state has 3+ related pieces
  • Dispatch actions in async handlers for coordinated updates
  • Extract async logic into custom hooks for reusability
  • Use TypeScript discriminated unions for type-safe reducers

3. Context API and Global State Management

3.1 createContext and Provider Component Patterns

API Syntax Description Return Value
createContext createContext(defaultValue) Creates a Context object with default value Context object with Provider and Consumer
Context.Provider <MyContext.Provider value={value}> Provides context value to child components Accepts value prop, passes to consumers
Context.Consumer <MyContext.Consumer>{value => ...}</MyContext.Consumer> Render prop pattern for consuming context Legacy - use useContext hook instead
Provider Pattern Implementation Use Case
Simple Provider <Context.Provider value={state}>{children}</Context.Provider> Pass static or computed value directly
Provider Component RECOMMENDED Wrap Provider in custom component with state management Encapsulate state logic, cleaner API for consumers
Provider with useState Provider component manages state with useState hook Simple global state, few update operations
Provider with useReducer Provider component manages state with useReducer Complex state logic, many actions, Redux-like

Example: Basic Context creation and Provider setup

import { createContext, useState } from 'react';

// 1. Create Context with default value
const ThemeContext = createContext('light');

// 2. Create Provider component
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  // Value object with state and updaters
  const value = { theme, toggleTheme };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Usage: Wrap app with Provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
}

Example: Provider with useReducer for complex state

import { createContext, useReducer } from 'react';

// Context
const CartContext = createContext(null);

// Reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter(i => i.id !== action.payload) };
    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(i =>
          i.id === action.payload.id ? { ...i, quantity: action.payload.quantity } : i
        )
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      return state;
  }
}

// Provider component
function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
  
  // Derived values
  const itemCount = state.items.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = state.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  
  const value = {
    items: state.items,
    itemCount,
    totalPrice,
    dispatch
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}
Default Value: The default value in createContext(defaultValue) is only used when a component doesn't have a matching Provider above it in the tree. It's useful for testing components in isolation.

3.2 useContext Hook and Context Consumption

API Syntax Description Return Value
useContext useContext(MyContext) Subscribes component to context changes Current context value from nearest Provider
Context Argument Pass Context object (not Provider/Consumer) Must be the Context object from createContext Returns value prop from nearest Provider
Consumption Pattern Implementation Benefit
Direct useContext const value = useContext(MyContext) Simple, direct access to context
Custom Hook RECOMMENDED const useMyContext = () => useContext(MyContext) Encapsulation, validation, better API
Selective Access const { user, setUser } = useContext(UserContext) Destructure only needed values from context

Example: Consuming context with useContext

import { useContext } from 'react';

// Consume context in component
function ThemeToggle() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

// Another consumer
function ThemedPanel() {
  const { theme } = useContext(ThemeContext);
  
  const styles = {
    background: theme === 'light' ? '#fff' : '#333',
    color: theme === 'light' ? '#333' : '#fff'
  };
  
  return <div style={styles}>Themed content</div>;
}

Example: Custom hook for context with validation

import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

// Provider
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  const login = (credentials) => {
    // Login logic
    setUser({ name: credentials.username });
    setIsAuthenticated(true);
  };
  
  const logout = () => {
    setUser(null);
    setIsAuthenticated(false);
  };
  
  const value = { user, isAuthenticated, login, logout };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook with validation
export function useAuth() {
  const context = useContext(AuthContext);
  
  if (context === null) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  
  return context;
}

// Usage in component
function UserProfile() {
  const { user, logout } = useAuth(); // Safe, validated access
  
  return (
    <div>
      <p>Welcome, {user.name}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}
Warning: Components using useContext will re-render when the context value changes. This happens even if the component only uses a small part of the context value. See section 3.4 for optimization techniques.

3.3 Multiple Context Providers and Context Composition

Pattern Implementation Use Case
Nested Providers Stack multiple Providers in tree hierarchy Different concerns (theme, auth, settings, etc.)
Composed Provider Single component wrapping multiple Providers Reduce nesting, cleaner App component
Dependent Contexts One context consumes another context's value Context depends on data from another context
Composition Strategy Code Pattern Pros/Cons
Manual Nesting <A><B><C>{children}</C></B></A> ✅ Explicit, clear dependencies
❌ Verbose, deep nesting
Provider Composer <ComposeProviders providers={[A, B, C]}> ✅ Clean, scalable
❌ Extra abstraction
Single Root Provider <AppProvider> wraps all contexts ✅ Single import, organized
❌ All contexts loaded together

Example: Multiple providers composition

// Manual nesting (simple but verbose)
function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LanguageProvider>
          <NotificationProvider>
            <Routes />
          </NotificationProvider>
        </LanguageProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

// Better: Compose providers helper
function ComposeProviders({ providers, children }) {
  return providers.reduceRight((acc, Provider) => {
    return <Provider>{acc}</Provider>;
  }, children);
}

function App() {
  return (
    <ComposeProviders providers={[
      ThemeProvider,
      AuthProvider,
      LanguageProvider,
      NotificationProvider
    ]}>
      <Routes />
    </ComposeProviders>
  );
}

// Best: Single root provider
function AppProvider({ children }) {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LanguageProvider>
          <NotificationProvider>
            {children}
          </NotificationProvider>
        </LanguageProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

function App() {
  return (
    <AppProvider>
      <Routes />
    </AppProvider>
  );
}

Example: Dependent contexts

// UserPreferences context depends on AuthContext
function UserPreferencesProvider({ children }) {
  const { user } = useAuth(); // Depends on AuthContext
  const [preferences, setPreferences] = useState(null);
  
  useEffect(() => {
    if (user) {
      // Load user preferences when user is available
      loadPreferences(user.id).then(setPreferences);
    } else {
      setPreferences(null);
    }
  }, [user]);
  
  const value = { preferences, setPreferences };
  
  return (
    <PreferencesContext.Provider value={value}>
      {children}
    </PreferencesContext.Provider>
  );
}

// Must be nested under AuthProvider
function App() {
  return (
    <AuthProvider>
      <UserPreferencesProvider>
        <Dashboard />
      </UserPreferencesProvider>
    </AuthProvider>
  );
}

3.4 Context Performance Optimization Techniques

Problem Cause Solution
Unnecessary Re-renders Context value changes, all consumers re-render Split contexts, memoize value, use selectors
New Object Every Render Provider creates new value object on each render Memoize context value with useMemo
Consuming Entire Context Component re-renders even if it uses one field Split context by concern, use selector pattern
Optimization Technique Implementation Benefit
Memoize Context Value ESSENTIAL const value = useMemo(() => ({...}), [deps]) Prevent re-renders from object identity changes
Split Context by Concern Separate contexts for data and updaters Components only re-render when needed data changes
React.memo on Consumers export default React.memo(Component) Prevent re-renders from parent updates
Selector Pattern Custom hook with selector function Subscribe to specific slice of context state
Separate State/Dispatch Contexts One context for state, another for dispatch Components using only dispatch never re-render

Example: Memoizing context value

import { createContext, useState, useMemo } from 'react';

// ❌ Bad: New object every render, causes unnecessary re-renders
function BadProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // New object reference on every render!
  const value = { count, setCount, name, setName };
  
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

// ✅ Good: Memoized value, only changes when dependencies change
function GoodProvider({ children }) {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  // Memoized - same object reference unless count or name changes
  const value = useMemo(
    () => ({ count, setCount, name, setName }),
    [count, name]
  );
  
  return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}

Example: Split context for state and dispatch

import { createContext, useContext, useReducer, useMemo } from 'react';

// Separate contexts for state and dispatch
const TodoStateContext = createContext(null);
const TodoDispatchContext = createContext(null);

function TodoProvider({ children }) {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  
  // State context value - changes when state changes
  const stateValue = useMemo(() => state, [state]);
  
  // Dispatch context value - never changes (dispatch is stable)
  const dispatchValue = useMemo(() => dispatch, []);
  
  return (
    <TodoStateContext.Provider value={stateValue}>
      <TodoDispatchContext.Provider value={dispatchValue}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoStateContext.Provider>
  );
}

// Hooks for consuming
export function useTodoState() {
  const context = useContext(TodoStateContext);
  if (!context) throw new Error('Must be used within TodoProvider');
  return context;
}

export function useTodoDispatch() {
  const context = useContext(TodoDispatchContext);
  if (!context) throw new Error('Must be used within TodoProvider');
  return context;
}

// Usage: Components only using dispatch never re-render when state changes
function AddTodoButton() {
  const dispatch = useTodoDispatch(); // Won't re-render on state changes
  
  return (
    <button onClick={() => dispatch({ type: 'ADD', payload: 'New Todo' })}>
      Add Todo
    </button>
  );
}

// Component using state re-renders when state changes
function TodoList() {
  const { todos } = useTodoState(); // Re-renders when state changes
  return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

Example: Selector pattern for granular subscriptions

// Custom hook with selector
function useStoreSelector(selector) {
  const store = useContext(StoreContext);
  return selector(store);
}

// Usage: Subscribe to specific data
function UserProfile() {
  // Only re-renders when user changes, not when cart or settings change
  const user = useStoreSelector(store => store.user);
  
  return <div>{user.name}</div>;
}

function CartCount() {
  // Only re-renders when cart items change
  const itemCount = useStoreSelector(store => store.cart.items.length);
  
  return <span>{itemCount} items</span>;
}
Performance Tip: For high-frequency updates, consider external state management libraries like Zustand or Jotai that have built-in selector optimizations, or use useSyncExternalStore for external stores.

3.5 Context vs Prop Drilling Trade-offs

Aspect Prop Drilling Context API
Explicitness ✅ Explicit data flow, easy to trace ❌ Implicit, harder to track dependencies
Boilerplate ❌ Pass props through many components ✅ Minimal, direct access anywhere
Reusability ✅ Components are portable, self-contained ❌ Components coupled to context structure
Performance ✅ Optimal, only affected components re-render ⚠️ All consumers re-render on context change
Testing ✅ Easy to test, pass props directly ❌ Must wrap in Provider, mock context
Refactoring ❌ Difficult to change prop names/structure ✅ Change context, consumers auto-update
Use Prop Drilling When Use Context When
2-3 levels of component nesting 4+ levels of component nesting
Props change frequently Props change infrequently (theme, locale, auth)
Component reusability is priority Developer experience is priority
Data is specific to component tree branch Data is truly global (app-wide)
Simple, small applications Medium to large applications
Performance is critical Convenience outweighs performance concerns

Example: When prop drilling is acceptable

// Acceptable: 2-3 levels, clear data flow
function App() {
  const [user, setUser] = useState(null);
  return <Dashboard user={user} setUser={setUser} />;
}

function Dashboard({ user, setUser }) {
  return <UserProfile user={user} setUser={setUser} />;
}

function UserProfile({ user, setUser }) {
  return <div>{user.name}</div>;
}

// Problematic: 5+ levels, props only used at the end
function App() {
  const [theme, setTheme] = useState('light');
  return <Layout theme={theme} setTheme={setTheme} />;
}

function Layout({ theme, setTheme }) {
  return <Sidebar theme={theme} setTheme={setTheme} />;
}

function Sidebar({ theme, setTheme }) {
  return <Menu theme={theme} setTheme={setTheme} />;
}

function Menu({ theme, setTheme }) {
  return <MenuItem theme={theme} setTheme={setTheme} />;
}

function MenuItem({ theme, setTheme }) {
  // Finally used here, but passed through 4 intermediate components
  return <button style={{ background: theme }}>Item</button>;
}

Decision Framework

  • Start with props - simpler, more explicit
  • Move to Context when props pass through 4+ components
  • Use Context for: theme, locale, auth, global UI state
  • Avoid Context for: frequently changing data, component-specific state
  • Hybrid approach: Context for global, props for local tree data

3.6 Context Testing and Development Patterns

Testing Pattern Implementation Use Case
Wrap in Provider Use real Provider with test values Integration tests, test Provider + Consumer
Custom Render Wrapper Create test utility wrapping component in Providers Avoid repeating Provider setup in every test
Mock Context Value Create mock Provider with controlled values Test component with specific context states
Test Provider Logic Test Provider component's state management separately Unit test context logic without consumers
Development Pattern Description Benefit
Context DevTools Use React DevTools to inspect context values Debug context state and changes
Context Logger Add logging to Provider for state changes Track when and why context updates
TypeScript Inference Strongly type context for autocomplete and safety Catch errors at compile time, better DX
Default Value Warning Throw error in custom hook if context is null Catch missing Provider early in development

Example: Testing components with Context

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Test with real Provider
test('ThemeToggle changes theme', async () => {
  render(
    <ThemeProvider>
      <ThemeToggle />
    </ThemeProvider>
  );
  
  const button = screen.getByRole('button');
  expect(button).toHaveTextContent('light');
  
  await userEvent.click(button);
  expect(button).toHaveTextContent('dark');
});

// Custom render wrapper for multiple providers
function renderWithProviders(ui, { themeValue = 'light', ...options } = {}) {
  function Wrapper({ children }) {
    return (
      <ThemeProvider initialTheme={themeValue}>
        <AuthProvider>
          <LanguageProvider>
            {children}
          </LanguageProvider>
        </AuthProvider>
      </ThemeProvider>
    );
  }
  
  return render(ui, { wrapper: Wrapper, ...options });
}

// Usage with custom wrapper
test('component renders with dark theme', () => {
  renderWithProviders(<MyComponent />, { themeValue: 'dark' });
  // Test component with dark theme
});

Example: Mock context for testing

// Mock Provider for testing
function MockAuthProvider({ children, value }) {
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

test('shows logout button when authenticated', () => {
  const mockValue = {
    user: { name: 'John', id: 1 },
    isAuthenticated: true,
    logout: jest.fn()
  };
  
  render(
    <MockAuthProvider value={mockValue}>
      <UserMenu />
    </MockAuthProvider>
  );
  
  expect(screen.getByText('Logout')).toBeInTheDocument();
});

test('shows login button when not authenticated', () => {
  const mockValue = {
    user: null,
    isAuthenticated: false,
    login: jest.fn()
  };
  
  render(
    <MockAuthProvider value={mockValue}>
      <UserMenu />
    </MockAuthProvider>
  );
  
  expect(screen.getByText('Login')).toBeInTheDocument();
});

Example: TypeScript context with full type safety

import { createContext, useContext, ReactNode } from 'react';

// Define types
interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// Create typed context (null as default indicates provider required)
const AuthContext = createContext<AuthContextType | null>(null);

// Provider
export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  
  const login = async (email: string, password: string) => {
    const userData = await loginAPI(email, password);
    setUser(userData);
  };
  
  const logout = () => setUser(null);
  
  const value: AuthContextType = {
    user,
    isAuthenticated: user !== null,
    login,
    logout
  };
  
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// Typed hook with runtime validation
export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  
  if (context === null) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  
  return context; // TypeScript knows this is AuthContextType
}

// Usage with full type safety
function Profile() {
  const { user, logout } = useAuth(); // Fully typed, autocomplete works
  
  if (!user) return null;
  
  return (
    <div>
      <h1>{user.name}</h1> {/* TypeScript knows user.name exists */}
      <p>{user.email}</p>
      <button onClick={logout}>Logout</button>
    </div>
  );
}

Context API Best Practices Summary

  • Create custom hooks with validation for consuming context
  • Always memoize context values with useMemo to prevent unnecessary re-renders
  • Split contexts by concern (state/dispatch, read/write) for performance
  • Use TypeScript for type-safe context and better developer experience
  • Prefer props for 2-3 levels, Context for 4+ levels or truly global state
  • Create custom render wrappers for testing components with context
  • Compose providers for cleaner app structure with multiple contexts

4. State Initialization and Default Values

4.1 Initial State from Props and External Sources

Initialization Source Pattern Use Case Caveat
Props (Direct) useState(props.initialValue) Set initial state from parent component ⚠️ Only used on mount, doesn't update when prop changes
Props (Lazy) useState(() => computeFromProps(props)) Compute initial state from props once ⚠️ Computation runs only on mount
URL Params useState(() => new URLSearchParams(location.search).get('id')) Initialize from query string Sync with URL on changes using useEffect
Environment Variables useState(process.env.REACT_APP_DEFAULT_THEME) Configuration-based defaults Build-time values, not runtime
External API Initialize empty, fetch in useEffect Data from server on mount Handle loading state, errors
Pattern Code Behavior
Initialize from Prop const [count, setCount] = useState(props.initialCount) Uses prop value on mount, ignores future prop changes
Sync with Prop Changes useEffect(() => setState(props.value), [props.value]) Updates state when prop changes (controlled pattern)
Key-based Reset <Component key={props.id} /> Remounts component with fresh state when key changes

Example: Initialize state from props

// Pattern 1: Direct initialization (one-time)
function Counter({ initialCount = 0 }) {
  // Only uses initialCount on first render
  const [count, setCount] = useState(initialCount);
  
  // If parent changes initialCount, this component's state won't update
  return <div>{count}</div>;
}

// Pattern 2: Sync with prop changes (controlled component)
function SyncedCounter({ count: propCount }) {
  const [count, setCount] = useState(propCount);
  
  // Update local state when prop changes
  useEffect(() => {
    setCount(propCount);
  }, [propCount]);
  
  return <div>{count}</div>;
}

// Pattern 3: Key-based reset (best for resetting state)
function EditForm({ userId }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  
  // When userId changes, component remounts with fresh state
  return <form>...</form>;
}

// Parent component uses key to reset EditForm
function UserEditor({ userId }) {
  return <EditForm key={userId} userId={userId} />;
}

Example: Initialize from URL and external sources

import { useState, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';

function ProductFilter() {
  // Initialize from URL query params
  const [searchParams] = useSearchParams();
  const [category, setCategory] = useState(() => 
    searchParams.get('category') || 'all'
  );
  const [minPrice, setMinPrice] = useState(() => 
    Number(searchParams.get('minPrice')) || 0
  );
  
  // Initialize from environment
  const [theme, setTheme] = useState(
    process.env.REACT_APP_DEFAULT_THEME || 'light'
  );
  
  // Initialize from external API
  const [userPreferences, setUserPreferences] = useState(null);
  
  useEffect(() => {
    // Fetch user preferences on mount
    fetch('/api/preferences')
      .then(res => res.json())
      .then(data => setUserPreferences(data));
  }, []);
  
  return <div>Filter UI</div>;
}
Common Mistake: Using useState(props.value) and expecting it to update when prop changes. This creates a "stale initial state" issue. Use controlled component pattern with useEffect or key-based reset instead.

4.2 Derived Initial State Patterns

Pattern Implementation Use Case
Computed from Props useState(() => transformProps(props)) Process/transform props for initial state
Merged Defaults useState({ ...defaults, ...props.overrides }) Combine default values with prop overrides
Normalized Data useState(() => normalizeData(props.items)) Convert array to normalized structure (byId, allIds)
Filtered/Sorted useState(() => props.items.filter(condition).sort(compare)) Pre-process collection for initial state
Scenario Avoid (Re-computed Every Render) Use (Computed Once)
Array Processing useState(items.map(transform)) useState(() => items.map(transform))
Object Merge useState({ ...defaults, ...props }) useState(() => ({ ...defaults, ...props }))
Date Calculation useState(new Date()) useState(() => new Date())

Example: Derived initial state patterns

// Pattern 1: Computed from props
function TaskList({ tasks, filter }) {
  // Compute initial filtered list once
  const [filteredTasks, setFilteredTasks] = useState(() =>
    tasks.filter(task => task.status === filter)
  );
  
  return <ul>{filteredTasks.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
}

// Pattern 2: Merged defaults with prop overrides
function ConfigPanel({ userConfig = {} }) {
  const defaultConfig = {
    theme: 'light',
    language: 'en',
    notifications: true,
    autoSave: false
  };
  
  // Merge defaults with user overrides
  const [config, setConfig] = useState(() => ({
    ...defaultConfig,
    ...userConfig
  }));
  
  return <div>Config UI</div>;
}

// Pattern 3: Normalize data structure
function UserDirectory({ users }) {
  // Normalize array to efficient lookup structure
  const [usersById, setUsersById] = useState(() => {
    const normalized = {};
    users.forEach(user => {
      normalized[user.id] = user;
    });
    return normalized;
  });
  
  const [userIds, setUserIds] = useState(() => users.map(u => u.id));
  
  return <div>User directory</div>;
}

// Pattern 4: Complex transformation
function DataChart({ rawData }) {
  const [chartData, setChartData] = useState(() => {
    // Expensive transformation done only once
    return rawData
      .filter(d => d.value > 0)
      .map(d => ({ x: d.timestamp, y: d.value }))
      .sort((a, b) => a.x - b.x);
  });
  
  return <Chart data={chartData} />;
}
Performance Tip: Always use lazy initialization with arrow function when computing initial state from props or external data. This ensures expensive computations run only once on mount, not on every render.

4.3 State Hydration from localStorage/sessionStorage

Storage Type Lifespan Scope Use Case
localStorage Permanent (until cleared) All tabs, same origin User preferences, settings, auth tokens
sessionStorage Session (tab close) Single tab only Temporary form data, wizard state
IndexedDB Permanent All tabs, large data Offline data, cached API responses
Hydration Pattern Implementation Features
Basic Read useState(() => JSON.parse(localStorage.getItem(key)) || default) Simple one-time load
With Error Handling Try-catch around JSON.parse Handles corrupt/invalid data
Auto-sync (Write) useEffect to save on state changes Persists state automatically
Custom Hook RECOMMENDED useLocalStorage(key, defaultValue) Reusable, encapsulated logic

Example: localStorage hydration patterns

import { useState, useEffect } from 'react';

// Pattern 1: Basic hydration with fallback
function ThemeSelector() {
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem('theme');
    return saved || 'light'; // Fallback to 'light'
  });
  
  // Save to localStorage when theme changes
  useEffect(() => {
    localStorage.setItem('theme', theme);
  }, [theme]);
  
  return <select value={theme} onChange={e => setTheme(e.target.value)}>...</select>;
}

// Pattern 2: With JSON parsing and error handling
function UserPreferences() {
  const [preferences, setPreferences] = useState(() => {
    try {
      const saved = localStorage.getItem('preferences');
      return saved ? JSON.parse(saved) : { notifications: true, autoSave: false };
    } catch (error) {
      console.error('Failed to parse preferences:', error);
      return { notifications: true, autoSave: false };
    }
  });
  
  useEffect(() => {
    try {
      localStorage.setItem('preferences', JSON.stringify(preferences));
    } catch (error) {
      console.error('Failed to save preferences:', error);
    }
  }, [preferences]);
  
  return <div>Preferences UI</div>;
}

Example: Custom useLocalStorage hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, defaultValue) {
  // Initialize from localStorage
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return defaultValue;
    }
  });
  
  // Sync to localStorage on changes
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, value]);
  
  return [value, setValue];
}

// Usage: Clean, reusable
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [user, setUser] = useLocalStorage('user', null);
  const [settings, setSettings] = useLocalStorage('settings', { lang: 'en' });
  
  return (
    <div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

Example: sessionStorage for temporary state

// Multi-step form with sessionStorage backup
function MultiStepForm() {
  const [step, setStep] = useState(() => {
    const saved = sessionStorage.getItem('formStep');
    return saved ? Number(saved) : 1;
  });
  
  const [formData, setFormData] = useState(() => {
    const saved = sessionStorage.getItem('formData');
    return saved ? JSON.parse(saved) : { name: '', email: '', phone: '' };
  });
  
  // Save to sessionStorage on changes
  useEffect(() => {
    sessionStorage.setItem('formStep', step.toString());
  }, [step]);
  
  useEffect(() => {
    sessionStorage.setItem('formData', JSON.stringify(formData));
  }, [formData]);
  
  // Clear on successful submit
  const handleSubmit = async () => {
    await submitForm(formData);
    sessionStorage.removeItem('formStep');
    sessionStorage.removeItem('formData');
  };
  
  return <form>Step {step} form</form>;
}
Storage Limits: localStorage/sessionStorage typically have 5-10MB limits per origin. Always handle quota exceeded errors. For larger data, use IndexedDB. Never store sensitive data unencrypted.

4.4 Controlled vs Uncontrolled Component Patterns

Pattern State Location Value Source Updates Via
Controlled React state (parent or component) value prop from state onChange handler updates state
Uncontrolled DOM (native element state) defaultValue prop (initial only) DOM manages its own state, access via ref
Aspect Controlled Uncontrolled
Single Source of Truth ✅ React state is source of truth ❌ DOM is source of truth
Real-time Validation ✅ Easy to validate on each keystroke ❌ Validate only on submit/blur
Conditional Rendering ✅ Can easily show/hide based on state ❌ Limited control over display logic
Performance ⚠️ Re-renders on every change ✅ No re-renders, faster for large forms
Code Complexity ❌ More boilerplate (state + handlers) ✅ Less code, simpler for basic forms
Integration ✅ Works well with React ecosystem ⚠️ Limited to native inputs

Example: Controlled component pattern

import { useState } from 'react';

// Controlled input: React state is source of truth
function ControlledForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [age, setAge] = useState('');
  
  // Real-time validation
  const isValidEmail = email.includes('@');
  const isValidAge = age === '' || (Number(age) > 0 && Number(age) < 150);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name, email, age });
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={e => setName(e.target.value)}
        placeholder="Name"
      />
      
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
        placeholder="Email"
      />
      {!isValidEmail && email && <span>Invalid email</span>}
      
      <input
        type="number"
        value={age}
        onChange={e => setAge(e.target.value)}
        placeholder="Age"
      />
      {!isValidAge && <span>Invalid age</span>}
      
      <button type="submit" disabled={!isValidEmail || !isValidAge}>
        Submit
      </button>
    </form>
  );
}

Example: Uncontrolled component pattern

import { useRef } from 'react';

// Uncontrolled input: DOM holds the state
function UncontrolledForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);
  const ageRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Access values from DOM via refs
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value,
      age: ageRef.current.value
    };
    
    console.log(formData);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        ref={nameRef}
        defaultValue=""
        placeholder="Name"
      />
      
      <input
        type="email"
        ref={emailRef}
        defaultValue=""
        placeholder="Email"
      />
      
      <input
        type="number"
        ref={ageRef}
        defaultValue=""
        placeholder="Age"
      />
      
      <button type="submit">Submit</button>
    </form>
  );
}

Example: Hybrid approach

// Hybrid: Controlled for some fields, uncontrolled for others
function HybridForm() {
  // Controlled for important fields that need validation
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  // Uncontrolled for less critical fields
  const nameRef = useRef(null);
  const bioRef = useRef(null);
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formData = {
      email,
      password,
      name: nameRef.current.value,
      bio: bioRef.current.value
    };
    
    console.log(formData);
  };
  
  const isValidEmail = email.includes('@');
  const isValidPassword = password.length >= 8;
  
  return (
    <form onSubmit={handleSubmit}>
      {/* Controlled - need validation */}
      <input
        type="email"
        value={email}
        onChange={e => setEmail(e.target.value)}
      />
      {!isValidEmail && email && <span>Invalid email</span>}
      
      <input
        type="password"
        value={password}
        onChange={e => setPassword(e.target.value)}
      />
      {!isValidPassword && password && <span>Min 8 characters</span>}
      
      {/* Uncontrolled - simple fields */}
      <input type="text" ref={nameRef} defaultValue="" placeholder="Name" />
      <textarea ref={bioRef} defaultValue="" placeholder="Bio" />
      
      <button type="submit" disabled={!isValidEmail || !isValidPassword}>
        Submit
      </button>
    </form>
  );
}
Recommendation: Prefer controlled components for React apps - they integrate better with React's data flow and enable powerful features like real-time validation. Use uncontrolled only for performance-critical scenarios or when integrating with non-React code.

4.5 Default Props and State Fallback Strategies

Fallback Strategy Implementation Use Case
Default Parameters function Comp({ value = 'default' }) Simple primitive defaults
Nullish Coalescing const val = prop ?? 'default' Distinguish null/undefined from other falsy values
Logical OR const val = prop || 'default' Any falsy value triggers fallback
Object Spread { ...defaults, ...props } Merge default object with prop overrides
Conditional Render {value !== undefined ? <Component /> : <Fallback />} Render different components based on prop presence
Operator Triggers Fallback When Example
|| (OR) Falsy: false, 0, '', null, undefined, NaN count || 10 → 0 triggers fallback (unexpected!)
?? (Nullish) RECOMMENDED Only null or undefined count ?? 10 → 0 doesn't trigger fallback ✅

Example: Default prop patterns

// Pattern 1: Default parameters (function signature)
function Button({ label = 'Click me', variant = 'primary', onClick }) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}

// Pattern 2: Nullish coalescing for state initialization
function Counter({ initialCount }) {
  // Correctly handles initialCount = 0 (0 is valid, not fallback)
  const [count, setCount] = useState(initialCount ?? 10);
  
  return <div>{count}</div>;
}

// ❌ Wrong: OR operator treats 0 as falsy
function BadCounter({ initialCount }) {
  const [count, setCount] = useState(initialCount || 10);
  // If initialCount = 0, uses 10 instead (unexpected!)
  
  return <div>{count}</div>;
}

// Pattern 3: Object spread for complex defaults
function UserProfile({ user = {} }) {
  const defaultUser = {
    name: 'Guest',
    avatar: '/default-avatar.png',
    role: 'viewer',
    preferences: {
      theme: 'light',
      language: 'en'
    }
  };
  
  // Merge defaults with provided user data
  const profile = {
    ...defaultUser,
    ...user,
    preferences: {
      ...defaultUser.preferences,
      ...user.preferences
    }
  };
  
  return <div>{profile.name}</div>;
}

Example: Fallback strategies for missing data

import { useState, useEffect } from 'react';

function DataDisplay({ dataSource }) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch(dataSource)
      .then(res => res.json())
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [dataSource]);
  
  // Fallback strategy: Check state in order
  if (loading) {
    return <LoadingSpinner />;
  }
  
  if (error) {
    return <ErrorMessage error={error} />;
  }
  
  if (!data || data.length === 0) {
    return <EmptyState message="No data available" />;
  }
  
  return (
    <div>
      {data.map(item => (
        <div key={item.id}>
          {/* Nested fallbacks for optional fields */}
          <h2>{item.title ?? 'Untitled'}</h2>
          <p>{item.description || 'No description provided'}</p>
          <span>{item.author?.name ?? 'Anonymous'}</span>
        </div>
      ))}
    </div>
  );
}

// Pattern: Optional chaining + nullish coalescing
function SafeDataAccess({ data }) {
  return (
    <div>
      {/* Safe navigation through nested properties */}
      <p>City: {data?.user?.address?.city ?? 'Unknown'}</p>
      <p>Posts: {data?.user?.posts?.length ?? 0}</p>
      <p>Name: {data?.user?.profile?.name || 'Guest'}</p>
    </div>
  );
}

Example: Conditional rendering fallbacks

function ContentPanel({ content, placeholder, emptyMessage }) {
  // Strategy 1: Render placeholder if content is undefined
  if (content === undefined) {
    return <Placeholder text={placeholder} />;
  }
  
  // Strategy 2: Render empty state if content exists but is empty
  if (content === null || (Array.isArray(content) && content.length === 0)) {
    return <EmptyState message={emptyMessage ?? 'No content available'} />;
  }
  
  // Strategy 3: Render actual content
  return <div>{content}</div>;
}

// Usage with multiple fallback levels
function App() {
  const [data, setData] = useState(undefined); // undefined = not loaded yet
  
  return (
    <ContentPanel
      content={data}
      placeholder="Loading content..."
      emptyMessage="No items to display"
    />
  );
}

State Initialization Best Practices Summary

  • Use lazy initialization for expensive computations from props or external sources
  • Prefer ?? (nullish coalescing) over || for defaults to handle 0, false, '' correctly
  • Implement custom hooks like useLocalStorage for reusable persistence logic
  • Choose controlled components for better React integration and validation
  • Use key prop to reset component state when data source changes
  • Add error handling for localStorage parsing and quota exceeded errors
  • Apply optional chaining (?.) with nullish coalescing for safe nested access

5. State Update Patterns and Immutability

5.1 Object State Updates with Spread Operator

Operation Mutable (❌ Wrong) Immutable (✅ Correct)
Update Property state.name = 'John' setState(prev => ({ ...prev, name: 'John' }))
Add Property state.newProp = 'value' setState(prev => ({ ...prev, newProp: 'value' }))
Delete Property delete state.prop const {prop, ...rest} = state; setState(rest)
Multiple Properties state.a = 1; state.b = 2 setState(prev => ({ ...prev, a: 1, b: 2 }))
Pattern Syntax Use Case
Shallow Spread { ...state, key: value } Update top-level properties
Computed Property Name { ...state, [key]: value } Dynamic property names from variables
Merge Objects { ...state, ...updates } Apply multiple changes at once
Remove Property const {removed, ...rest} = state Destructure to exclude properties
Conditional Update { ...state, ...(condition && {key: value}) } Conditionally add properties

Example: Object state updates

function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    age: 30,
    email: 'john@example.com',
    address: { city: 'NYC', country: 'USA' }
  });
  
  // ❌ Wrong: Direct mutation doesn't trigger re-render
  const updateNameBad = () => {
    user.name = 'Jane';
    setUser(user); // Same reference, React won't re-render!
  };
  
  // ✅ Correct: Create new object with spread
  const updateName = (name) => {
    setUser(prev => ({ ...prev, name }));
  };
  
  // ✅ Update multiple properties
  const updateProfile = () => {
    setUser(prev => ({
      ...prev,
      name: 'Jane',
      age: 31,
      email: 'jane@example.com'
    }));
  };
  
  // ✅ Dynamic property name
  const updateField = (field, value) => {
    setUser(prev => ({ ...prev, [field]: value }));
  };
  
  // ✅ Merge with another object
  const applyUpdates = (updates) => {
    setUser(prev => ({ ...prev, ...updates }));
  };
  
  // ✅ Remove property
  const removeEmail = () => {
    setUser(prev => {
      const { email, ...rest } = prev;
      return rest;
    });
  };
  
  // ✅ Conditional property
  const updateWithOptionalField = (includeEmail) => {
    setUser(prev => ({
      ...prev,
      name: 'Updated',
      ...(includeEmail && { email: 'new@example.com' })
    }));
  };
  
  return <div>{user.name}</div>;
}
Critical: Spread operator creates a shallow copy. Nested objects/arrays are still referenced. For nested updates, see section 5.3.

5.2 Array State Manipulation (add, remove, update)

Operation Mutable (❌ Wrong) Immutable (✅ Correct)
Add to End arr.push(item) [...arr, item]
Add to Start arr.unshift(item) [item, ...arr]
Remove from End arr.pop() arr.slice(0, -1)
Remove from Start arr.shift() arr.slice(1)
Remove by Index arr.splice(index, 1) arr.filter((_, i) => i !== index)
Remove by Value arr.splice(arr.indexOf(val), 1) arr.filter(item => item !== val)
Update by Index arr[index] = newValue arr.map((item, i) => i === index ? newValue : item)
Sort arr.sort() [...arr].sort() or arr.toSorted()
Reverse arr.reverse() [...arr].reverse() or arr.toReversed()
Pattern Code Use Case
Add Item setItems(prev => [...prev, newItem]) Append to end of array
Prepend Item setItems(prev => [newItem, ...prev]) Add to beginning
Insert at Position setItems(prev => [...prev.slice(0, i), item, ...prev.slice(i)]) Insert at specific index
Remove by ID setItems(prev => prev.filter(item => item.id !== id)) Delete item by unique identifier
Update by ID setItems(prev => prev.map(item => item.id === id ? {...item, ...updates} : item)) Modify specific item immutably
Toggle Property setItems(prev => prev.map(item => item.id === id ? {...item, done: !item.done} : item)) Toggle boolean property
Replace Array setItems(newArray) Complete replacement

Example: Array state operations

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Task 1', completed: false },
    { id: 2, text: 'Task 2', completed: true }
  ]);
  
  // ✅ Add new todo
  const addTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos(prev => [...prev, newTodo]);
  };
  
  // ✅ Remove todo by id
  const removeTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };
  
  // ✅ Update todo text
  const updateTodoText = (id, newText) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, text: newText } : todo
    ));
  };
  
  // ✅ Toggle completed status
  const toggleTodo = (id) => {
    setTodos(prev => prev.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };
  
  // ✅ Insert at beginning
  const prependTodo = (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos(prev => [newTodo, ...prev]);
  };
  
  // ✅ Insert at specific position
  const insertTodoAt = (index, text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    setTodos(prev => [
      ...prev.slice(0, index),
      newTodo,
      ...prev.slice(index)
    ]);
  };
  
  // ✅ Sort immutably
  const sortByText = () => {
    setTodos(prev => [...prev].sort((a, b) => a.text.localeCompare(b.text)));
  };
  
  // ✅ Clear completed
  const clearCompleted = () => {
    setTodos(prev => prev.filter(todo => !todo.completed));
  };
  
  // ✅ Mark all as completed
  const completeAll = () => {
    setTodos(prev => prev.map(todo => ({ ...todo, completed: true })));
  };
  
  return <ul>{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}</ul>;
}
Modern Array Methods: ES2023 introduced immutable array methods: toSorted(), toReversed(), toSpliced(), and with(). These create copies without mutating the original. NEW

5.3 Nested Object State Updates with Immer Integration

Nesting Level Manual Immutable Update Complexity
1 Level { ...state, prop: value } ✅ Simple, manageable
2 Levels { ...state, nested: { ...state.nested, prop: value } } ⚠️ Verbose but doable
3+ Levels { ...state, a: { ...state.a, b: { ...state.a.b, c: value } } } ❌ Error-prone, use Immer
Approach Code Style Pros/Cons
Manual Spread Nested spread operators ✅ No dependencies
❌ Verbose, error-prone for deep nesting
Immer RECOMMENDED Write mutable-style code, produces immutable updates ✅ Clean, intuitive
✅ Safe for deep nesting
❌ Extra dependency
Immutability-helper MongoDB-style update syntax ✅ Declarative
❌ Unfamiliar syntax

Example: Manual nested updates (verbose)

function UserProfile() {
  const [user, setUser] = useState({
    name: 'John',
    profile: {
      bio: 'Developer',
      social: {
        twitter: '@john',
        github: 'john'
      }
    },
    settings: {
      theme: 'light',
      notifications: {
        email: true,
        push: false
      }
    }
  });
  
  // Update deeply nested property (manual spread - verbose!)
  const updateTwitter = (newHandle) => {
    setUser(prev => ({
      ...prev,
      profile: {
        ...prev.profile,
        social: {
          ...prev.profile.social,
          twitter: newHandle
        }
      }
    }));
  };
  
  // Update multiple nested properties
  const updateNotifications = (email, push) => {
    setUser(prev => ({
      ...prev,
      settings: {
        ...prev.settings,
        notifications: {
          ...prev.settings.notifications,
          email,
          push
        }
      }
    }));
  };
  
  return <div>{user.name}</div>;
}

Example: Immer for cleaner nested updates

import { useState } from 'react';
import { useImmer } from 'use-immer';

// Option 1: use-immer hook (recommended)
function UserProfileWithImmer() {
  const [user, updateUser] = useImmer({
    name: 'John',
    profile: {
      bio: 'Developer',
      social: { twitter: '@john', github: 'john' }
    },
    settings: {
      theme: 'light',
      notifications: { email: true, push: false }
    }
  });
  
  // ✅ Write "mutating" code, Immer makes it immutable
  const updateTwitter = (newHandle) => {
    updateUser(draft => {
      draft.profile.social.twitter = newHandle;
    });
  };
  
  const updateNotifications = (email, push) => {
    updateUser(draft => {
      draft.settings.notifications.email = email;
      draft.settings.notifications.push = push;
    });
  };
  
  // Complex update with array manipulation
  const addHobby = (hobby) => {
    updateUser(draft => {
      if (!draft.profile.hobbies) {
        draft.profile.hobbies = [];
      }
      draft.profile.hobbies.push(hobby);
    });
  };
  
  return <div>{user.name}</div>;
}

// Option 2: Manual Immer with produce
import produce from 'immer';

function UserProfileManualImmer() {
  const [user, setUser] = useState({...});
  
  const updateTwitter = (newHandle) => {
    setUser(prev => produce(prev, draft => {
      draft.profile.social.twitter = newHandle;
    }));
  };
  
  return <div>{user.name}</div>;
}

Example: Array of objects with nested updates

import { useImmer } from 'use-immer';

function TeamManager() {
  const [teams, updateTeams] = useImmer([
    {
      id: 1,
      name: 'Engineering',
      members: [
        { id: 101, name: 'Alice', role: 'Lead', skills: ['React', 'Node'] },
        { id: 102, name: 'Bob', role: 'Dev', skills: ['Python'] }
      ]
    }
  ]);
  
  // Update nested member's skill
  const addSkill = (teamId, memberId, skill) => {
    updateTeams(draft => {
      const team = draft.find(t => t.id === teamId);
      if (team) {
        const member = team.members.find(m => m.id === memberId);
        if (member) {
          member.skills.push(skill);
        }
      }
    });
  };
  
  // Update member role
  const updateMemberRole = (teamId, memberId, newRole) => {
    updateTeams(draft => {
      const team = draft.find(t => t.id === teamId);
      if (team) {
        const member = team.members.find(m => m.id === memberId);
        if (member) {
          member.role = newRole;
        }
      }
    });
  };
  
  // Without Immer (compare complexity):
  const addSkillManual = (teamId, memberId, skill) => {
    setTeams(prev => prev.map(team =>
      team.id === teamId
        ? {
            ...team,
            members: team.members.map(member =>
              member.id === memberId
                ? { ...member, skills: [...member.skills, skill] }
                : member
            )
          }
        : team
    ));
  };
  
  return <div>Teams</div>;
}
When to Use Immer: If you have 3+ levels of nesting, multiple nested arrays, or complex update logic, Immer significantly improves code readability and reduces bugs. Install: npm install immer use-immer

5.4 State Normalization for Complex Data Structures

Structure Type Problem Solution
Nested Arrays Slow lookups, duplicate data, update complexity Normalize to { byId: {}, allIds: [] }
Deeply Nested Objects Hard to update, performance issues Flatten to separate entities with references
Relational Data Duplication, inconsistency Normalize like a database (entities + IDs)
Pattern Structure Benefit
byId + allIds { byId: { 1: {...}, 2: {...} }, allIds: [1, 2] } O(1) lookups, easy updates, preserve order
Entity Slices { users: {byId, allIds}, posts: {byId, allIds} } Separate concerns, Redux-style normalization
References Store IDs instead of full objects Single source of truth, no duplication

Example: Denormalized vs Normalized state

// ❌ Denormalized: Nested, hard to update
const denormalizedState = {
  posts: [
    {
      id: 1,
      title: 'Post 1',
      author: { id: 10, name: 'Alice', email: 'alice@example.com' },
      comments: [
        { id: 101, text: 'Nice!', author: { id: 10, name: 'Alice', email: 'alice@example.com' } },
        { id: 102, text: 'Cool', author: { id: 11, name: 'Bob', email: 'bob@example.com' } }
      ]
    },
    {
      id: 2,
      title: 'Post 2',
      author: { id: 11, name: 'Bob', email: 'bob@example.com' },
      comments: []
    }
  ]
};

// Problem: To update Alice's email, must find and update in multiple places!

// ✅ Normalized: Flat, single source of truth
const normalizedState = {
  users: {
    byId: {
      10: { id: 10, name: 'Alice', email: 'alice@example.com' },
      11: { id: 11, name: 'Bob', email: 'bob@example.com' }
    },
    allIds: [10, 11]
  },
  posts: {
    byId: {
      1: { id: 1, title: 'Post 1', authorId: 10, commentIds: [101, 102] },
      2: { id: 2, title: 'Post 2', authorId: 11, commentIds: [] }
    },
    allIds: [1, 2]
  },
  comments: {
    byId: {
      101: { id: 101, text: 'Nice!', authorId: 10, postId: 1 },
      102: { id: 102, text: 'Cool', authorId: 11, postId: 1 }
    },
    allIds: [101, 102]
  }
};

// Easy update: Change Alice's email in ONE place
const updateUserEmail = (userId, newEmail) => {
  setState(prev => ({
    ...prev,
    users: {
      ...prev.users,
      byId: {
        ...prev.users.byId,
        [userId]: { ...prev.users.byId[userId], email: newEmail }
      }
    }
  }));
};

Example: Normalized state operations

function BlogApp() {
  const [state, setState] = useState({
    posts: { byId: {}, allIds: [] },
    comments: { byId: {}, allIds: [] },
    users: { byId: {}, allIds: [] }
  });
  
  // Add post
  const addPost = (post) => {
    setState(prev => ({
      ...prev,
      posts: {
        byId: { ...prev.posts.byId, [post.id]: post },
        allIds: [...prev.posts.allIds, post.id]
      }
    }));
  };
  
  // Remove post
  const removePost = (postId) => {
    setState(prev => {
      const { [postId]: removed, ...remainingPosts } = prev.posts.byId;
      return {
        ...prev,
        posts: {
          byId: remainingPosts,
          allIds: prev.posts.allIds.filter(id => id !== postId)
        }
      };
    });
  };
  
  // Update post
  const updatePost = (postId, updates) => {
    setState(prev => ({
      ...prev,
      posts: {
        ...prev.posts,
        byId: {
          ...prev.posts.byId,
          [postId]: { ...prev.posts.byId[postId], ...updates }
        }
      }
    }));
  };
  
  // Get post with author (selector pattern)
  const getPostWithAuthor = (postId) => {
    const post = state.posts.byId[postId];
    const author = state.users.byId[post.authorId];
    return { ...post, author };
  };
  
  return <div>Blog</div>;
}
Normalization Libraries: For complex normalization, consider normalizr library or Redux Toolkit's createEntityAdapter which provides utilities for normalized state management.

5.5 Avoiding State Mutation and Side Effects

Mutation Type Example (❌ Wrong) Why It Fails
Direct Assignment state.value = 123; setState(state) Same reference, React won't detect change
Array Methods arr.push(item); setArr(arr) Mutates original array, same reference
Object Methods Object.assign(obj, updates); setObj(obj) Mutates original object
Nested Assignment state.nested.value = 1; setState({...state}) Shallow copy doesn't protect nested objects
Side Effect Type Problem Solution
Render-time Mutations Changing state during render causes inconsistencies Only update state in event handlers or useEffect
External Mutations Modifying objects outside component Clone before modifying, keep state local
Closure Stale State Async operations use old state values Use functional updates: setState(prev => ...)

Example: Common mutation mistakes and fixes

function MutationExamples() {
  const [items, setItems] = useState([1, 2, 3]);
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  // ❌ WRONG: Mutations that don't trigger re-render
  const bad1 = () => {
    items.push(4);
    setItems(items); // Same reference!
  };
  
  const bad2 = () => {
    user.name = 'Jane';
    setUser(user); // Same reference!
  };
  
  const bad3 = () => {
    items.sort();
    setItems(items); // Mutated in place!
  };
  
  const bad4 = () => {
    const copy = user;
    copy.name = 'Jane';
    setUser(copy); // Shallow copy - same reference!
  };
  
  // ✅ CORRECT: Immutable updates
  const good1 = () => {
    setItems(prev => [...prev, 4]); // New array
  };
  
  const good2 = () => {
    setUser(prev => ({ ...prev, name: 'Jane' })); // New object
  };
  
  const good3 = () => {
    setItems(prev => [...prev].sort()); // Copy first, then sort
  };
  
  const good4 = () => {
    setUser(prev => ({ ...prev })); // Proper copy
  };
  
  return <div>Items: {items.length}</div>;
}

Example: Avoiding side effects in render

// ❌ WRONG: Mutation during render
function BadComponent({ data }) {
  const [processed, setProcessed] = useState([]);
  
  // BAD: Setting state during render!
  if (data.length > 0 && processed.length === 0) {
    setProcessed(data.map(transform)); // Causes infinite loop or warnings
  }
  
  return <div>{processed.length}</div>;
}

// ✅ CORRECT: Side effects in useEffect
function GoodComponent({ data }) {
  const [processed, setProcessed] = useState([]);
  
  useEffect(() => {
    setProcessed(data.map(transform));
  }, [data]);
  
  return <div>{processed.length}</div>;
}

// ✅ BETTER: Compute during render without state
function BestComponent({ data }) {
  const processed = useMemo(() => data.map(transform), [data]);
  
  return <div>{processed.length}</div>;
}
React's Assumption: React assumes state updates are immutable. Mutating state directly breaks React's diffing algorithm, can cause stale UI, missed re-renders, and hard-to-debug issues. Always create new references.

5.6 State Update Optimization and Performance

Optimization Technique Benefit
Bail Out Return same state object if no changes React skips re-render if same reference
Structural Sharing Reuse unchanged parts of state object Reduces memory, faster comparisons
Batch Updates Multiple setState calls batched into one render Fewer re-renders (automatic in React 18)
Lazy Updates Defer non-critical state updates Prioritize important UI updates
Split State Separate frequently/infrequently changing state Localize re-renders to affected components
Anti-pattern Problem Solution
Unnecessary Copying Always creating new object even when unchanged Check if update is needed, return same state
Large State Objects Updating small part causes full re-render Split into smaller, focused state pieces
Derived State Storing computed values in state Compute during render or use useMemo
Deep Copying JSON.parse(JSON.stringify()) for deep copy Slow, use Immer or selective copying

Example: Bail out of updates

function OptimizedComponent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'John', age: 30 });
  
  // ❌ Always creates new object, even if unchanged
  const updateUserBad = (newName) => {
    setUser(prev => ({ ...prev, name: newName }));
    // Even if newName === prev.name, creates new object!
  };
  
  // ✅ Bail out if unchanged
  const updateUserGood = (newName) => {
    setUser(prev => {
      if (prev.name === newName) {
        return prev; // Same reference, React skips re-render
      }
      return { ...prev, name: newName };
    });
  };
  
  // ✅ Bail out for primitive values (automatic)
  const incrementIfEven = () => {
    setCount(prev => {
      if (prev % 2 === 0) {
        return prev + 1;
      }
      return prev; // Same value, React bails out
    });
  };
  
  return <div>{user.name}: {count}</div>;
}

Example: Structural sharing

function TodoApp() {
  const [state, setState] = useState({
    todos: [],
    filter: 'all',
    sortBy: 'date',
    ui: { sidebarOpen: true, theme: 'light' }
  });
  
  // ❌ Creates entirely new object
  const toggleSidebarBad = () => {
    setState(prev => ({
      todos: [...prev.todos], // Unnecessary copy
      filter: prev.filter,
      sortBy: prev.sortBy,
      ui: { ...prev.ui, sidebarOpen: !prev.ui.sidebarOpen }
    }));
  };
  
  // ✅ Structural sharing - reuse unchanged parts
  const toggleSidebarGood = () => {
    setState(prev => ({
      ...prev, // Reuse todos, filter, sortBy references
      ui: { ...prev.ui, sidebarOpen: !prev.ui.sidebarOpen }
    }));
  };
  
  // ✅ Update only affected slice
  const updateFilter = (newFilter) => {
    setState(prev => ({ ...prev, filter: newFilter }));
    // todos, sortBy, ui maintain same references
  };
  
  return <div>Todos</div>;
}

Example: State splitting for performance

// ❌ Monolithic state - sidebar toggle re-renders entire app
function BadApp() {
  const [state, setState] = useState({
    todos: [], // Rarely changes
    user: {}, // Rarely changes
    sidebarOpen: true, // Changes frequently
    currentView: 'list' // Changes frequently
  });
  
  // Every state change re-renders everything
  return <div>...</div>;
}

// ✅ Split state - localized re-renders
function GoodApp() {
  // Separate state by update frequency
  const [todos, setTodos] = useState([]);
  const [user, setUser] = useState({});
  const [sidebarOpen, setSidebarOpen] = useState(true);
  const [currentView, setCurrentView] = useState('list');
  
  // Or split into contexts for different concerns
  return (
    <UserContext.Provider value={user}>
      <TodoContext.Provider value={todos}>
        <UIContext.Provider value={{ sidebarOpen, currentView }}>
          <Content />
        </UIContext.Provider>
      </TodoContext.Provider>
    </UserContext.Provider>
  );
}

// Components using only UI state won't re-render when todos change
function Sidebar() {
  const { sidebarOpen } = useContext(UIContext);
  return <aside>{sidebarOpen && 'Sidebar'}</aside>;
}

Immutability Best Practices Summary

  • Always create new references for objects and arrays when updating
  • Use spread operators for shallow updates, Immer for deep nesting
  • Normalize state for relational data (byId + allIds pattern)
  • Never mutate state directly - use functional updates
  • Bail out of updates by returning same reference when unchanged
  • Apply structural sharing - only copy changed parts
  • Split state by update frequency to minimize re-renders
  • Avoid deep cloning (JSON stringify/parse) - use selective copying

6. Derived State and Computed Values

6.1 useMemo Hook for Expensive Calculations

useMemo Hook Syntax

Feature Syntax Description
Basic Syntax const value = useMemo(() => computation, [deps]) Memoizes expensive calculation result
Return Value Memoized value Returns cached value if dependencies unchanged
Dependencies [dep1, dep2, ...] Recalculates only when dependencies change
Empty Deps [] Calculate once on mount
No Deps undefined ⚠️ Recalculates every render (no memoization)

useMemo vs Recalculation Comparison

Aspect Without useMemo With useMemo
Calculation Timing Every render Only when dependencies change
Performance May cause unnecessary work Optimized for expensive operations
Memory Lower memory overhead Stores cached result in memory
Referential Equality New reference each render Same reference if deps unchanged
Use Case Simple/cheap calculations Expensive computations, reference stability

Example: Expensive Filtering with useMemo

const FilteredList = ({ items, filterText }) => {
  // ❌ Without useMemo - filters on every render
  const filteredItems = items.filter(item => 
    item.name.toLowerCase().includes(filterText.toLowerCase())
  );

  // ✅ With useMemo - filters only when items or filterText change
  const memoizedItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => 
      item.name.toLowerCase().includes(filterText.toLowerCase())
    );
  }, [items, filterText]);

  return <ul>{memoizedItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
};

Example: Complex Calculation with useMemo

const DataAnalysis = ({ data }) => {
  const statistics = useMemo(() => {
    const sum = data.reduce((acc, val) => acc + val, 0);
    const avg = sum / data.length;
    const sorted = [...data].sort((a, b) => a - b);
    const median = sorted[Math.floor(sorted.length / 2)];
    
    return { sum, avg, median, count: data.length };
  }, [data]); // Only recalculate when data changes

  return (
    <div>
      <p>Sum: {statistics.sum}</p>
      <p>Average: {statistics.avg}</p>
      <p>Median: {statistics.median}</p>
    </div>
  );
};
Note: Only use useMemo for genuinely expensive calculations. Simple operations (string concatenation, basic math) don't benefit from memoization and add unnecessary complexity.

6.2 Derived State from Props and State Combinations

Derived State Patterns

Pattern Implementation Use Case
Direct Calculation const derived = computeValue(state, props) Simple derivations during render
useMemo Derivation const derived = useMemo(() => compute(), [deps]) Expensive calculations
Multi-source Derivation const result = combine(state1, state2, props) Combining multiple state sources
Conditional Derivation const value = condition ? calc1() : calc2() Different calculations based on conditions
❌ Derived State Anti-pattern useState(computeValue(props)) DON'T store derived values in state

Derived State vs Stored State Decision Matrix

Criteria Use Derived State (Calculate) Use Stored State (useState)
Source of Truth Can be computed from other values Independent source of truth
Synchronization Always in sync with dependencies Requires manual synchronization
Performance Cheap to calculate Expensive to recalculate every render
Updates Updates automatically Must be updated explicitly
Example fullName from firstName + lastName User input, toggle state

Example: Derived State from Multiple Sources

const ShoppingCart = ({ items, taxRate, discountCode }) => {
  const [selectedItems, setSelectedItems] = useState([]);

  // ✅ Derive subtotal from items (no state needed)
  const subtotal = useMemo(() => 
    selectedItems.reduce((sum, id) => {
      const item = items.find(i => i.id === id);
      return sum + (item?.price || 0);
    }, 0),
    [selectedItems, items]
  );

  // ✅ Derive discount from subtotal and code
  const discount = useMemo(() => {
    if (discountCode === 'SAVE20') return subtotal * 0.2;
    if (discountCode === 'SAVE10') return subtotal * 0.1;
    return 0;
  }, [subtotal, discountCode]);

  // ✅ Derive tax from subtotal after discount
  const tax = useMemo(() => 
    (subtotal - discount) * taxRate,
    [subtotal, discount, taxRate]
  );

  // ✅ Derive final total
  const total = subtotal - discount + tax;

  return (
    <div>
      <p>Subtotal: ${subtotal.toFixed(2)}</p>
      <p>Discount: -${discount.toFixed(2)}</p>
      <p>Tax: ${tax.toFixed(2)}</p>
      <p>Total: ${total.toFixed(2)}</p>
    </div>
  );
};
Anti-pattern Warning: Don't store derived values in state. This creates synchronization bugs when source values change but derived state isn't updated.

Example: ❌ Bad - Storing Derived State

// ❌ BAD - fullName stored in state can get out of sync
const UserProfile = ({ initialFirstName, initialLastName }) => {
  const [firstName, setFirstName] = useState(initialFirstName);
  const [lastName, setLastName] = useState(initialLastName);
  const [fullName, setFullName] = useState(`${initialFirstName} ${initialLastName}`);

  // ❌ Must manually sync fullName - error-prone!
  const handleFirstNameChange = (e) => {
    setFirstName(e.target.value);
    setFullName(`${e.target.value} ${lastName}`); // Can forget this!
  };

  return <input value={firstName} onChange={handleFirstNameChange} />;
};

// ✅ GOOD - Derive fullName from firstName and lastName
const UserProfile = ({ initialFirstName, initialLastName }) => {
  const [firstName, setFirstName] = useState(initialFirstName);
  const [lastName, setLastName] = useState(initialLastName);
  
  // ✅ Always in sync - no manual updates needed
  const fullName = `${firstName} ${lastName}`;

  return <input value={firstName} onChange={(e) => setFirstName(e.target.value)} />;
};

6.3 Selector Patterns for State Selection

Selector Pattern Types

Pattern Implementation Use Case
Simple Selector const value = state.property Direct property access
Computed Selector const derived = computeFromState(state) Transform or combine state values
Memoized Selector useMemo(() => selectFromState(state), [deps]) Expensive computations
Parametrized Selector const selector = (id) => state.find(i => i.id === id) Select with runtime parameters
Reselect Library createSelector([deps], computation) Advanced memoization across components

Selector Best Practices

Practice Recommendation Benefit
Keep Selectors Pure No side effects, same input = same output Predictable, testable, memoizable
Colocate with State Define selectors near state definition Easier to maintain and understand
Compose Selectors Build complex selectors from simple ones Reusability and maintainability
Minimize Derivations Only derive what components need Performance optimization
Use TypeScript Type selector functions for safety Type safety and autocomplete

Example: Selector Pattern for Complex State

// State structure
const [state, setState] = useState({
  users: [
    { id: 1, name: 'Alice', role: 'admin', active: true },
    { id: 2, name: 'Bob', role: 'user', active: false },
    { id: 3, name: 'Charlie', role: 'user', active: true }
  ],
  filters: { role: 'user', activeOnly: true }
});

// ✅ Simple selectors
const selectUsers = (state) => state.users;
const selectFilters = (state) => state.filters;

// ✅ Computed selector (memoized)
const selectFilteredUsers = useMemo(() => {
  const users = selectUsers(state);
  const filters = selectFilters(state);
  
  return users.filter(user => {
    if (filters.role && user.role !== filters.role) return false;
    if (filters.activeOnly && !user.active) return false;
    return true;
  });
}, [state.users, state.filters]);

// ✅ Parametrized selector
const selectUserById = (id) => 
  state.users.find(user => user.id === id);

// ✅ Composed selector
const selectActiveAdmins = useMemo(() => 
  selectUsers(state).filter(u => u.role === 'admin' && u.active),
  [state.users]
);

Example: Reselect Library Pattern

import { createSelector } from 'reselect';

// Input selectors (not memoized)
const selectUsers = (state) => state.users;
const selectSearchTerm = (state) => state.searchTerm;
const selectSortOrder = (state) => state.sortOrder;

// Memoized selector - only recomputes when inputs change
const selectFilteredSortedUsers = createSelector(
  [selectUsers, selectSearchTerm, selectSortOrder],
  (users, searchTerm, sortOrder) => {
    // Expensive filtering
    const filtered = users.filter(user => 
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    
    // Expensive sorting
    return [...filtered].sort((a, b) => {
      if (sortOrder === 'asc') return a.name.localeCompare(b.name);
      return b.name.localeCompare(a.name);
    });
  }
);

// Usage in component
const UserList = () => {
  const [state, setState] = useState({/* ... */});
  
  // Only recomputes when users, searchTerm, or sortOrder change
  const users = selectFilteredSortedUsers(state);
  
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
};
Note: For simple state selection, plain JavaScript is sufficient. Use libraries like Reselect only when you need advanced memoization across multiple components.

6.4 Custom Hooks for Derived State Logic

Custom Hook Patterns for Derived State

Pattern Purpose Example
useComputed Encapsulate derived state logic useFilteredItems(items, filter)
useSelector Select and derive from complex state useUsersByRole(state, 'admin')
useDebounced Debounced derived value useDebouncedSearch(searchTerm, 300)
useMemoizedValue Reusable memoization wrapper useMemoizedValue(compute, deps)
useTransform Transform data with reusable logic useTransform(data, transformer)

Example: useFilteredItems Custom Hook

// ✅ Custom hook for filtered and sorted items
const useFilteredItems = (items, filterText, sortKey) => {
  return useMemo(() => {
    // Filter items
    const filtered = items.filter(item => 
      item.name.toLowerCase().includes(filterText.toLowerCase())
    );
    
    // Sort items
    if (sortKey) {
      return [...filtered].sort((a, b) => {
        if (a[sortKey] < b[sortKey]) return -1;
        if (a[sortKey] > b[sortKey]) return 1;
        return 0;
      });
    }
    
    return filtered;
  }, [items, filterText, sortKey]);
};

// Usage in component
const ItemList = ({ items }) => {
  const [filterText, setFilterText] = useState('');
  const [sortKey, setSortKey] = useState('name');
  
  // Clean, reusable derived state logic
  const filteredItems = useFilteredItems(items, filterText, sortKey);
  
  return (
    <div>
      <input 
        value={filterText} 
        onChange={(e) => setFilterText(e.target.value)} 
        placeholder="Filter..."
      />
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
};

Example: useDebounced Custom Hook for Derived State

// ✅ Custom hook for debounced derived value
const useDebounced = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
};

// Usage in search component
const SearchComponent = ({ items }) => {
  const [searchTerm, setSearchTerm] = useState('');
  
  // Debounce search term to avoid expensive filtering on every keystroke
  const debouncedSearch = useDebounced(searchTerm, 300);
  
  // Only filter when debounced value changes
  const results = useMemo(() => 
    items.filter(item => 
      item.name.toLowerCase().includes(debouncedSearch.toLowerCase())
    ),
    [items, debouncedSearch] // Uses debounced value, not immediate searchTerm
  );
  
  return (
    <div>
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <p>Found {results.length} results</p>
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
};

Example: useSelector Pattern with Context

// State context
const StoreContext = createContext(null);

// ✅ Custom selector hook
const useSelector = (selector) => {
  const state = useContext(StoreContext);
  return useMemo(() => selector(state), [state, selector]);
};

// Reusable selectors
const selectUsers = (state) => state.users;
const selectActiveUsers = (state) => 
  state.users.filter(u => u.active);
const selectUserById = (state, id) => 
  state.users.find(u => u.id === id);

// Usage in components
const UserList = () => {
  const users = useSelector(selectUsers);
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
};

const ActiveUserCount = () => {
  const activeUsers = useSelector(selectActiveUsers);
  return <p>Active users: {activeUsers.length}</p>;
};

const UserDetail = ({ userId }) => {
  const user = useSelector(state => selectUserById(state, userId));
  return <div>{user?.name}</div>;
};
Best Practice: Custom hooks for derived state improve code reusability, testability, and maintainability. Extract complex derivation logic into custom hooks when it's used in multiple components.

6.5 Avoiding Derived State Anti-patterns

Common Derived State Anti-patterns

Anti-pattern Problem Solution
Storing Derived Data in State Synchronization bugs, redundant state Calculate derived values during render
useEffect to Sync Derived State Unnecessary renders, complexity Derive directly or use useMemo
Props in State (Derived from Props) Stale values when props change Use props directly or derive with key
Premature Memoization Complexity without performance benefit Profile first, optimize if needed
Over-normalization Complex derivations for simple data Keep data structure simple when possible

Anti-pattern Detection Checklist

Question Red Flag (Anti-pattern) Green Light (Good Pattern)
Can this be computed? Using useState for computable value Deriving value from existing state
Is useEffect updating state? useEffect syncing derived value to state Direct calculation or useMemo
Does state depend on props? useState(props.value) Use props directly or key prop reset
Is calculation expensive? useMemo for trivial operations Plain calculation or useMemo if profiled

Anti-pattern #1: Storing Derived Data in State

// ❌ BAD - Storing derived fullName in state
const UserForm = () => {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState(''); // ❌ Redundant state!
  
  // ❌ Must manually keep fullName in sync
  useEffect(() => {
    setFullName(`${firstName} ${lastName}`);
  }, [firstName, lastName]); // Extra render cycle!
  
  return <div>{fullName}</div>;
};

// ✅ GOOD - Derive fullName during render
const UserForm = () => {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  
  // ✅ Always in sync, no extra render
  const fullName = `${firstName} ${lastName}`;
  
  return <div>{fullName}</div>;
};

Anti-pattern #2: Props in State (Mirror Props)

// ❌ BAD - Mirroring props in state
const UserProfile = ({ user }) => {
  const [currentUser, setCurrentUser] = useState(user); // ❌ Stale when props change!
  
  // ❌ Requires useEffect to sync
  useEffect(() => {
    setCurrentUser(user);
  }, [user]);
  
  return <div>{currentUser.name}</div>;
};

// ✅ GOOD - Use props directly
const UserProfile = ({ user }) => {
  return <div>{user.name}</div>;
};

// ✅ ALTERNATIVE - If you need to modify, derive initial state with key
const UserProfile = ({ user }) => {
  const [edits, setEdits] = useState({});
  
  // Derive current values from props + edits
  const currentName = edits.name ?? user.name;
  const currentEmail = edits.email ?? user.email;
  
  return (
    <div>
      <input value={currentName} onChange={(e) => 
        setEdits(prev => ({ ...prev, name: e.target.value }))} 
      />
    </div>
  );
};

// ✅ OR use key prop to reset state when user changes
const UserProfile = ({ user }) => {
  const [name, setName] = useState(user.name);
  return <input value={name} onChange={(e) => setName(e.target.value)} />;
};

// Usage with key
<UserProfile key={user.id} user={user} /> // Remounts when user.id changes

Anti-pattern #3: Premature useMemo Optimization

// ❌ BAD - Unnecessary useMemo for simple operation
const Component = ({ firstName, lastName }) => {
  // ❌ Overkill - string concatenation is cheap
  const fullName = useMemo(() => 
    `${firstName} ${lastName}`, 
    [firstName, lastName]
  );
  
  return <div>{fullName}</div>;
};

// ✅ GOOD - Just calculate it
const Component = ({ firstName, lastName }) => {
  const fullName = `${firstName} ${lastName}`; // Fast enough!
  return <div>{fullName}</div>;
};

// ✅ USE useMemo when calculation is actually expensive
const Component = ({ items }) => {
  const sortedAndFiltered = useMemo(() => {
    // Expensive: filter + sort + transform large array
    return items
      .filter(item => item.active)
      .sort((a, b) => a.timestamp - b.timestamp)
      .map(item => ({ ...item, formatted: formatComplexData(item) }));
  }, [items]); // Worth memoizing
  
  return <div>{sortedAndFiltered.length} items</div>;
};

Derived State Best Practices Summary:

  • Calculate during render - Don't store computable values in state
  • Use useMemo selectively - Only for expensive operations (profile first)
  • Avoid props in state - Use props directly or derive with key prop reset
  • No useEffect for sync - Derive directly instead of syncing with effects
  • Single source of truth - Keep one canonical source, derive everything else
  • Custom hooks - Extract complex derivation logic for reusability
  • Selector patterns - Use selectors for complex state transformations
  • Test derivations - Pure derivation functions are easy to test

7. State and Side Effects Integration

7.1 useEffect Hook with State Dependencies

useEffect Hook Syntax and State Dependencies

Pattern Syntax Behavior
Basic useEffect useEffect(() => { /* effect */ }) Runs after every render
Empty Dependencies useEffect(() => { /* effect */ }, []) Runs only on mount
State Dependencies useEffect(() => { /* effect */ }, [state]) Runs when state changes
Multiple Dependencies useEffect(() => { /* effect */ }, [a, b, c]) Runs when any dependency changes
With Cleanup useEffect(() => { return () => cleanup(); }, [deps]) Cleanup runs before next effect and unmount

Common useEffect with State Patterns

Pattern Use Case Example
Fetch Data on State Change Load data when filter/query changes useEffect(() => fetchData(query), [query])
Sync State to localStorage Persist state changes useEffect(() => localStorage.set(key, state), [state])
Update Document Title Reflect state in browser title useEffect(() => document.title = title, [title])
Focus Input on State Focus element when condition changes useEffect(() => inputRef.current?.focus(), [isEdit])
Trigger Animation Start animation on state change useEffect(() => animate(), [isVisible])

Example: Fetch Data on State Change

const UserList = () => {
  const [users, setUsers] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [loading, setLoading] = useState(false);

  // ✅ Fetch data when searchTerm changes
  useEffect(() => {
    const fetchUsers = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users?search=${searchTerm}`);
        const data = await response.json();
        setUsers(data);
      } catch (error) {
        console.error('Failed to fetch users:', error);
      } finally {
        setLoading(false);
      }
    };

    if (searchTerm) {
      fetchUsers();
    } else {
      setUsers([]); // Clear users when search is empty
    }
  }, [searchTerm]); // Re-run when searchTerm changes

  return (
    <div>
      <input 
        value={searchTerm} 
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      {loading ? <p>Loading...</p> : <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>}
    </div>
  );
};

Example: Sync State to localStorage

const usePersistentState = (key, initialValue) => {
  // Initialize from localStorage
  const [state, setState] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored ? JSON.parse(stored) : initialValue;
  });

  // ✅ Sync to localStorage whenever state changes
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(state));
  }, [key, state]); // Dependencies include key and state

  return [state, setState];
};

// Usage
const TodoApp = () => {
  const [todos, setTodos] = usePersistentState('todos', []);
  
  const addTodo = (text) => {
    setTodos(prev => [...prev, { id: Date.now(), text }]);
  };
  
  // State automatically persists to localStorage
  return <div>{/* ... */}</div>;
};
Warning: Avoid infinite loops! Don't update a state variable inside useEffect that's also in the dependency array without proper conditions. Always include all dependencies that are used inside the effect to avoid stale closures.

Example: ❌ Infinite Loop Anti-pattern

// ❌ BAD - Infinite loop!
const BadComponent = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    setCount(count + 1); // Updates count on every render
  }, [count]); // count is dependency, creates infinite loop!
  
  return <div>{count}</div>;
};

// ✅ GOOD - Conditional update
const GoodComponent = () => {
  const [count, setCount] = useState(0);
  const [maxReached, setMaxReached] = useState(false);
  
  useEffect(() => {
    if (count < 10 && !maxReached) {
      setCount(count + 1);
    } else {
      setMaxReached(true);
    }
  }, [count, maxReached]); // Safe with condition
  
  return <div>{count}</div>;
};

7.2 State Synchronization with External Systems

External System Synchronization Patterns

System Type Synchronization Method Use Case
Browser APIs useEffect with state dependencies Window resize, scroll, media queries
Web Storage useEffect + storage events localStorage, sessionStorage persistence
WebSocket useEffect with connection management Real-time data updates
Third-party Libraries useSyncExternalStore (React 18+) Redux, MobX, external stores
Timers/Intervals useEffect with cleanup Polling, countdowns, animations
DOM Mutations useEffect with MutationObserver Track DOM changes outside React

useSyncExternalStore Hook (React 18+)

Feature Description Example
Purpose Subscribe to external store safely Concurrent rendering compatible
Syntax useSyncExternalStore(subscribe, getSnapshot) Returns current snapshot value
Subscribe Function (callback) => unsubscribe Called when store subscribes/unsubscribes
GetSnapshot Function () => value Returns current store value
Tearing Prevention Prevents inconsistent UI during concurrent render Critical for external stores

Example: Window Resize State Synchronization

const useWindowSize = () => {
  const [windowSize, setWindowSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    // ✅ Sync state with window resize events
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    window.addEventListener('resize', handleResize);
    
    // Cleanup subscription on unmount
    return () => window.removeEventListener('resize', handleResize);
  }, []); // Empty deps - setup once on mount

  return windowSize;
};

// Usage
const ResponsiveComponent = () => {
  const { width, height } = useWindowSize();
  
  return (
    <div>
      <p>Window size: {width} x {height}</p>
      {width < 768 ? <MobileView /> : <DesktopView />}
    </div>
  );
};

Example: WebSocket State Synchronization

const useWebSocket = (url) => {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    // ✅ Connect to WebSocket and sync messages to state
    const ws = new WebSocket(url);

    ws.onopen = () => {
      setIsConnected(true);
      console.log('WebSocket connected');
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      setMessages(prev => [...prev, message]);
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      setIsConnected(false);
    };

    ws.onclose = () => {
      setIsConnected(false);
      console.log('WebSocket disconnected');
    };

    // ✅ Cleanup - close connection on unmount
    return () => {
      ws.close();
    };
  }, [url]); // Reconnect if URL changes

  const sendMessage = (message) => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(message));
    }
  };

  return { messages, isConnected, sendMessage };
};

Example: useSyncExternalStore with Custom Store

// Custom external store
class CounterStore {
  constructor() {
    this.count = 0;
    this.listeners = new Set();
  }

  subscribe = (callback) => {
    this.listeners.add(callback);
    return () => this.listeners.delete(callback);
  };

  getSnapshot = () => {
    return this.count;
  };

  increment = () => {
    this.count++;
    this.listeners.forEach(listener => listener());
  };
}

const counterStore = new CounterStore();

// ✅ Use with useSyncExternalStore (React 18+)
const useCounter = () => {
  const count = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getSnapshot
  );

  return { count, increment: counterStore.increment };
};

// Usage
const Counter = () => {
  const { count, increment } = useCounter();
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};
Note: When syncing with external systems, always clean up subscriptions in the useEffect cleanup function to prevent memory leaks. Use useSyncExternalStore for external stores in React 18+ to ensure concurrent rendering compatibility.

7.3 Cleanup Functions and State Management

Cleanup Function Patterns

Pattern Purpose Example
Event Listener Cleanup Remove DOM event listeners return () => el.removeEventListener()
Timer Cleanup Clear timeouts and intervals return () => clearTimeout(timerId)
Subscription Cleanup Unsubscribe from observables return () => subscription.unsubscribe()
Request Cancellation Cancel pending async requests return () => abortController.abort()
Connection Cleanup Close WebSocket/network connections return () => ws.close()
Resource Release Release memory, object URLs return () => URL.revokeObjectURL(url)

Cleanup Function Execution Timing

Scenario When Cleanup Runs Purpose
Component Unmount Before component is removed from DOM Final cleanup before removal
Effect Re-run Before effect runs again Clean up previous effect's resources
Dependencies Change Before effect with new dependencies Reset for new dependency values
Strict Mode (Dev) After mount, then remount immediately Test cleanup function correctness

Example: Timer Cleanup

const Countdown = ({ initialSeconds }) => {
  const [seconds, setSeconds] = useState(initialSeconds);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    if (!isActive) return; // Don't start timer if not active

    // ✅ Start interval
    const intervalId = setInterval(() => {
      setSeconds(prev => {
        if (prev <= 1) {
          setIsActive(false);
          return 0;
        }
        return prev - 1;
      });
    }, 1000);

    // ✅ Cleanup - clear interval on unmount or when isActive changes
    return () => {
      clearInterval(intervalId);
      console.log('Timer cleaned up');
    };
  }, [isActive]); // Re-run when isActive changes

  return (
    <div>
      <p>Time remaining: {seconds}s</p>
      <button onClick={() => setIsActive(!isActive)}>
        {isActive ? 'Pause' : 'Start'}
      </button>
    </div>
  );
};

Example: Abort Controller for Request Cancellation

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Create AbortController for this request
    const abortController = new AbortController();
    
    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal // Pass abort signal
        });
        
        if (!response.ok) throw new Error('Failed to fetch');
        
        const data = await response.json();
        setUser(data);
      } catch (err) {
        // ✅ Ignore abort errors (expected on cleanup)
        if (err.name === 'AbortError') {
          console.log('Request aborted');
          return;
        }
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // ✅ Cleanup - abort pending request
    return () => {
      abortController.abort();
      console.log('Fetch aborted for userId:', userId);
    };
  }, [userId]); // Re-run when userId changes, canceling previous request

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  return <div>{user?.name}</div>;
};

Example: Multiple Cleanup Operations

const ChatRoom = ({ roomId }) => {
  const [messages, setMessages] = useState([]);
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    let ws;
    let reconnectTimer;

    const connect = () => {
      ws = new WebSocket(`wss://chat.example.com/${roomId}`);

      ws.onopen = () => setIsConnected(true);
      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        setMessages(prev => [...prev, message]);
      };
      ws.onclose = () => {
        setIsConnected(false);
        // Attempt reconnect after 5 seconds
        reconnectTimer = setTimeout(connect, 5000);
      };
    };

    connect();

    // ✅ Cleanup multiple resources
    return () => {
      console.log('Cleaning up chat room:', roomId);
      
      // Clear reconnect timer if pending
      if (reconnectTimer) {
        clearTimeout(reconnectTimer);
      }
      
      // Close WebSocket connection
      if (ws) {
        ws.onclose = null; // Prevent reconnect attempt
        ws.close();
      }
      
      // Clear messages on room change
      setMessages([]);
    };
  }, [roomId]); // Cleanup and reconnect when roomId changes

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      <ul>{messages.map((m, i) => <li key={i}>{m.text}</li>)}</ul>
    </div>
  );
};
Warning: Always return a cleanup function from useEffect when dealing with subscriptions, timers, or async operations. Forgetting cleanup can lead to memory leaks, state updates on unmounted components, and race conditions.

7.4 State and Subscription Patterns

Subscription Pattern Types

Pattern Implementation Use Case
Observable Subscription useEffect + observable.subscribe() RxJS, streams, event emitters
Event Emitter Pattern useEffect + emitter.on/off Node.js EventEmitter, custom events
PubSub Pattern useEffect + subscribe/unsubscribe Message buses, global events
Store Subscription useSyncExternalStore Redux, Zustand, external stores
Real-time Data useEffect + WebSocket/SSE Live updates, notifications

Subscription Best Practices

Practice Recommendation Benefit
Always Unsubscribe Return cleanup function Prevent memory leaks
Stable Callbacks Use useCallback for handlers Avoid unnecessary resubscriptions
Error Handling Handle subscription errors gracefully Robust error recovery
Loading States Track subscription connection status Better UX with loading indicators
Reconnection Logic Implement exponential backoff Resilient connections

Example: RxJS Observable Subscription

import { interval } from 'rxjs';
import { map, filter } from 'rxjs/operators';

const useObservable = (observable$, initialValue) => {
  const [value, setValue] = useState(initialValue);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Subscribe to observable
    const subscription = observable$.subscribe({
      next: (data) => setValue(data),
      error: (err) => setError(err),
      complete: () => console.log('Observable completed')
    });

    // ✅ Cleanup - unsubscribe
    return () => {
      subscription.unsubscribe();
      console.log('Unsubscribed from observable');
    };
  }, [observable$]);

  return { value, error };
};

// Usage
const TickerComponent = () => {
  // Create observable that emits even numbers every second
  const ticker$ = interval(1000).pipe(
    map(n => n * 2),
    filter(n => n < 20)
  );

  const { value, error } = useObservable(ticker$, 0);

  if (error) return <p>Error: {error.message}</p>;
  return <p>Current value: {value}</p>;
};

Example: Event Emitter Subscription

// Custom event emitter
class EventBus {
  constructor() {
    this.listeners = new Map();
  }

  on(event, callback) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event).add(callback);
  }

  off(event, callback) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).delete(callback);
    }
  }

  emit(event, data) {
    if (this.listeners.has(event)) {
      this.listeners.get(event).forEach(callback => callback(data));
    }
  }
}

const eventBus = new EventBus();

// ✅ Custom hook for event subscription
const useEventBus = (event, handler) => {
  useEffect(() => {
    // Subscribe to event
    eventBus.on(event, handler);

    // ✅ Cleanup - unsubscribe
    return () => {
      eventBus.off(event, handler);
      console.log(`Unsubscribed from ${event}`);
    };
  }, [event, handler]); // Re-subscribe if event or handler changes
};

// Usage
const NotificationListener = () => {
  const [notifications, setNotifications] = useState([]);

  // ✅ Subscribe to 'notification' events
  useEventBus('notification', (data) => {
    setNotifications(prev => [...prev, data]);
  });

  return (
    <ul>
      {notifications.map((n, i) => <li key={i}>{n.message}</li>)}
    </ul>
  );
};

Example: Server-Sent Events (SSE) Subscription

const useServerSentEvents = (url) => {
  const [events, setEvents] = useState([]);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // ✅ Create EventSource for SSE
    const eventSource = new EventSource(url);

    eventSource.onopen = () => {
      setIsConnected(true);
      setError(null);
      console.log('SSE connection opened');
    };

    eventSource.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setEvents(prev => [...prev, data]);
    };

    eventSource.onerror = (err) => {
      setIsConnected(false);
      setError('Connection failed');
      console.error('SSE error:', err);
    };

    // ✅ Cleanup - close connection
    return () => {
      eventSource.close();
      console.log('SSE connection closed');
    };
  }, [url]); // Reconnect if URL changes

  return { events, isConnected, error };
};

// Usage
const LiveFeed = () => {
  const { events, isConnected, error } = useServerSentEvents('/api/live-feed');

  return (
    <div>
      <p>Status: {isConnected ? 'Connected' : 'Disconnected'}</p>
      {error && <p>Error: {error}</p>}
      <ul>{events.map((e, i) => <li key={i}>{e.message}</li>)}</ul>
    </div>
  );
};
Best Practice: When working with subscriptions, use useCallback to memoize event handlers to prevent unnecessary unsubscribe/resubscribe cycles. Always test that cleanup functions properly unsubscribe to avoid memory leaks.

7.5 Race Condition Prevention in State Updates

Common Race Condition Scenarios

Scenario Problem Solution
Multiple Async Requests Older request completes after newer one AbortController, request ID tracking
Rapid State Changes Intermediate states get overwritten Debouncing, throttling, latest request wins
Stale Closure Effect uses old state value Functional state updates, proper dependencies
Unmounted Component Update setState called after unmount Cleanup flag, AbortSignal
Concurrent Requests Multiple requests update same state Request deduplication, cancel in-flight

Race Condition Prevention Techniques

Technique Implementation Use Case
AbortController Cancel previous requests Search, autocomplete, pagination
Request ID Tracking Ignore responses from stale requests Sequential requests with changing params
Cleanup Flag Track if component is mounted Prevent state updates after unmount
Debouncing Delay request until input settles Search input, form validation
Optimistic Updates Update UI immediately, rollback on error Form submissions, toggle actions
State Machines Explicit state transitions Complex async flows

Example: AbortController to Prevent Race Conditions

const SearchResults = () => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }

    // ✅ Create AbortController for this search
    const abortController = new AbortController();
    
    const searchProducts = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortController.signal // Cancel if query changes
        });
        
        const data = await response.json();
        
        // ✅ Only update state if not aborted
        setResults(data);
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('Search aborted for:', query);
          return; // Don't update state on abort
        }
        console.error('Search error:', error);
      } finally {
        setLoading(false);
      }
    };

    searchProducts();

    // ✅ Cleanup - abort previous search when query changes
    return () => {
      abortController.abort();
    };
  }, [query]); // New query = abort previous search

  return (
    <div>
      <input 
        value={query} 
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {loading && <p>Searching...</p>}
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
};

Example: Request ID Tracking

const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isCurrent = true; // ✅ Flag to track if this request is current
    let requestId = Symbol(); // Unique ID for this request

    const fetchUser = async () => {
      setLoading(true);
      
      try {
        // Simulate network delay
        await new Promise(resolve => setTimeout(resolve, 
          Math.random() * 2000)); // Random delay 0-2s
        
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        // ✅ Only update if this is still the current request
        if (isCurrent) {
          setUser(data);
          console.log('Updated user:', userId, requestId.toString());
        } else {
          console.log('Ignored stale response for:', userId, requestId.toString());
        }
      } catch (error) {
        if (isCurrent) {
          console.error('Error fetching user:', error);
        }
      } finally {
        if (isCurrent) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // ✅ Cleanup - mark request as stale
    return () => {
      isCurrent = false;
      console.log('Marking stale:', userId, requestId.toString());
    };
  }, [userId]); // New userId = previous request marked stale

  if (loading) return <p>Loading...</p>;
  return <div>{user?.name}</div>;
};

Example: Debounced Search with Race Condition Prevention

const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
};

const DebouncedSearch = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  const debouncedSearch = useDebounce(searchTerm, 300); // 300ms delay

  useEffect(() => {
    if (!debouncedSearch) {
      setResults([]);
      return;
    }

    const abortController = new AbortController();

    const performSearch = async () => {
      try {
        const response = await fetch(
          `/api/search?q=${debouncedSearch}`,
          { signal: abortController.signal }
        );
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Search error:', error);
        }
      }
    };

    performSearch();

    // ✅ Double protection: debounce + abort
    return () => abortController.abort();
  }, [debouncedSearch]); // Only search when debounced value changes

  return (
    <div>
      <input 
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Type to search..."
      />
      <p>Searching for: {debouncedSearch}</p>
      <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
    </div>
  );
};
Race Condition Warning: Never rely on the order of async operations completing. Always use AbortController, request ID tracking, or cleanup flags to ensure only the latest or relevant responses update state. Test with artificial delays to expose race conditions during development.

State and Side Effects Best Practices Summary:

  • Always cleanup - Return cleanup function from useEffect for subscriptions/timers
  • Abort requests - Use AbortController to cancel stale async requests
  • Track mount status - Prevent state updates on unmounted components
  • Debounce inputs - Reduce unnecessary API calls for rapid user input
  • Handle race conditions - Use request IDs or abort signals
  • Sync carefully - Be cautious with infinite loops in useEffect
  • Use proper deps - Include all values used in effect to avoid stale closures
  • Test cleanup - Verify cleanup functions prevent memory leaks

8. Performance Optimization for State Management

8.1 React.memo and State Change Prevention

Technique Syntax Purpose When to Use
React.memo React.memo(Component) Memoizes component, prevents re-render if props unchanged Pure components receiving same props frequently
Custom comparison React.memo(Comp, areEqual) Custom equality function for props comparison Complex props or specific comparison logic needed
Prop stability Stable references with useCallback/useMemo Ensures props don't change unnecessarily When passing callbacks/objects as props to memoized components
Children pattern {children} prop Children don't re-render when parent state changes Wrap expensive components in parent with local state

Example: React.memo preventing unnecessary re-renders

import { memo, useState } from 'react';

// ❌ Without memo - re-renders on every parent update
const ExpensiveChild = ({ value }) => {
  console.log('ExpensiveChild rendered');
  return <div>{value}</div>;
};

// ✅ With memo - only re-renders when value prop changes
const MemoizedChild = memo(({ value }) => {
  console.log('MemoizedChild rendered');
  return <div>{value}</div>;
});

// ✅ Custom comparison for complex props
const CustomMemoChild = memo(
  ({ user, settings }) => <div>{user.name}: {settings.theme}</div>,
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id &&
           prevProps.settings.theme === nextProps.settings.theme;
  }
);

function Parent() {
  const [count, setCount] = useState(0);
  const [value] = useState('constant');

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild value={value} /> {/* Re-renders every time */}
      <MemoizedChild value={value} />  {/* Only renders once */}
    </>
  );
}
React.memo Best Practices:
  • Use for expensive components that receive same props frequently
  • Don't overuse - has its own overhead for shallow prop comparison
  • Ensure all props passed are stable (use useCallback/useMemo)
  • Custom comparison should be cheap - avoid deep equality checks
  • Children prop bypasses memo - use composition for optimization

8.2 useCallback Hook for Event Handler Optimization

Pattern Syntax Purpose Dependencies
useCallback useCallback(fn, deps) Memoizes function reference between renders Values used inside callback function
Event handler useCallback((e) => {...}, []) Stable reference for event handlers Empty if no external values used
With state useCallback(() => {...}, [state]) Callback using current state value Include state variables in dependency array
Functional update useCallback(() => setState(s => s + 1), []) Update state without depending on current value Empty deps - uses functional setState

Example: useCallback optimization patterns

import { useState, useCallback, memo } from 'react';

const Button = memo(({ onClick, label }) => {
  console.log(`Rendering ${label}`);
  return <button onClick={onClick}>{label}</button>;
});

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [count, setCount] = useState(0);

  // ❌ New function every render - breaks Button memo
  const handleBadAdd = () => {
    setTodos([...todos, { id: Date.now(), text: 'New' }]);
  };

  // ✅ Stable reference with functional update - empty deps
  const handleAdd = useCallback(() => {
    setTodos(prev => [...prev, { id: Date.now(), text: 'New' }]);
  }, []); // No dependencies needed with functional update

  // ✅ With dependencies - recreates when todos changes
  const handleClear = useCallback(() => {
    if (todos.length > 0) {
      setTodos([]);
    }
  }, [todos]); // Depends on todos.length check

  // ✅ Best: Use functional update to avoid dependency
  const handleClearBetter = useCallback(() => {
    setTodos(prev => prev.length > 0 ? [] : prev);
  }, []); // No dependencies - optimal

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <Button onClick={handleAdd} label="Add Todo" />
      <Button onClick={handleClearBetter} label="Clear" />
    </div>
  );
}
useCallback Pitfalls: Don't use useCallback everywhere - it has overhead. Only use when: (1) Passing callback to memoized child component, (2) Callback is dependency of useEffect/useMemo, (3) Callback creates expensive closure. Premature optimization can make code harder to read.

8.3 State Splitting and Component Granularity

Strategy Implementation Benefit Use Case
Split state by concern Multiple useState calls instead of one object Components re-render only when relevant state changes Independent state variables that update separately
Collocate state Move state to component that uses it Reduces parent re-renders, limits update scope State only used by single child component
Lift state down Push state to lowest common ancestor Prevents sibling components from re-rendering State shared by subset of children
Extract components Separate stateful logic into smaller components Isolates re-renders to affected component tree Large components with multiple state updates

❌ Bad: Single state object causes unnecessary re-renders

function Form() {
  // All in one state
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    count: 0
  });

  // Every keystroke re-renders entire form
  // Even the counter that doesn't care about form inputs
  return (
    <>
      <input 
        value={formData.name}
        onChange={e => setFormData({
          ...formData, 
          name: e.target.value
        })}
      />
      <ExpensiveCounter count={formData.count} />
    </>
  );
}

✅ Good: Split state by concern

function Form() {
  // Split independent state
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [count, setCount] = useState(0);

  // Typing in name only re-renders
  // name input, not counter
  return (
    <>
      <input 
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <ExpensiveCounter count={count} />
    </>
  );
}

Example: Collocating state for performance

// ❌ State in parent - all children re-render
function Parent() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <Header /> {/* Re-renders on isOpen change */}
      <Sidebar isOpen={isOpen} setIsOpen={setIsOpen} />
      <Content /> {/* Re-renders unnecessarily */}
    </>
  );
}

// ✅ State collocated in component that needs it
function Parent() {
  return (
    <>
      <Header /> {/* Never re-renders */}
      <Sidebar /> {/* Manages own state */}
      <Content /> {/* Never re-renders */}
    </>
  );
}

function Sidebar() {
  const [isOpen, setIsOpen] = useState(false);
  // State isolated to this component
  return <div>...</div>;
}

8.4 useMemo for Expensive State Derivations

Pattern Syntax Purpose Dependencies
useMemo useMemo(() => computation, deps) Memoizes computed value, recalculates only when deps change Input values used in computation
Expensive calculation useMemo(() => expensiveFn(data), [data]) Avoid re-computing on every render Source data for calculation
Reference equality useMemo(() => ({...obj}), [obj]) Maintain stable object/array reference Primitive values that comprise object
Filtered/sorted data useMemo(() => items.filter(...), [items]) Cache derived collections Source collection and filter criteria

Example: useMemo for expensive computations

import { useState, useMemo } from 'react';

function DataTable({ data, sortKey }) {
  const [filter, setFilter] = useState('');

  // ❌ Recalculates on every render (even unrelated state changes)
  const badSortedData = data
    .filter(item => item.name.includes(filter))
    .sort((a, b) => a[sortKey] - b[sortKey]);

  // ✅ Only recalculates when data, filter, or sortKey changes
  const sortedData = useMemo(() => {
    console.log('Expensive sort calculation');
    return data
      .filter(item => item.name.includes(filter))
      .sort((a, b) => a[sortKey] - b[sortKey]);
  }, [data, filter, sortKey]);

  // ✅ Stable object reference for props
  const tableConfig = useMemo(() => ({
    rowHeight: 50,
    columnWidth: 100,
    virtualization: true
  }), []); // Empty deps - config never changes

  return <Table data={sortedData} config={tableConfig} />;
}

// Advanced: Memoize complex selector
function useFilteredItems(items, filters) {
  return useMemo(() => {
    return items.filter(item => {
      return Object.entries(filters).every(([key, value]) => {
        if (!value) return true; // Skip empty filters
        return item[key]?.toString().toLowerCase()
          .includes(value.toLowerCase());
      });
    });
  }, [items, filters]); // Recalc when items or filters change
}
When to use useMemo:
  • Expensive calculations - Heavy array operations, complex math, parsing
  • Reference equality matters - Object/array passed to memoized component or as effect dependency
  • Don't optimize prematurely - Profile first, then optimize bottlenecks
  • Avoid for cheap operations - useMemo overhead can exceed benefit

8.5 Context Performance and Re-render Optimization

Technique Implementation Benefit Trade-off
Split contexts Separate frequently/infrequently changing data into different contexts Components subscribe only to relevant data More context providers to manage
Memoize context value useMemo(() => ({state, actions}), [state]) Prevents context consumers re-rendering on every provider render Must remember to memoize and manage deps
Context selectors Custom hook to select subset of context Component only re-renders when selected value changes Requires additional abstraction layer
Component composition Pass children before context provider Children don't re-render on context changes Limited to components that don't need context

❌ Bad: All consumers re-render

const AppContext = createContext();

function Provider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  
  // New object every render!
  const value = {
    user, setUser,
    theme, setTheme
  };
  
  return (
    <AppContext.Provider value={value}>
      {children}
    </AppContext.Provider>
  );
}

// All consumers re-render even if
// they only use theme or only use user

✅ Good: Split contexts

const UserContext = createContext();
const ThemeContext = createContext();

function Provider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  
  const userValue = useMemo(
    () => ({ user, setUser }), 
    [user]
  );
  
  const themeValue = useMemo(
    () => ({ theme, setTheme }),
    [theme]
  );
  
  return (
    <UserContext.Provider value={userValue}>
      <ThemeContext.Provider value={themeValue}>
        {children}
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

Example: Context selector pattern for fine-grained subscriptions

import { createContext, useContext, useRef, useSyncExternalStore } from 'react';

// Create store outside React
function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();

  return {
    getState: () => state,
    setState: (newState) => {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener());
    },
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
}

const StoreContext = createContext(null);

// Provider component
function StoreProvider({ children }) {
  const storeRef = useRef();
  if (!storeRef.current) {
    storeRef.current = createStore({ count: 0, user: null, theme: 'light' });
  }

  return (
    <StoreContext.Provider value={storeRef.current}>
      {children}
    </StoreContext.Provider>
  );
}

// Selector hook - only re-renders when selected value changes
function useStoreSelector(selector) {
  const store = useContext(StoreContext);
  
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getState())
  );
}

// Usage - component only re-renders when count changes, not theme or user
function Counter() {
  const count = useStoreSelector(state => state.count);
  const store = useContext(StoreContext);
  
  return (
    <button onClick={() => store.setState({ count: count + 1 })}>
      {count}
    </button>
  );
}
Context Performance Warning: Every component using useContext re-renders when context value changes, even if using memo. Split contexts by update frequency and use selectors for fine-grained subscriptions. Consider state management libraries (Zustand, Jotai) for complex scenarios.

8.6 State Update Batching and flushSync Usage

Concept Behavior React Version Use Case
Automatic batching React 18 Multiple setState calls batched automatically in all contexts React 18+ Default behavior - optimal for most cases
Legacy batching Only batched in React event handlers (not timeouts, promises) React 17 and earlier Manual batching needed for async operations
flushSync() Forces synchronous DOM update immediately All versions DOM measurements, third-party integrations
unstable_batchedUpdates Manual batching for React 17 React 17 (deprecated in 18) Legacy apps - not needed in React 18

Example: Automatic batching in React 18

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  console.log('Render'); // How many times does this log?

  // React 17: 3 renders (event handler, then 2 more in timeout)
  // React 18: 2 renders (batched in both event handler AND timeout)
  function handleClick() {
    setCount(c => c + 1); // Batched
    setFlag(f => !f);     // Batched
    // Only 1 re-render for above updates

    setTimeout(() => {
      setCount(c => c + 1); // React 18: Batched ✅
      setFlag(f => !f);     // React 18: Batched ✅
      // React 18: Only 1 re-render
      // React 17: 2 re-renders (not batched in timeout)
    }, 0);
  }

  return <button onClick={handleClick}>Click</button>;
}

// React 18 batches in:
// - Event handlers ✅
// - setTimeout/setInterval ✅
// - Promise.then callbacks ✅
// - Native event handlers ✅
// - Any async code ✅

Example: flushSync for synchronous updates

import { useState, useRef } from 'react';
import { flushSync } from 'react-dom';

function ScrollExample() {
  const [items, setItems] = useState([1, 2, 3]);
  const listRef = useRef(null);

  function handleAdd() {
    // ❌ Without flushSync - scroll position won't be accurate
    setItems([...items, items.length + 1]);
    listRef.current.scrollTop = listRef.current.scrollHeight;
    // DOM hasn't updated yet, so scrollHeight is old value

    // ✅ With flushSync - forces immediate DOM update
    flushSync(() => {
      setItems([...items, items.length + 1]);
    });
    // Now DOM is updated, scrollHeight is correct
    listRef.current.scrollTop = listRef.current.scrollHeight;
  }

  return (
    <div ref={listRef} style={{ height: 200, overflow: 'auto' }}>
      {items.map(i => <div key={i}>Item {i}</div>)}
      <button onClick={handleAdd}>Add Item</button>
    </div>
  );
}

// Common flushSync use cases:
// 1. DOM measurements (offsetHeight, scrollHeight, getBoundingClientRect)
// 2. Focus management (element.focus())
// 3. Third-party library integration (chart libraries, D3)
// 4. Print dialogs (window.print())
// 5. Selection APIs (window.getSelection())
flushSync Performance Warning: flushSync disables batching and forces synchronous rendering, which hurts performance. Only use when you need immediate DOM updates for measurements or third-party integrations. Overuse can cause performance degradation.
React 17 Manual Batching (Legacy):
import { unstable_batchedUpdates } from 'react-dom';

// React 17 only
setTimeout(() => {
  unstable_batchedUpdates(() => {
    setCount(c => c + 1);
    setFlag(f => !f);
    // Now batched in React 17
  });
}, 0);
Opt-out of Batching (React 18):
import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCount(c => c + 1);
  }); // Render immediately
  
  flushSync(() => {
    setFlag(f => !f);
  }); // Render immediately again
  
  // 2 separate renders (not batched)
}

Performance Optimization Summary:

  • React.memo - Prevent re-renders for pure components with stable props
  • useCallback - Memoize callbacks passed to memoized children or effect deps
  • useMemo - Cache expensive calculations and maintain reference equality
  • Split state - Separate independent state for granular re-renders
  • Collocate state - Keep state close to where it's used
  • Split contexts - Separate fast/slow changing data into different contexts
  • Context selectors - Subscribe to subset of context for fine-grained updates
  • Automatic batching - React 18 batches all updates automatically
  • flushSync sparingly - Only for DOM measurements and third-party integrations
  • Profile first - Use React DevTools Profiler before optimizing

9. Asynchronous State Management Patterns

9.1 Loading State Patterns and Boolean Flags

Pattern State Structure Use Case Pros/Cons
Boolean flag const [isLoading, setIsLoading] Simple single async operation ✅ Simple, clear
❌ Can't track multiple operations
Status enum 'idle' | 'loading' | 'success' | 'error' Single operation with multiple states ✅ Mutually exclusive states
❌ Verbose for multiple operations
Multiple flags {isLoading, isRefreshing, isLoadingMore} Different types of loading states ✅ Granular control
❌ Can have invalid combinations
Request ID tracking const [loadingIds, setLoadingIds] Multiple concurrent operations ✅ Track individual requests
❌ More complex state management
Pattern 1: Boolean loading flag
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setIsLoading(true);
    
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setIsLoading(false);
      })
      .catch(() => setIsLoading(false));
  }, [userId]);

  if (isLoading) return <Spinner />;
  return <div>{user?.name}</div>;
}
Pattern 2: Status enum (recommended)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [status, setStatus] = useState('idle');

  useEffect(() => {
    setStatus('loading');
    
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setStatus('success');
      })
      .catch(() => setStatus('error'));
  }, [userId]);

  if (status === 'loading') return <Spinner />;
  if (status === 'error') return <Error />;
  return <div>{user?.name}</div>;
}

Example: Multiple loading states for different operations

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [loadingStates, setLoadingStates] = useState({
    initial: false,
    refreshing: false,
    loadingMore: false,
    deleting: new Set() // Track which items are being deleted
  });

  const fetchInitial = async () => {
    setLoadingStates(prev => ({ ...prev, initial: true }));
    const data = await fetch('/api/todos').then(r => r.json());
    setTodos(data);
    setLoadingStates(prev => ({ ...prev, initial: false }));
  };

  const refresh = async () => {
    setLoadingStates(prev => ({ ...prev, refreshing: true }));
    const data = await fetch('/api/todos').then(r => r.json());
    setTodos(data);
    setLoadingStates(prev => ({ ...prev, refreshing: false }));
  };

  const deleteTodo = async (id) => {
    setLoadingStates(prev => ({
      ...prev,
      deleting: new Set([...prev.deleting, id])
    }));
    
    await fetch(`/api/todos/${id}`, { method: 'DELETE' });
    
    setTodos(prev => prev.filter(t => t.id !== id));
    setLoadingStates(prev => {
      const newDeleting = new Set(prev.deleting);
      newDeleting.delete(id);
      return { ...prev, deleting: newDeleting };
    });
  };

  return (
    <div>
      {loadingStates.initial && <Spinner />}
      {loadingStates.refreshing && <RefreshIndicator />}
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          isDeleting={loadingStates.deleting.has(todo.id)}
          onDelete={() => deleteTodo(todo.id)}
        />
      ))}
    </div>
  );
}

9.2 Error State Handling and Error Boundaries

Approach Implementation Scope Best For
Local error state const [error, setError] = useState(null) Component level Recoverable errors, form validation, API errors
Error Boundaries Class component with componentDidCatch Component tree Unexpected errors, render errors, fallback UI
Global error state Context or state management library Application wide Toast notifications, centralized error logging
useErrorHandler hook Custom hook combining boundaries and state Flexible Reusable error handling logic

Example: Comprehensive error state management

// Pattern 1: Local error state with detailed error info
function DataFetcher() {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('idle');

  const fetchData = async () => {
    setStatus('loading');
    setError(null); // Clear previous errors
    
    try {
      const response = await fetch('/api/data');
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      const json = await response.json();
      setData(json);
      setStatus('success');
    } catch (err) {
      setError({
        message: err.message,
        timestamp: new Date().toISOString(),
        type: err.name,
        // Store additional context
        context: { url: '/api/data' }
      });
      setStatus('error');
    }
  };

  const retry = () => {
    setError(null);
    fetchData();
  };

  if (status === 'loading') return <Spinner />;
  
  if (error) {
    return (
      <div className="error">
        <h3>Error: {error.message}</h3>
        <p>Time: {error.timestamp}</p>
        <button onClick={retry}>Retry</button>
      </div>
    );
  }

  return <div>{JSON.stringify(data)}</div>;
}

Example: Error Boundary for catching render errors

import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so next render shows fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Log error to error reporting service
    console.error('Error caught by boundary:', error, errorInfo);
    
    this.setState({
      error: error,
      errorInfo: errorInfo
    });

    // Send to error tracking service (e.g., Sentry)
    // logErrorToService(error, errorInfo);
  }

  resetError = () => {
    this.setState({ hasError: false, error: null, errorInfo: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Something went wrong</h2>
          <details>
            <summary>Error details</summary>
            <pre>{this.state.error?.toString()}</pre>
            <pre>{this.state.errorInfo?.componentStack}</pre>
          </details>
          <button onClick={this.resetError}>Try Again</button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Usage
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}
Error Boundary Limitations:
  • Only catches errors in render, lifecycle methods, and constructors of child components
  • Does NOT catch errors in: event handlers, async code, server-side rendering, errors in boundary itself
  • For event handlers, use try/catch and local error state
  • For async operations, use Promise .catch() or try/catch with async/await

9.3 Success State and Data Management

Pattern State Design Purpose Example
Data + timestamp {data, lastUpdated} Track data freshness Cache invalidation, stale-while-revalidate
Optimistic updates {data, optimisticData} Instant UI feedback Social media likes, todo toggles
Paginated data {items, hasMore, nextCursor} Infinite scroll, pagination Feed scrolling, search results
Normalized data {byId: {}, allIds: []} Efficient updates, no duplication Redux-style entity management

Example: Success state with metadata

function useFetchWithMetadata(url) {
  const [state, setState] = useState({
    data: null,
    status: 'idle',
    error: null,
    // Metadata
    lastUpdated: null,
    stale: false,
    refetchCount: 0
  });

  const fetchData = async (isRefetch = false) => {
    setState(prev => ({
      ...prev,
      status: 'loading',
      error: null,
      refetchCount: isRefetch ? prev.refetchCount + 1 : 0
    }));

    try {
      const response = await fetch(url);
      const data = await response.json();
      
      setState({
        data,
        status: 'success',
        error: null,
        lastUpdated: Date.now(),
        stale: false,
        refetchCount: state.refetchCount
      });
    } catch (error) {
      setState(prev => ({
        ...prev,
        status: 'error',
        error: error.message
      }));
    }
  };

  // Mark data as stale after 5 minutes
  useEffect(() => {
    if (state.lastUpdated) {
      const timer = setTimeout(() => {
        setState(prev => ({ ...prev, stale: true }));
      }, 5 * 60 * 1000);
      
      return () => clearTimeout(timer);
    }
  }, [state.lastUpdated]);

  return {
    ...state,
    fetch: fetchData,
    refetch: () => fetchData(true)
  };
}

9.4 useReducer for Complex Async State Machines

Action Type State Transition Payload Use Case
FETCH_START idle → loading None Initiate async operation
FETCH_SUCCESS loading → success {data} Store successful result
FETCH_ERROR loading → error {error} Store error information
REFETCH success/error → loading None Retry or refresh data

Example: useReducer for async state machine

const asyncReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_START':
      return {
        ...state,
        status: 'loading',
        error: null
      };
    
    case 'FETCH_SUCCESS':
      return {
        status: 'success',
        data: action.payload,
        error: null
      };
    
    case 'FETCH_ERROR':
      return {
        ...state,
        status: 'error',
        error: action.payload
      };
    
    case 'REFETCH':
      return {
        ...state,
        status: 'loading',
        error: null
        // Keep previous data during refetch
      };
    
    case 'RESET':
      return {
        status: 'idle',
        data: null,
        error: null
      };
    
    default:
      return state;
  }
};

function useAsyncData(fetchFn) {
  const [state, dispatch] = useReducer(asyncReducer, {
    status: 'idle',
    data: null,
    error: null
  });

  const execute = async (...args) => {
    dispatch({ type: 'FETCH_START' });
    
    try {
      const data = await fetchFn(...args);
      dispatch({ type: 'FETCH_SUCCESS', payload: data });
      return { success: true, data };
    } catch (error) {
      dispatch({ type: 'FETCH_ERROR', payload: error.message });
      return { success: false, error };
    }
  };

  const refetch = async (...args) => {
    dispatch({ type: 'REFETCH' });
    return execute(...args);
  };

  return {
    ...state,
    execute,
    refetch,
    reset: () => dispatch({ type: 'RESET' })
  };
}

// Usage
function UserList() {
  const { data, status, error, execute } = useAsyncData(
    async () => {
      const res = await fetch('/api/users');
      return res.json();
    }
  );

  useEffect(() => {
    execute();
  }, []);

  if (status === 'loading') return <Spinner />;
  if (status === 'error') return <Error message={error} />;
  if (status === 'success') return <List items={data} />;
  return null;
}
useReducer Benefits for Async:
  • Centralized state transitions - all async states managed in one place
  • Predictable state updates - reducer enforces valid state transitions
  • Easier testing - reducer is pure function, testable in isolation
  • Better for complex flows - multi-step operations, conditional transitions
  • Time-travel debugging - action history for debugging state changes

9.5 Promise-based State Updates and Race Conditions

Problem Cause Solution Implementation
Race condition Multiple async operations complete out of order Request ID tracking or AbortController Track latest request, ignore stale responses
Stale closure Callback captures old state value Functional setState updates setState(prev => ...)
Memory leak setState on unmounted component Cleanup with mounted flag Check if mounted before setState
Multiple triggers Rapid user actions trigger multiple requests Debounce or cancel previous request AbortController + debounce

Example: Preventing race conditions with request ID

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // Create unique ID for this request
    const requestId = Date.now();
    let isLatestRequest = true;

    const searchData = async () => {
      setIsLoading(true);

      // Simulate variable network delay
      await new Promise(resolve => setTimeout(resolve, Math.random() * 2000));

      const response = await fetch(`/api/search?q=${query}`);
      const data = await response.json();

      // Only update state if this is still the latest request
      if (isLatestRequest) {
        setResults(data);
        setIsLoading(false);
      }
    };

    searchData();

    // Cleanup - mark this request as stale
    return () => {
      isLatestRequest = false;
    };
  }, [query]);

  return <div>{/* render results */}</div>;
}

// Better: Using ref to track latest request ID
function SearchResultsBetter({ query }) {
  const [results, setResults] = useState([]);
  const latestRequestId = useRef(0);

  useEffect(() => {
    const requestId = ++latestRequestId.current;

    const searchData = async () => {
      const response = await fetch(`/api/search?q=${query}`);
      const data = await response.json();

      // Only update if this is the latest request
      if (requestId === latestRequestId.current) {
        setResults(data);
      }
    };

    searchData();
  }, [query]);

  return <div>{/* render results */}</div>;
}

Example: Preventing memory leaks on unmounted components

function DataComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const json = await response.json();
        
        // Only update state if component is still mounted
        if (isMounted) {
          setData(json);
        }
      } catch (error) {
        if (isMounted) {
          console.error(error);
        }
      }
    };

    fetchData();

    // Cleanup function sets flag to false
    return () => {
      isMounted = false;
    };
  }, []);

  return <div>{data?.value}</div>;
}

// Custom hook to check if mounted
function useIsMounted() {
  const isMounted = useRef(false);

  useEffect(() => {
    isMounted.current = true;
    return () => {
      isMounted.current = false;
    };
  }, []);

  return isMounted;
}

// Usage with custom hook
function SafeComponent() {
  const [data, setData] = useState(null);
  const isMounted = useIsMounted();

  const fetchData = async () => {
    const response = await fetch('/api/data');
    const json = await response.json();
    
    if (isMounted.current) {
      setData(json);
    }
  };

  return <div>{data?.value}</div>;
}

9.6 Abort Controllers and Cleanup for Async Operations

Technique API Purpose Browser Support
AbortController new AbortController() Cancel in-flight fetch requests All modern browsers
AbortSignal controller.signal Pass cancellation signal to fetch All modern browsers
controller.abort() Trigger cancellation Cancel ongoing request All modern browsers
Cleanup function Return from useEffect Abort when component unmounts or deps change React API

Example: AbortController with fetch

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    const abortController = new AbortController();

    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: abortController.signal // Pass signal to fetch
        });

        if (!response.ok) {
          throw new Error('Failed to fetch');
        }

        const data = await response.json();
        setUser(data);
      } catch (err) {
        // Don't update state if request was aborted
        if (err.name === 'AbortError') {
          console.log('Fetch aborted');
          return;
        }
        setError(err.message);
      }
    };

    fetchUser();

    // Cleanup: abort fetch when component unmounts or userId changes
    return () => {
      abortController.abort();
    };
  }, [userId]);

  if (error) return <Error message={error} />;
  if (!user) return <Loading />;
  return <div>{user.name}</div>;
}

Example: Reusable hook with AbortController

function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    if (!url) return;

    const abortController = new AbortController();
    
    const fetchData = async () => {
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch(url, {
          ...options,
          signal: abortController.signal
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const json = await response.json();
        setData(json);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (!abortController.signal.aborted) {
          setIsLoading(false);
        }
      }
    };

    fetchData();

    return () => abortController.abort();
  }, [url, JSON.stringify(options)]);

  return { data, error, isLoading };
}

// Usage
function MyComponent() {
  const [userId, setUserId] = useState(1);
  const { data, error, isLoading } = useFetch(`/api/users/${userId}`);

  // When userId changes, previous fetch is automatically aborted
  return (
    <div>
      <button onClick={() => setUserId(id => id + 1)}>Next User</button>
      {isLoading && <Spinner />}
      {error && <Error message={error} />}
      {data && <User data={data} />}
    </div>
  );
}

Example: Multiple AbortControllers for different operations

function Dashboard() {
  const [users, setUsers] = useState([]);
  const [posts, setPosts] = useState([]);
  const [comments, setComments] = useState([]);

  useEffect(() => {
    // Separate abort controller for each request
    const usersController = new AbortController();
    const postsController = new AbortController();
    const commentsController = new AbortController();

    // Fetch all data in parallel
    Promise.all([
      fetch('/api/users', { signal: usersController.signal })
        .then(r => r.json())
        .then(setUsers)
        .catch(err => {
          if (err.name !== 'AbortError') console.error('Users error:', err);
        }),
      
      fetch('/api/posts', { signal: postsController.signal })
        .then(r => r.json())
        .then(setPosts)
        .catch(err => {
          if (err.name !== 'AbortError') console.error('Posts error:', err);
        }),
      
      fetch('/api/comments', { signal: commentsController.signal })
        .then(r => r.json())
        .then(setComments)
        .catch(err => {
          if (err.name !== 'AbortError') console.error('Comments error:', err);
        })
    ]);

    // Cleanup: abort all requests
    return () => {
      usersController.abort();
      postsController.abort();
      commentsController.abort();
    };
  }, []);

  return <div>{/* render data */}</div>;
}
AbortController Best Practices:
  • Always check for AbortError in catch blocks to avoid logging cancelled requests
  • Create new AbortController for each request - don't reuse controllers
  • Return cleanup function from useEffect to abort on unmount/dep changes
  • Use with debouncing for search inputs to cancel outdated requests
  • Not just for fetch - can be used with any API that accepts AbortSignal

Async State Management Summary:

  • Status enums - Use 'idle' | 'loading' | 'success' | 'error' for clarity
  • Multiple loading states - Track different operations separately (initial, refresh, loadMore)
  • Error boundaries - Catch render errors; use try/catch for async/events
  • Error metadata - Store error message, timestamp, context for debugging
  • useReducer for complex flows - State machines for multi-step async operations
  • Race condition prevention - Use request IDs or AbortController
  • Memory leak prevention - Check isMounted flag before setState
  • AbortController - Cancel stale requests on unmount or dependency changes
  • Functional updates - Use setState(prev => ...) to avoid stale closures
  • Success metadata - Track lastUpdated, stale status for cache management

10. Form State Management and Validation

10.1 Controlled Input Components and State Binding

Input Type State Binding Update Handler Key Props
Text input value={state} onChange={e => setState(e.target.value)} value, onChange, name, id
Textarea value={state} onChange={e => setState(e.target.value)} value, onChange, rows, cols
Checkbox checked={state} onChange={e => setState(e.target.checked)} checked, onChange, type="checkbox"
Radio button checked={state === value} onChange={() => setState(value)} checked, onChange, value, name
Select dropdown value={state} onChange={e => setState(e.target.value)} value, onChange, multiple (optional)
File input N/A (always uncontrolled) onChange={e => setFile(e.target.files[0])} onChange, type="file", accept

Example: Controlled form with multiple input types

function RegistrationForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    age: '',
    newsletter: false,
    gender: '',
    country: 'US'
  });

  // Generic handler for text inputs
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Form data:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Text input */}
      <input
        type="text"
        name="username"
        value={formData.username}
        onChange={handleChange}
        placeholder="Username"
      />

      {/* Email input */}
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />

      {/* Password input */}
      <input
        type="password"
        name="password"
        value={formData.password}
        onChange={handleChange}
        placeholder="Password"
      />

      {/* Number input */}
      <input
        type="number"
        name="age"
        value={formData.age}
        onChange={handleChange}
        placeholder="Age"
      />

      {/* Checkbox */}
      <label>
        <input
          type="checkbox"
          name="newsletter"
          checked={formData.newsletter}
          onChange={handleChange}
        />
        Subscribe to newsletter
      </label>

      {/* Radio buttons */}
      <div>
        <label>
          <input
            type="radio"
            name="gender"
            value="male"
            checked={formData.gender === 'male'}
            onChange={handleChange}
          />
          Male
        </label>
        <label>
          <input
            type="radio"
            name="gender"
            value="female"
            checked={formData.gender === 'female'}
            onChange={handleChange}
          />
          Female
        </label>
      </div>

      {/* Select dropdown */}
      <select name="country" value={formData.country} onChange={handleChange}>
        <option value="US">United States</option>
        <option value="UK">United Kingdom</option>
        <option value="CA">Canada</option>
      </select>

      <button type="submit">Register</button>
    </form>
  );
}
Controlled vs Uncontrolled Inputs:
Aspect Controlled Uncontrolled
Value source React state DOM (ref)
Updates onChange updates state DOM manages itself
Validation Real-time validation Validation on submit
Use case Most forms, validation required Simple forms, file uploads

10.2 Uncontrolled Components with useRef Patterns

Pattern Implementation Access Method Use Case
Single input ref const ref = useRef(null) ref.current.value Simple form with few fields
Multiple refs Separate ref for each input ref1.current.value, ref2.current.value Multiple inputs, accessed on submit
Ref object const refs = useRef({}) refs.current[name].value Dynamic number of inputs
Default value defaultValue={initial} Set initial value without state Uncontrolled input with initial value
Pattern 1: Simple uncontrolled form
function UncontrolledForm() {
  const nameRef = useRef(null);
  const emailRef = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formData = {
      name: nameRef.current.value,
      email: emailRef.current.value
    };
    
    console.log('Form data:', formData);
    
    // Reset form
    nameRef.current.value = '';
    emailRef.current.value = '';
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={nameRef}
        type="text"
        defaultValue=""
        placeholder="Name"
      />
      <input
        ref={emailRef}
        type="email"
        defaultValue=""
        placeholder="Email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}
Pattern 2: File upload (always uncontrolled)
function FileUpload() {
  const [preview, setPreview] = useState(null);
  const fileRef = useRef(null);

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    
    if (file) {
      // Create preview
      const reader = new FileReader();
      reader.onloadend = () => {
        setPreview(reader.result);
      };
      reader.readAsDataURL(file);
    }
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const file = fileRef.current.files[0];
    
    const formData = new FormData();
    formData.append('file', file);
    
    // Upload file
    fetch('/api/upload', {
      method: 'POST',
      body: formData
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={fileRef}
        type="file"
        onChange={handleFileChange}
        accept="image/*"
      />
      {preview && <img src={preview} />}
      <button type="submit">Upload</button>
    </form>
  );
}

10.3 Form Validation State and Error Handling

Validation Type Timing State Structure User Experience
On submit Form submission {fieldName: 'error message'} Show all errors at once
On blur Field loses focus {fieldName: {error, touched}} Validate after user finishes field
On change (real-time) Every keystroke {fieldName: {error, dirty}} Immediate feedback (can be annoying)
Hybrid (touched + dirty) Blur first, then onChange {fieldName: {error, touched, dirty}} Best UX - validate on blur, update on change

Example: Comprehensive form validation

function ValidatedForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  // Validation rules
  const validate = (name, value) => {
    switch (name) {
      case 'username':
        if (!value) return 'Username is required';
        if (value.length < 3) return 'Username must be at least 3 characters';
        if (!/^[a-zA-Z0-9_]+$/.test(value)) return 'Only letters, numbers, underscore';
        return '';

      case 'email':
        if (!value) return 'Email is required';
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Invalid email format';
        return '';

      case 'password':
        if (!value) return 'Password is required';
        if (value.length < 8) return 'Password must be at least 8 characters';
        if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(value)) {
          return 'Must contain uppercase, lowercase, and number';
        }
        return '';

      case 'confirmPassword':
        if (!value) return 'Please confirm password';
        if (value !== formData.password) return 'Passwords do not match';
        return '';

      default:
        return '';
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    
    setFormData(prev => ({ ...prev, [name]: value }));
    
    // Validate on change if field was touched
    if (touched[name]) {
      const error = validate(name, value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;
    
    // Mark field as touched
    setTouched(prev => ({ ...prev, [name]: true }));
    
    // Validate on blur
    const error = validate(name, value);
    setErrors(prev => ({ ...prev, [name]: error }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Validate all fields
    const newErrors = {};
    Object.keys(formData).forEach(name => {
      const error = validate(name, formData[name]);
      if (error) newErrors[name] = error;
    });
    
    setErrors(newErrors);
    setTouched(Object.keys(formData).reduce((acc, key) => {
      acc[key] = true;
      return acc;
    }, {}));
    
    // If no errors, submit
    if (Object.keys(newErrors).length === 0) {
      console.log('Form submitted:', formData);
    }
  };

  const getFieldError = (name) => touched[name] && errors[name];

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="username"
          value={formData.username}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Username"
          className={getFieldError('username') ? 'error' : ''}
        />
        {getFieldError('username') && (
          <span className="error-message">{errors.username}</span>
        )}
      </div>

      <div>
        <input
          name="email"
          type="email"
          value={formData.email}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Email"
          className={getFieldError('email') ? 'error' : ''}
        />
        {getFieldError('email') && (
          <span className="error-message">{errors.email}</span>
        )}
      </div>

      <div>
        <input
          name="password"
          type="password"
          value={formData.password}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Password"
          className={getFieldError('password') ? 'error' : ''}
        />
        {getFieldError('password') && (
          <span className="error-message">{errors.password}</span>
        )}
      </div>

      <div>
        <input
          name="confirmPassword"
          type="password"
          value={formData.confirmPassword}
          onChange={handleChange}
          onBlur={handleBlur}
          placeholder="Confirm Password"
          className={getFieldError('confirmPassword') ? 'error' : ''}
        />
        {getFieldError('confirmPassword') && (
          <span className="error-message">{errors.confirmPassword}</span>
        )}
      </div>

      <button type="submit">Register</button>
    </form>
  );
}
Validation Best Practices:
  • Use on-blur for initial validation (better UX than real-time)
  • Switch to on-change after field is touched (immediate feedback for corrections)
  • Show errors only for touched fields (don't overwhelm on page load)
  • Validate all fields on submit (catch any missed validations)
  • Consider async validation for username/email availability checks
  • Use schema validation libraries (Yup, Zod) for complex forms

10.4 Multi-step Form State Management

Pattern State Structure Navigation Use Case
Step index {step: 0, data: {}} Increment/decrement step number Linear wizard flow
Step array {steps: ['personal', 'address'], current: 0} Move through array of step names Named steps, flexible order
State machine {state: 'personal', data: {}} State transitions with validation Complex conditional flows
Validated steps {step: 0, completed: [0], data: {}} Track which steps are valid Allow non-linear navigation

Example: Multi-step form with validation

function MultiStepForm() {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({
    // Step 1: Personal Info
    firstName: '',
    lastName: '',
    email: '',
    // Step 2: Address
    street: '',
    city: '',
    zipCode: '',
    // Step 3: Payment
    cardNumber: '',
    expiryDate: '',
    cvv: ''
  });
  const [errors, setErrors] = useState({});

  const steps = [
    { title: 'Personal Info', fields: ['firstName', 'lastName', 'email'] },
    { title: 'Address', fields: ['street', 'city', 'zipCode'] },
    { title: 'Payment', fields: ['cardNumber', 'expiryDate', 'cvv'] }
  ];

  const validateStep = (stepIndex) => {
    const stepFields = steps[stepIndex].fields;
    const stepErrors = {};

    stepFields.forEach(field => {
      if (!formData[field]) {
        stepErrors[field] = 'This field is required';
      }
      // Add specific validations
      if (field === 'email' && formData[field]) {
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData[field])) {
          stepErrors[field] = 'Invalid email';
        }
      }
      if (field === 'zipCode' && formData[field]) {
        if (!/^\d{5}$/.test(formData[field])) {
          stepErrors[field] = 'Invalid zip code';
        }
      }
    });

    setErrors(stepErrors);
    return Object.keys(stepErrors).length === 0;
  };

  const handleNext = () => {
    if (validateStep(currentStep)) {
      setCurrentStep(prev => Math.min(prev + 1, steps.length - 1));
    }
  };

  const handleBack = () => {
    setCurrentStep(prev => Math.max(prev - 1, 0));
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    // Clear error for this field
    setErrors(prev => {
      const newErrors = { ...prev };
      delete newErrors[name];
      return newErrors;
    });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validateStep(currentStep)) {
      console.log('Form submitted:', formData);
      // Submit to API
    }
  };

  const renderStep = () => {
    const currentFields = steps[currentStep].fields;

    return (
      <div>
        {currentFields.map(field => (
          <div key={field}>
            <input
              name={field}
              value={formData[field]}
              onChange={handleChange}
              placeholder={field}
            />
            {errors[field] && <span className="error">{errors[field]}</span>}
          </div>
        ))}
      </div>
    );
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Progress indicator */}
      <div className="progress">
        Step {currentStep + 1} of {steps.length}: {steps[currentStep].title}
      </div>

      {/* Step content */}
      {renderStep()}

      {/* Navigation */}
      <div className="buttons">
        {currentStep > 0 && (
          <button type="button" onClick={handleBack}>
            Back
          </button>
        )}
        
        {currentStep < steps.length - 1 ? (
          <button type="button" onClick={handleNext}>
            Next
          </button>
        ) : (
          <button type="submit">Submit</button>
        )}
      </div>
    </form>
  );
}
Multi-step Form Tips:
  • Store all form data in single state object (easier to manage)
  • Validate each step before allowing navigation to next
  • Show progress indicator (step X of Y)
  • Allow back navigation without validation
  • Consider saving draft to localStorage on step change
  • Use URL params to enable direct linking to specific steps

10.5 Form Reset and Clear Operations

Operation Implementation When to Use Side Effects
Reset to initial setFormData(initialState) After successful submit, cancel Clears all fields to original values
Clear all fields setFormData({}) or empty strings New entry, clear button Sets all fields to empty/default
Reset single field setFormData(prev => ({...prev, field: ''})) Clear individual input Resets one field, keeps others
Form.reset() formRef.current.reset() Uncontrolled forms only Browser native reset (doesn't update state)

Example: Form reset patterns

function FormWithReset() {
  const initialFormData = {
    name: '',
    email: '',
    message: ''
  };

  const [formData, setFormData] = useState(initialFormData);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      
      // ✅ Reset to initial state after successful submit
      resetForm();
      alert('Form submitted successfully!');
    } catch (error) {
      setErrors({ submit: 'Failed to submit form' });
    }
  };

  // Complete reset - clears data, errors, and touched state
  const resetForm = () => {
    setFormData(initialFormData);
    setErrors({});
    setTouched({});
  };

  // Clear single field
  const clearField = (fieldName) => {
    setFormData(prev => ({ ...prev, [fieldName]: '' }));
    setErrors(prev => {
      const newErrors = { ...prev };
      delete newErrors[fieldName];
      return newErrors;
    });
    setTouched(prev => {
      const newTouched = { ...prev };
      delete newTouched[fieldName];
      return newTouched;
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <input
          name="name"
          value={formData.name}
          onChange={handleChange}
          placeholder="Name"
        />
        {formData.name && (
          <button type="button" onClick={() => clearField('name')}>
            Clear
          </button>
        )}
      </div>

      <div>
        <input
          name="email"
          value={formData.email}
          onChange={handleChange}
          placeholder="Email"
        />
      </div>

      <div>
        <textarea
          name="message"
          value={formData.message}
          onChange={handleChange}
          placeholder="Message"
        />
      </div>

      <div className="buttons">
        <button type="submit">Submit</button>
        <button type="button" onClick={resetForm}>
          Reset Form
        </button>
      </div>
    </form>
  );
}
Reset Gotchas:
  • form.reset() only works for uncontrolled forms - doesn't update React state
  • Remember to clear validation errors and touched state when resetting
  • Consider showing confirmation dialog before clearing filled form
  • Don't reset form on failed submission (preserve user data)

10.6 Dynamic Form Fields and Array State Management

Pattern State Structure Operations Use Case
Array of objects [{id, field1, field2}] Add, remove, update by index/id Repeated field groups (contacts, items)
Nested object with IDs {[id]: {field1, field2}} Add/remove by key, update by id Normalized structure, easier lookups
Array with unique keys [{key: uuid(), ...fields}] Use UUID for stable React keys Prevent key issues with add/remove

Example: Dynamic fields - add/remove items

import { useState } from 'react';

function DynamicForm() {
  const [contacts, setContacts] = useState([
    { id: Date.now(), name: '', email: '', phone: '' }
  ]);

  const addContact = () => {
    setContacts(prev => [
      ...prev,
      { id: Date.now(), name: '', email: '', phone: '' }
    ]);
  };

  const removeContact = (id) => {
    setContacts(prev => prev.filter(contact => contact.id !== id));
  };

  const updateContact = (id, field, value) => {
    setContacts(prev =>
      prev.map(contact =>
        contact.id === id
          ? { ...contact, [field]: value }
          : contact
      )
    );
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Contacts:', contacts);
  };

  return (
    <form onSubmit={handleSubmit}>
      {contacts.map((contact, index) => (
        <div key={contact.id} className="contact-group">
          <h4>Contact {index + 1}</h4>
          
          <input
            type="text"
            value={contact.name}
            onChange={(e) => updateContact(contact.id, 'name', e.target.value)}
            placeholder="Name"
          />
          
          <input
            type="email"
            value={contact.email}
            onChange={(e) => updateContact(contact.id, 'email', e.target.value)}
            placeholder="Email"
          />
          
          <input
            type="tel"
            value={contact.phone}
            onChange={(e) => updateContact(contact.id, 'phone', e.target.value)}
            placeholder="Phone"
          />
          
          {contacts.length > 1 && (
            <button
              type="button"
              onClick={() => removeContact(contact.id)}
            >
              Remove
            </button>
          )}
        </div>
      ))}

      <button type="button" onClick={addContact}>
        Add Another Contact
      </button>
      
      <button type="submit">Submit All Contacts</button>
    </form>
  );
}

Example: Nested dynamic fields with validation

function OrderForm() {
  const [order, setOrder] = useState({
    customerName: '',
    items: [
      { id: 1, product: '', quantity: 1, price: 0 }
    ]
  });

  const addItem = () => {
    setOrder(prev => ({
      ...prev,
      items: [
        ...prev.items,
        { id: Date.now(), product: '', quantity: 1, price: 0 }
      ]
    }));
  };

  const removeItem = (id) => {
    setOrder(prev => ({
      ...prev,
      items: prev.items.filter(item => item.id !== id)
    }));
  };

  const updateItem = (id, field, value) => {
    setOrder(prev => ({
      ...prev,
      items: prev.items.map(item =>
        item.id === id ? { ...item, [field]: value } : item
      )
    }));
  };

  // Calculate total
  const total = order.items.reduce(
    (sum, item) => sum + (item.quantity * item.price),
    0
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // Validate
    if (!order.customerName) {
      alert('Customer name required');
      return;
    }
    if (order.items.some(item => !item.product)) {
      alert('All items must have a product');
      return;
    }
    
    console.log('Order:', order, 'Total:', total);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={order.customerName}
        onChange={(e) => setOrder(prev => ({
          ...prev,
          customerName: e.target.value
        }))}
        placeholder="Customer Name"
      />

      <h3>Items</h3>
      {order.items.map((item, index) => (
        <div key={item.id}>
          <input
            type="text"
            value={item.product}
            onChange={(e) => updateItem(item.id, 'product', e.target.value)}
            placeholder="Product"
          />
          
          <input
            type="number"
            value={item.quantity}
            onChange={(e) => updateItem(item.id, 'quantity', parseInt(e.target.value))}
            min="1"
          />
          
          <input
            type="number"
            value={item.price}
            onChange={(e) => updateItem(item.id, 'price', parseFloat(e.target.value))}
            step="0.01"
            min="0"
          />
          
          <span>Subtotal: ${(item.quantity * item.price).toFixed(2)}</span>
          
          {order.items.length > 1 && (
            <button type="button" onClick={() => removeItem(item.id)}>
              Remove
            </button>
          )}
        </div>
      ))}

      <button type="button" onClick={addItem}>Add Item</button>
      
      <div><strong>Total: ${total.toFixed(2)}</strong></div>
      
      <button type="submit">Submit Order</button>
    </form>
  );
}
Dynamic Fields Best Practices:
  • Use unique IDs (timestamp or UUID) for array items, not index
  • Provide "Remove" button only when more than minimum items exist
  • Consider minimum/maximum limits for dynamic fields
  • Validate each item individually and show errors per item
  • Use functional updates when modifying nested arrays
  • Consider using libraries like react-hook-form for complex dynamic forms

Form State Management Summary:

  • Controlled inputs - Use value + onChange for real-time validation and state sync
  • Uncontrolled with refs - Simple forms, file uploads, or third-party integration
  • Validation timing - Blur for initial, change for corrections (hybrid approach)
  • Error state - Track errors + touched state, show only for touched fields
  • Multi-step forms - Single state object, validate per step, show progress
  • Form reset - Clear data, errors, and touched state together
  • Dynamic fields - Use unique IDs (not index), functional updates for nested state
  • Generic handlers - Single onChange handler for multiple inputs using name attribute
  • Form libraries - Consider react-hook-form or Formik for complex forms

11. List and Collection State Management

11.1 Array State Operations (CRUD) with Keys

Operation Immutable Pattern Key Strategy Performance Note
Create (Add) [...items, newItem] or items.concat(newItem) Use unique ID (UUID, timestamp, server ID) O(n) - spreads entire array
Read (Access) items.find(i => i.id === id) Search by unique identifier O(n) - consider normalized state for O(1)
Update (Modify) items.map(i => i.id === id ? updated : i) Preserve key, update properties O(n) - creates new array
Delete (Remove) items.filter(i => i.id !== id) Remove by ID, not index O(n) - creates new array
Reorder Array splice or swap indices Maintain stable keys during reorder Use key for React reconciliation

Example: Complete CRUD operations for todo list

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build app', completed: false }
  ]);

  // CREATE - Add new todo
  const addTodo = (text) => {
    const newTodo = {
      id: Date.now(), // or crypto.randomUUID() in modern browsers
      text,
      completed: false
    };
    setTodos(prev => [...prev, newTodo]); // Add to end
    // or: setTodos(prev => [newTodo, ...prev]); // Add to beginning
  };

  // READ - Find specific todo
  const getTodo = (id) => {
    return todos.find(todo => todo.id === id);
  };

  // UPDATE - Modify todo
  const updateTodo = (id, updates) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id
          ? { ...todo, ...updates }
          : todo
      )
    );
  };

  // Toggle completed status
  const toggleTodo = (id) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id
          ? { ...todo, completed: !todo.completed }
          : todo
      )
    );
  };

  // DELETE - Remove todo
  const deleteTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  // DELETE ALL - Clear completed
  const clearCompleted = () => {
    setTodos(prev => prev.filter(todo => !todo.completed));
  };

  // BULK UPDATE - Mark all as completed
  const completeAll = () => {
    setTodos(prev =>
      prev.map(todo => ({ ...todo, completed: true }))
    );
  };

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}> {/* CRITICAL: Use unique ID as key */}
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => deleteTodo(todo.id)}>Delete</button>
        </div>
      ))}
      <button onClick={() => addTodo('New task')}>Add Todo</button>
      <button onClick={clearCompleted}>Clear Completed</button>
      <button onClick={completeAll}>Complete All</button>
    </div>
  );
}
Key Anti-patterns:
  • key={index} - Breaks when items reorder, add, or remove. React can't track identity.
  • key={Math.random()} - New key every render, forces remount, loses component state
  • ❌ Mutating array - items.push(), items[i] = val - React won't detect change
  • key={item.id} - Stable, unique identifier from data
  • key={crypto.randomUUID()} - Only when creating new item, stored in state

11.2 Filtering and Sorting State Implementation

Pattern State Structure Derived Calculation Performance
Filter state {items, filter: 'all'} items.filter(matchesFilter) Recalculate on render (fast for small lists)
Sort state {items, sortBy: 'name', sortDir: 'asc'} items.sort(compareFn) ⚠️ Don't mutate, use .slice().sort()
Combined filter + sort {items, filter, sortBy, sortDir} Filter first, then sort Use useMemo for expensive operations
Memoized derived state Same structure useMemo(() => filter + sort, deps) Only recalculates when dependencies change

Example: Filtering and sorting implementation

function ProductList() {
  const [products] = useState([
    { id: 1, name: 'Laptop', category: 'Electronics', price: 999 },
    { id: 2, name: 'Desk', category: 'Furniture', price: 299 },
    { id: 3, name: 'Mouse', category: 'Electronics', price: 29 },
    { id: 4, name: 'Chair', category: 'Furniture', price: 199 }
  ]);

  const [filter, setFilter] = useState('all'); // 'all', 'Electronics', 'Furniture'
  const [sortBy, setSortBy] = useState('name'); // 'name', 'price'
  const [sortDir, setSortDir] = useState('asc'); // 'asc', 'desc'

  // Derived state - filter and sort
  const filteredAndSorted = useMemo(() => {
    console.log('Recalculating filtered and sorted products');

    // Step 1: Filter
    let result = products;
    if (filter !== 'all') {
      result = result.filter(p => p.category === filter);
    }

    // Step 2: Sort (don't mutate - use slice())
    result = result.slice().sort((a, b) => {
      let comparison = 0;
      
      if (sortBy === 'name') {
        comparison = a.name.localeCompare(b.name);
      } else if (sortBy === 'price') {
        comparison = a.price - b.price;
      }
      
      return sortDir === 'asc' ? comparison : -comparison;
    });

    return result;
  }, [products, filter, sortBy, sortDir]);

  const toggleSort = (field) => {
    if (sortBy === field) {
      // Same field - toggle direction
      setSortDir(prev => prev === 'asc' ? 'desc' : 'asc');
    } else {
      // New field - default to ascending
      setSortBy(field);
      setSortDir('asc');
    }
  };

  return (
    <div>
      {/* Filter controls */}
      <div>
        <button onClick={() => setFilter('all')}>All</button>
        <button onClick={() => setFilter('Electronics')}>Electronics</button>
        <button onClick={() => setFilter('Furniture')}>Furniture</button>
      </div>

      {/* Sort controls */}
      <div>
        <button onClick={() => toggleSort('name')}>
          Sort by Name {sortBy === 'name' && (sortDir === 'asc' ? '▲' : '▼')}
        </button>
        <button onClick={() => toggleSort('price')}>
          Sort by Price {sortBy === 'price' && (sortDir === 'asc' ? '▲' : '▼')}
        </button>
      </div>

      {/* Display filtered and sorted results */}
      <div>
        {filteredAndSorted.map(product => (
          <div key={product.id}>
            {product.name} - ${product.price} ({product.category})
          </div>
        ))}
      </div>
    </div>
  );
}
Sorting Gotcha: Array's .sort() method mutates the array! Always use .slice().sort() or [...array].sort() to avoid mutating state. For large datasets (>1000 items), consider server-side filtering/sorting or virtualization.

11.3 Pagination State and Virtual Scrolling

Pattern State Structure Calculation Best For
Page-based pagination {page: 1, pageSize: 10} items.slice(start, end) Traditional pagination with page numbers
Cursor-based pagination {cursor: null, hasMore: true} Load next batch using cursor Infinite scroll, real-time data
Load more (append) {items: [], page: 0, hasMore} Append new items to existing array Social media feeds, infinite scroll
Virtual scrolling {scrollTop, visibleRange} Render only visible items Very large lists (10k+ items)
Pattern 1: Page-based pagination
function PaginatedList({ items }) {
  const [currentPage, setCurrentPage] = useState(1);
  const pageSize = 10;

  // Calculate pagination values
  const totalPages = Math.ceil(items.length / pageSize);
  const startIndex = (currentPage - 1) * pageSize;
  const endIndex = startIndex + pageSize;
  const currentItems = items.slice(startIndex, endIndex);

  const goToPage = (page) => {
    setCurrentPage(Math.max(1, Math.min(page, totalPages)));
  };

  return (
    <div>
      {/* Items for current page */}
      <div>
        {currentItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>

      {/* Pagination controls */}
      <div>
        <button
          onClick={() => goToPage(currentPage - 1)}
          disabled={currentPage === 1}
        >
          Previous
        </button>
        
        <span>Page {currentPage} of {totalPages}</span>
        
        <button
          onClick={() => goToPage(currentPage + 1)}
          disabled={currentPage === totalPages}
        >
          Next
        </button>
      </div>

      {/* Page numbers */}
      <div>
        {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
          <button
            key={page}
            onClick={() => goToPage(page)}
            disabled={page === currentPage}
          >
            {page}
          </button>
        ))}
      </div>
    </div>
  );
}
Pattern 2: Infinite scroll (load more)
function InfiniteScrollList() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(0);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);

  const loadMore = async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    try {
      const response = await fetch(
        `/api/items?page=${page}&limit=20`
      );
      const newItems = await response.json();

      if (newItems.length === 0) {
        setHasMore(false);
      } else {
        // Append new items to existing
        setItems(prev => [...prev, ...newItems]);
        setPage(prev => prev + 1);
      }
    } finally {
      setLoading(false);
    }
  };

  // Load initial data
  useEffect(() => {
    loadMore();
  }, []);

  // Intersection Observer for auto-load
  const observerRef = useRef();
  const lastItemRef = useCallback(node => {
    if (loading) return;
    if (observerRef.current) {
      observerRef.current.disconnect();
    }
    
    observerRef.current = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && hasMore) {
          loadMore();
        }
      }
    );
    
    if (node) observerRef.current.observe(node);
  }, [loading, hasMore]);

  return (
    <div>
      {items.map((item, index) => {
        // Attach ref to last item
        const isLast = index === items.length - 1;
        return (
          <div
            key={item.id}
            ref={isLast ? lastItemRef : null}
          >
            {item.name}
          </div>
        );
      })}
      {loading && <div>Loading...</div>}
      {!hasMore && <div>No more items</div>}
    </div>
  );
}
Virtual Scrolling: For lists with 10,000+ items, use libraries like react-window or react-virtual. They only render visible items (+ buffer), drastically improving performance. Example: 100,000 items but only render 20 visible = 5000x fewer DOM nodes.

11.4 Selection State for Multi-select Components

Pattern State Type Operations Use Case
Set of IDs Set<id> add, delete, has - O(1) Efficient lookup and toggle
Array of IDs string[] includes, filter - O(n) Simple, serializable for API
Boolean map {[id]: boolean} Direct property access - O(1) Quick lookup, works with forms
Select all state {selected: Set, allSelected: bool} Track if all items selected Bulk operations, select all checkbox
function MultiSelectList({ items }) {
  const [selected, setSelected] = useState(new Set());

  const toggleItem = (id) => {
    setSelected(prev => {
      const newSet = new Set(prev);
      if (newSet.has(id)) {
        newSet.delete(id);
      } else {
        newSet.add(id);
      }
      return newSet;
    });
  };

  const selectAll = () => {
    setSelected(new Set(items.map(item => item.id)));
  };

  const clearSelection = () => {
    setSelected(new Set());
  };

  const isAllSelected = selected.size === items.length;
  const isSomeSelected = selected.size > 0 && selected.size < items.length;

  const deleteSelected = () => {
    // Convert Set to Array for API call
    const selectedIds = Array.from(selected);
    console.log('Deleting:', selectedIds);
    // After deletion, clear selection
    clearSelection();
  };

  return (
    <div>
      {/* Select all checkbox */}
      <label>
        <input
          type="checkbox"
          checked={isAllSelected}
          ref={input => {
            if (input) input.indeterminate = isSomeSelected;
          }}
          onChange={(e) => {
            if (e.target.checked) {
              selectAll();
            } else {
              clearSelection();
            }
          }}
        />
        Select All
      </label>

      {/* Item list */}
      {items.map(item => (
        <div key={item.id}>
          <label>
            <input
              type="checkbox"
              checked={selected.has(item.id)}
              onChange={() => toggleItem(item.id)}
            />
            {item.name}
          </label>
        </div>
      ))}

      {/* Bulk actions */}
      <div>
        <span>{selected.size} selected</span>
        {selected.size > 0 && (
          <>
            <button onClick={deleteSelected}>Delete Selected</button>
            <button onClick={clearSelection}>Clear Selection</button>
          </>
        )}
      </div>
    </div>
  );
}
Set vs Array for Selection:
  • Set: O(1) add/delete/has, better performance for large lists
  • ⚠️ Array: O(n) operations, but easier to serialize for APIs
  • 💡 Best of both: Use Set internally, convert to Array when needed: Array.from(selectedSet)
  • 🔄 Convert back: new Set(selectedArray)

11.5 Drag and Drop State Management

State Purpose Update Timing Value
draggingId Track which item is being dragged onDragStart / onDragEnd ID of dragged item or null
dragOverId Track drop target (visual feedback) onDragOver / onDragLeave ID of hovered item or null
items order List order after drop onDrop Reordered array
dragData Data being transferred Set in onDragStart, read in onDrop Item data or ID

Example: Drag and drop reordering

function DraggableList() {
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' },
    { id: 4, text: 'Item 4' }
  ]);
  const [draggingId, setDraggingId] = useState(null);
  const [dragOverId, setDragOverId] = useState(null);

  const handleDragStart = (e, id) => {
    setDraggingId(id);
    e.dataTransfer.effectAllowed = 'move';
    e.dataTransfer.setData('text/plain', id);
  };

  const handleDragOver = (e, id) => {
    e.preventDefault(); // Required to allow drop
    e.dataTransfer.dropEffect = 'move';
    setDragOverId(id);
  };

  const handleDragLeave = () => {
    setDragOverId(null);
  };

  const handleDrop = (e, dropTargetId) => {
    e.preventDefault();

    if (draggingId === dropTargetId) {
      // Dropped on itself, do nothing
      setDraggingId(null);
      setDragOverId(null);
      return;
    }

    // Reorder items
    setItems(prev => {
      const dragIndex = prev.findIndex(item => item.id === draggingId);
      const dropIndex = prev.findIndex(item => item.id === dropTargetId);

      const newItems = [...prev];
      const [draggedItem] = newItems.splice(dragIndex, 1);
      newItems.splice(dropIndex, 0, draggedItem);

      return newItems;
    });

    setDraggingId(null);
    setDragOverId(null);
  };

  const handleDragEnd = () => {
    setDraggingId(null);
    setDragOverId(null);
  };

  return (
    <div>
      {items.map(item => (
        <div
          key={item.id}
          draggable
          onDragStart={(e) => handleDragStart(e, item.id)}
          onDragOver={(e) => handleDragOver(e, item.id)}
          onDragLeave={handleDragLeave}
          onDrop={(e) => handleDrop(e, item.id)}
          onDragEnd={handleDragEnd}
          style={{
            opacity: draggingId === item.id ? 0.5 : 1,
            backgroundColor: dragOverId === item.id ? '#e0e0e0' : 'white',
            border: '1px solid #ccc',
            padding: '10px',
            margin: '5px',
            cursor: 'move'
          }}
        >
          {item.text}
        </div>
      ))}
    </div>
  );
}
Drag and Drop Complexity: Native HTML5 drag and drop has quirks and limited mobile support. For production apps, consider libraries like react-beautiful-dnd, dnd-kit, or react-dnd which handle edge cases, accessibility, and touch events.

11.6 Search and Filter State with Debouncing

Technique Implementation Benefit Delay
Debounce Wait N ms after last keystroke before searching Reduce API calls, avoid search on every keystroke 300-500ms typical
Throttle Execute search at most once every N ms Limit rate of expensive operations 200-500ms typical
Immediate local search Filter state immediately for local data Instant feedback for small datasets 0ms - no delay
Debounced + Loading Show loading state during debounce period Visual feedback that search is pending Debounce + network time
import { useState, useEffect } from 'react';

// Reusable debounce hook
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Set timeout to update debounced value after delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cleanup: cancel timeout if value changes before delay expires
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

// Usage in search component
function SearchableList({ items }) {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);

  // Only filter when debounced value changes
  const filteredItems = useMemo(() => {
    console.log('Filtering with:', debouncedSearchTerm);
    
    if (!debouncedSearchTerm) return items;

    return items.filter(item =>
      item.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
    );
  }, [items, debouncedSearchTerm]);

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <p>Searching for: "{debouncedSearchTerm}"</p>
      <div>
        {filteredItems.map(item => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  );
}

Example: Debounced API search with loading state

function APISearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const debouncedQuery = useDebounce(query, 500);

  useEffect(() => {
    // Don't search if query is empty
    if (!debouncedQuery) {
      setResults([]);
      return;
    }

    const searchAPI = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(debouncedQuery)}`
        );
        const data = await response.json();
        setResults(data);
      } catch (error) {
        console.error('Search failed:', error);
        setResults([]);
      } finally {
        setLoading(false);
      }
    };

    searchAPI();
  }, [debouncedQuery]);

  // Show typing indicator
  const isTyping = query !== debouncedQuery;

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search products..."
      />
      
      {isTyping && <span>⌨️ Typing...</span>}
      {loading && <span>🔍 Searching...</span>}
      
      <div>
        {results.length === 0 && debouncedQuery && !loading && (
          <p>No results found for "{debouncedQuery}"</p>
        )}
        
        {results.map(result => (
          <div key={result.id}>
            {result.name} - ${result.price}
          </div>
        ))}
      </div>
    </div>
  );
}

Example: Advanced search with multiple filters

function AdvancedSearch() {
  const [filters, setFilters] = useState({
    search: '',
    category: 'all',
    minPrice: 0,
    maxPrice: 1000,
    inStock: false
  });

  // Debounce only the search text, not the other filters
  const debouncedSearch = useDebounce(filters.search, 300);

  const updateFilter = (key, value) => {
    setFilters(prev => ({ ...prev, [key]: value }));
  };

  // Combine all filters
  const filteredItems = useMemo(() => {
    return items.filter(item => {
      // Text search (debounced)
      if (debouncedSearch && !item.name.toLowerCase().includes(
        debouncedSearch.toLowerCase()
      )) {
        return false;
      }

      // Category filter (immediate)
      if (filters.category !== 'all' && item.category !== filters.category) {
        return false;
      }

      // Price range (immediate)
      if (item.price < filters.minPrice || item.price > filters.maxPrice) {
        return false;
      }

      // Stock filter (immediate)
      if (filters.inStock && !item.inStock) {
        return false;
      }

      return true;
    });
  }, [debouncedSearch, filters.category, filters.minPrice, filters.maxPrice, filters.inStock]);

  return (
    <div>
      {/* Search input - debounced */}
      <input
        type="text"
        value={filters.search}
        onChange={(e) => updateFilter('search', e.target.value)}
        placeholder="Search..."
      />

      {/* Category - immediate */}
      <select
        value={filters.category}
        onChange={(e) => updateFilter('category', e.target.value)}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>

      {/* Price range - immediate */}
      <input
        type="range"
        min="0"
        max="1000"
        value={filters.minPrice}
        onChange={(e) => updateFilter('minPrice', parseInt(e.target.value))}
      />

      {/* In stock - immediate */}
      <label>
        <input
          type="checkbox"
          checked={filters.inStock}
          onChange={(e) => updateFilter('inStock', e.target.checked)}
        />
        In Stock Only
      </label>

      <p>{filteredItems.length} results</p>
      
      {filteredItems.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}
Debounce Best Practices:
  • Use 300-500ms delay for search inputs (balances responsiveness vs API load)
  • Debounce text search, but apply other filters (checkboxes, selects) immediately
  • Show "typing" indicator when input value differs from debounced value
  • Cancel pending API requests when component unmounts (use AbortController)
  • For very large local datasets (>10k items), debounce the filter calculation too
  • Consider using libraries like lodash.debounce or use-debounce

List and Collection State Summary:

  • CRUD operations - Use functional updates with map/filter for immutability
  • Unique keys - Always use stable IDs (not index) for React keys
  • Filtering/Sorting - Use useMemo for derived state, avoid mutating with sort()
  • Pagination - Page-based for traditional, cursor-based for infinite scroll
  • Selection - Use Set for O(1) operations, convert to Array for APIs
  • Drag and drop - Track draggingId and dragOverId for visual feedback
  • Search debouncing - 300-500ms delay for text input, immediate for other filters
  • Virtual scrolling - Use react-window for lists with 10k+ items
  • Performance - Memoize expensive calculations, normalize for large datasets

12. State Persistence and Storage Integration

12.1 localStorage Integration with useState

Pattern Implementation Persistence Use Case
Basic localStorage Read on mount, write on state change Persists across sessions User preferences, settings, themes
useLocalStorage hook Custom hook wrapping useState + localStorage Automatic sync with storage Reusable state persistence
Lazy initialization useState(() => getFromStorage()) Read only once on mount Avoid reading storage on every render
JSON serialization JSON.stringify/parse Store complex objects Objects, arrays (not functions, undefined)

Example: Custom useLocalStorage hook

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  // Lazy initialization - only read from localStorage once
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      // Parse stored json or return initialValue
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return initialValue;
    }
  });

  // Update localStorage whenever state changes
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// Usage
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

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

  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}

// Complex object storage
function UserSettings() {
  const [settings, setSettings] = useLocalStorage('userSettings', {
    notifications: true,
    language: 'en',
    fontSize: 14
  });

  const updateSetting = (key, value) => {
    setSettings(prev => ({ ...prev, [key]: value }));
  };

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={settings.notifications}
          onChange={(e) => updateSetting('notifications', e.target.checked)}
        />
        Notifications
      </label>
      
      <select
        value={settings.language}
        onChange={(e) => updateSetting('language', e.target.value)}
      >
        <option value="en">English</option>
        <option value="es">Spanish</option>
      </select>
    </div>
  );
}
localStorage Limitations:
  • Storage limit: ~5-10MB per domain (varies by browser)
  • Synchronous API - can block main thread with large data
  • Stores strings only - need JSON.stringify/parse for objects
  • Not available in incognito/private mode in some browsers
  • Can throw QuotaExceededError when full - always use try/catch
  • No expiration - data persists indefinitely until cleared

12.2 sessionStorage Patterns for Temporary State

Feature localStorage sessionStorage Best Use
Lifetime Persists until manually cleared Cleared when tab/window closes sessionStorage for temporary data
Scope Shared across all tabs/windows Isolated per tab/window sessionStorage for tab-specific state
Size limit ~5-10MB ~5-10MB Same limits
API window.localStorage window.sessionStorage Same API methods

Example: useSessionStorage for form drafts

function useSessionStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.sessionStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error reading sessionStorage:', error);
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.sessionStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error('Error writing sessionStorage:', error);
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// Usage: Save form draft during session
function ContactForm() {
  const [formData, setFormData] = useSessionStorage('contactFormDraft', {
    name: '',
    email: '',
    message: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      
      // Clear draft after successful submission
      setFormData({ name: '', email: '', message: '' });
      alert('Form submitted!');
    } catch (error) {
      alert('Failed to submit');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name"
        value={formData.name}
        onChange={handleChange}
        placeholder="Name"
      />
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="Email"
      />
      <textarea
        name="message"
        value={formData.message}
        onChange={handleChange}
        placeholder="Message"
      />
      <button type="submit">Send</button>
      <p>💡 Your draft is saved for this tab session</p>
    </form>
  );
}

// Use sessionStorage for:
// - Form drafts (don't persist across sessions)
// - Multi-step wizard progress (tab-specific)
// - Temporary filters/search state
// - Tab-specific UI state (sidebar open/closed)

12.3 IndexedDB Integration for Complex Data

Storage Type Capacity Data Types API Type
localStorage ~5-10MB Strings only (JSON serialized) Synchronous
IndexedDB ~50MB-unlimited (quota based) Objects, Blobs, Files, Arrays, primitives Asynchronous (Promise-based)

Example: IndexedDB wrapper for React state

// Simple IndexedDB wrapper using idb library
import { openDB } from 'idb'; // npm install idb

// Initialize database
const dbPromise = openDB('myApp', 1, {
  upgrade(db) {
    if (!db.objectStoreNames.contains('state')) {
      db.createObjectStore('state');
    }
  }
});

// IndexedDB helpers
const idbStorage = {
  async getItem(key) {
    return (await dbPromise).get('state', key);
  },
  async setItem(key, value) {
    return (await dbPromise).put('state', value, key);
  },
  async removeItem(key) {
    return (await dbPromise).delete('state', key);
  },
  async clear() {
    return (await dbPromise).clear('state');
  }
};

// Custom hook for IndexedDB state
function useIndexedDBState(key, initialValue) {
  const [state, setState] = useState(initialValue);
  const [isLoading, setIsLoading] = useState(true);

  // Load from IndexedDB on mount
  useEffect(() => {
    const loadState = async () => {
      try {
        const storedValue = await idbStorage.getItem(key);
        if (storedValue !== undefined) {
          setState(storedValue);
        }
      } catch (error) {
        console.error('Error loading from IndexedDB:', error);
      } finally {
        setIsLoading(false);
      }
    };

    loadState();
  }, [key]);

  // Save to IndexedDB when state changes
  useEffect(() => {
    if (!isLoading) {
      idbStorage.setItem(key, state).catch(error => {
        console.error('Error saving to IndexedDB:', error);
      });
    }
  }, [key, state, isLoading]);

  return [state, setState, isLoading];
}

// Usage: Store large datasets
function ImageGallery() {
  const [images, setImages, isLoading] = useIndexedDBState('galleryImages', []);

  const addImage = async (file) => {
    const imageData = {
      id: Date.now(),
      name: file.name,
      blob: file, // IndexedDB can store Blob/File directly!
      timestamp: new Date().toISOString()
    };

    setImages(prev => [...prev, imageData]);
  };

  if (isLoading) return <div>Loading gallery...</div>;

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => addImage(e.target.files[0])}
      />
      <div>
        {images.map(img => (
          <div key={img.id}>
            <img src={URL.createObjectURL(img.blob)} alt={img.name} />
            <p>{img.name}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
When to Use IndexedDB:
  • Large datasets (>5MB) - user-generated content, cached API responses
  • Binary data - images, videos, files (Blob/File objects)
  • Offline-first apps - store data for offline access
  • Complex queries - supports indexes and cursors
  • Use libraries: idb, dexie, localforage for easier API

12.4 State Hydration and SSR Compatibility

Issue Problem Solution Pattern
SSR mismatch localStorage unavailable on server Check for window object typeof window !== 'undefined'
Hydration error Server HTML ≠ client initial render Use useEffect for storage access Render default first, update after mount
Flash of content Default state shown before storage loads Show loading state or use script tag Inline script before React loads

Example: SSR-safe localStorage hook

import { useState, useEffect } from 'react';

function useLocalStorageSSR(key, initialValue) {
  // Always start with initialValue on server and first client render
  const [storedValue, setStoredValue] = useState(initialValue);
  const [isHydrated, setIsHydrated] = useState(false);

  // After hydration, read from localStorage
  useEffect(() => {
    // This only runs on client
    try {
      const item = window.localStorage.getItem(key);
      if (item !== null) {
        setStoredValue(JSON.parse(item));
      }
    } catch (error) {
      console.error('Error reading localStorage:', error);
    }
    setIsHydrated(true);
  }, [key]);

  // Save to localStorage when value changes (client only)
  useEffect(() => {
    if (isHydrated) {
      try {
        window.localStorage.setItem(key, JSON.stringify(storedValue));
      } catch (error) {
        console.error('Error writing localStorage:', error);
      }
    }
  }, [key, storedValue, isHydrated]);

  return [storedValue, setStoredValue];
}

// Alternative: Check for window before accessing storage
function useLocalStorageWithCheck(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    // Only access localStorage if window is defined (client-side)
    if (typeof window === 'undefined') {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    if (typeof window !== 'undefined') {
      try {
        window.localStorage.setItem(key, JSON.stringify(storedValue));
      } catch (error) {
        console.error('Error writing localStorage:', error);
      }
    }
  }, [key, storedValue]);

  return [storedValue, setStoredValue];
}

// Next.js example with no hydration mismatch
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorageSSR('theme', 'light');
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  // Don't render theme-dependent content until mounted
  if (!mounted) {
    return <div>Loading...</div>;
  }

  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}
SSR Hydration Issues:
  • Never access localStorage during component render on server
  • Use useEffect to read storage (runs only on client)
  • Return consistent initial state on server and first client render
  • Show loading state or wait for mount before rendering storage-dependent UI
  • For Next.js: Use dynamic imports with ssr: false for storage-dependent components

12.5 Cross-tab State Synchronization Patterns

Method API Direction Use Case
storage event window.addEventListener('storage') Unidirectional (other tabs only) Sync localStorage changes across tabs
BroadcastChannel new BroadcastChannel(name) Bidirectional (all tabs including sender) Custom messages between tabs
SharedWorker new SharedWorker() Shared state across tabs Complex shared state management

Example: Cross-tab sync with storage event

function useLocalStorageSync(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  // Listen for storage changes from other tabs
  useEffect(() => {
    const handleStorageChange = (e) => {
      // Only respond to changes for this key from other tabs
      if (e.key === key && e.newValue !== null) {
        try {
          setStoredValue(JSON.parse(e.newValue));
        } catch (error) {
          console.error('Error parsing storage event:', error);
        }
      }
    };

    // Storage event only fires on OTHER tabs, not the one making the change
    window.addEventListener('storage', handleStorageChange);

    return () => {
      window.removeEventListener('storage', handleStorageChange);
    };
  }, [key]);

  // Update localStorage and state
  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error('Error setting localStorage:', error);
    }
  };

  return [storedValue, setValue];
}

// Usage: Theme syncs across all tabs
function SyncedThemeToggle() {
  const [theme, setTheme] = useLocalStorageSync('theme', 'light');

  return (
    <div>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
      <p>Current theme: {theme}</p>
      <p>💡 Open this page in multiple tabs - theme syncs across all!</p>
    </div>
  );
}

Example: BroadcastChannel for custom messages

function useBroadcastChannel(channelName) {
  const [channel] = useState(() => new BroadcastChannel(channelName));
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const handleMessage = (event) => {
      setMessages(prev => [...prev, event.data]);
    };

    channel.addEventListener('message', handleMessage);

    return () => {
      channel.removeEventListener('message', handleMessage);
      channel.close();
    };
  }, [channel]);

  const postMessage = (data) => {
    channel.postMessage(data);
  };

  return { messages, postMessage };
}

// Usage: Chat across tabs
function CrossTabChat() {
  const { messages, postMessage } = useBroadcastChannel('chat');
  const [input, setInput] = useState('');

  const sendMessage = () => {
    if (input.trim()) {
      postMessage({
        text: input,
        timestamp: Date.now(),
        sender: 'Tab ' + Math.random().toString(36).substr(2, 4)
      });
      setInput('');
    }
  };

  return (
    <div>
      <div>
        {messages.map((msg, i) => (
          <div key={i}>
            <strong>{msg.sender}:</strong> {msg.text}
          </div>
        ))}
      </div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
      />
      <button onClick={sendMessage}>Send</button>
      <p>💡 Messages broadcast to all open tabs</p>
    </div>
  );
}
Cross-tab Sync Use Cases:
  • Authentication: Logout in one tab = logout all tabs
  • Shopping cart: Add item in tab A, see it in tab B
  • Notifications: Mark as read in one tab, update badge in all
  • Real-time collaboration: Multiple tabs editing same document
  • ⚠️ storage event doesn't fire in originating tab - need BroadcastChannel for that

12.6 State Backup and Recovery Strategies

Strategy Trigger Storage Purpose
Auto-save draft Debounced state changes localStorage/sessionStorage Recover unsaved work after crash/reload
Versioned state Major state changes Array of snapshots Undo/redo, state history
Export/Import Manual user action JSON file download Backup to disk, transfer between devices
Cloud sync Periodic or on change Backend API Cross-device sync, data safety

Example: Auto-save with recovery

function useAutoSave(key, data, delay = 2000) {
  const [lastSaved, setLastSaved] = useState(null);
  const [isDirty, setIsDirty] = useState(false);

  // Debounced auto-save
  useEffect(() => {
    setIsDirty(true);
    
    const timer = setTimeout(() => {
      try {
        localStorage.setItem(key, JSON.stringify({
          data,
          savedAt: new Date().toISOString()
        }));
        setLastSaved(new Date());
        setIsDirty(false);
      } catch (error) {
        console.error('Auto-save failed:', error);
      }
    }, delay);

    return () => clearTimeout(timer);
  }, [key, data, delay]);

  return { lastSaved, isDirty };
}

// Usage: Document editor with auto-save
function DocumentEditor() {
  const [content, setContent] = useState('');
  const [recovered, setRecovered] = useState(false);

  // Try to recover saved draft on mount
  useEffect(() => {
    const saved = localStorage.getItem('documentDraft');
    if (saved) {
      try {
        const { data, savedAt } = JSON.parse(saved);
        const shouldRecover = window.confirm(
          `Found draft from ${new Date(savedAt).toLocaleString()}. Recover?`
        );
        if (shouldRecover) {
          setContent(data);
          setRecovered(true);
        } else {
          localStorage.removeItem('documentDraft');
        }
      } catch (error) {
        console.error('Failed to recover draft:', error);
      }
    }
  }, []);

  const { lastSaved, isDirty } = useAutoSave('documentDraft', content, 2000);

  const clearDraft = () => {
    localStorage.removeItem('documentDraft');
    setContent('');
  };

  return (
    <div>
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        rows={10}
        cols={50}
        placeholder="Start typing..."
      />
      
      <div>
        {isDirty && <span>💾 Saving...</span>}
        {!isDirty && lastSaved && (
          <span>✓ Saved at {lastSaved.toLocaleTimeString()}</span>
        )}
      </div>
      
      {recovered && <p>✓ Draft recovered</p>}
      
      <button onClick={clearDraft}>Clear Draft</button>
    </div>
  );
}

Example: Export/Import state as JSON

function useStateBackup(state, filename = 'state-backup.json') {
  // Export state to JSON file
  const exportState = () => {
    const dataStr = JSON.stringify(state, null, 2);
    const dataBlob = new Blob([dataStr], { type: 'application/json' });
    const url = URL.createObjectURL(dataBlob);
    
    const link = document.createElement('a');
    link.href = url;
    link.download = filename;
    link.click();
    
    URL.revokeObjectURL(url);
  };

  // Import state from JSON file
  const importState = () => {
    return new Promise((resolve, reject) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = 'application/json';
      
      input.onchange = (e) => {
        const file = e.target.files[0];
        const reader = new FileReader();
        
        reader.onload = (event) => {
          try {
            const imported = JSON.parse(event.target.result);
            resolve(imported);
          } catch (error) {
            reject(error);
          }
        };
        
        reader.readAsText(file);
      };
      
      input.click();
    });
  };

  return { exportState, importState };
}

// Usage: Backup app settings
function SettingsManager() {
  const [settings, setSettings] = useState({
    theme: 'light',
    language: 'en',
    notifications: true
  });

  const { exportState, importState } = useStateBackup(
    settings,
    'my-settings.json'
  );

  const handleImport = async () => {
    try {
      const imported = await importState();
      setSettings(imported);
      alert('Settings imported successfully!');
    } catch (error) {
      alert('Failed to import settings');
    }
  };

  return (
    <div>
      <h3>Settings</h3>
      {/* Settings UI */}
      
      <div>
        <button onClick={exportState}>Export Settings</button>
        <button onClick={handleImport}>Import Settings</button>
      </div>
    </div>
  );
}

State Persistence Summary:

  • localStorage - 5-10MB, persists forever, synchronous, strings only
  • sessionStorage - Tab-scoped, cleared on close, same API as localStorage
  • IndexedDB - Large capacity, async, stores objects/blobs, use idb/dexie libraries
  • SSR compatibility - Check typeof window, use useEffect for storage access
  • Cross-tab sync - storage event for localStorage, BroadcastChannel for custom messages
  • Auto-save - Debounce saves, show save status, offer draft recovery
  • Export/Import - Download/upload JSON for backups and device transfer
  • Error handling - Always wrap storage access in try/catch (quota errors)
  • Lazy initialization - Use useState(() => getStorage()) to read only once

13. Advanced State Patterns and Techniques

13.1 State Machine Implementation with XState Integration

Concept Implementation Description Use Case
State Machine createMachine(config) Finite state machine with explicit states and transitions Complex UI flows with strict state transitions (auth, checkout, wizards)
XState Integration useMachine(machine) React hook for state machine with interpreter Type-safe state management with visual state charts
States states: { idle, loading, success, error } Mutually exclusive states with entry/exit actions Prevent impossible states (loading + error simultaneously)
Transitions on: { EVENT: 'targetState' } Event-driven state changes with guards and actions Controlled flow preventing invalid state transitions
Context context: { data, errorMsg } Extended state data alongside finite states Store additional data while maintaining strict state logic
Actions actions: { assignData, logError } Side effects on transitions (assign, send, raise) Update context, trigger effects during state changes
Guards cond: (context) => boolean Conditional transitions based on context/event Validate before transition (check permissions, quotas)
Services invoke: { src: promiseCreator } Async operations with automatic state management API calls with loading/success/error handling

Example: Authentication state machine with XState

import { createMachine } from 'xstate';
import { useMachine } from '@xstate/react';

// Define state machine
const authMachine = createMachine({
  id: 'auth',
  initial: 'idle',
  context: { user: null, error: null },
  states: {
    idle: {
      on: { LOGIN: 'authenticating' }
    },
    authenticating: {
      invoke: {
        src: (context, event) => loginAPI(event.credentials),
        onDone: {
          target: 'authenticated',
          actions: 'assignUser'
        },
        onError: {
          target: 'failed',
          actions: 'assignError'
        }
      }
    },
    authenticated: {
      on: { LOGOUT: 'idle' }
    },
    failed: {
      on: { RETRY: 'authenticating' }
    }
  }
}, {
  actions: {
    assignUser: (context, event) => ({ user: event.data }),
    assignError: (context, event) => ({ error: event.data })
  }
});

// Use in component
function AuthFlow() {
  const [state, send] = useMachine(authMachine);
  
  return (
    <div>
      {state.matches('idle') && (
        <button onClick={() => send({ type: 'LOGIN', credentials })}>
          Login
        </button>
      )}
      {state.matches('authenticating') && <Spinner />}
      {state.matches('authenticated') && (
        <div>Welcome {state.context.user.name}</div>
      )}
      {state.matches('failed') && (
        <div>
          {state.context.error}
          <button onClick={() => send('RETRY')}>Retry</button>
        </div>
      )}
    </div>
  );
}

Example: Simple state machine without XState (pure React)

function useStateMachine(initialState, transitions) {
  const [state, setState] = useState(initialState);
  
  const send = useCallback((event) => {
    setState(currentState => {
      const transition = transitions[currentState]?.[event];
      return transition || currentState;
    });
  }, [transitions]);
  
  return [state, send];
}

// Usage
const transitions = {
  idle: { START: 'loading' },
  loading: { SUCCESS: 'success', ERROR: 'error' },
  success: { RESET: 'idle' },
  error: { RETRY: 'loading', RESET: 'idle' }
};

const [state, send] = useStateMachine('idle', transitions);
Note: State machines prevent impossible states and clarify business logic. Use XState for complex flows with nested states, parallel states, and visual tooling. For simpler cases, a custom useReducer with strict action types may suffice.

13.2 Undo/Redo State Management with History Stack

Pattern Implementation Description Considerations
History Stack { past: [], present, future: [] } Three arrays tracking state timeline past = previous states, present = current, future = undone states
Undo Operation past.pop() → present, present → future Move current to future, restore from past Disable when past is empty
Redo Operation future.pop() → present, present → past Move current to past, restore from future Disable when future is empty
New Action present → past, clear future Save current to past, clear redo stack Any new action invalidates future (redo)
Memory Limit past.slice(-limit) Keep only N recent states to prevent memory bloat Typical limit: 50-100 states depending on data size
Grouping Actions batch(() => { ... }) Group multiple changes as single history entry Prevents undo granularity issues (e.g., typing letter-by-letter)
Selective History if (action.trackable) saveToHistory() Only track certain actions in history Exclude UI state changes (hover, focus) from undo/redo
Deep Clone structuredClone(state) Create immutable snapshots of state Use structuredClone or JSON parse/stringify for deep copies

Example: Complete undo/redo implementation with useReducer

function undoableReducer(state, action) {
  const { past, present, future } = state;
  
  switch (action.type) {
    case 'UNDO':
      if (past.length === 0) return state;
      return {
        past: past.slice(0, -1),
        present: past[past.length - 1],
        future: [present, ...future]
      };
      
    case 'REDO':
      if (future.length === 0) return state;
      return {
        past: [...past, present],
        present: future[0],
        future: future.slice(1)
      };
      
    case 'SET':
      return {
        past: [...past, present].slice(-50), // Keep last 50
        present: action.payload,
        future: [] // Clear redo on new action
      };
      
    case 'RESET':
      return {
        past: [],
        present: action.payload,
        future: []
      };
      
    default:
      return state;
  }
}

function useUndoable(initialState) {
  const [state, dispatch] = useReducer(undoableReducer, {
    past: [],
    present: initialState,
    future: []
  });
  
  const canUndo = state.past.length > 0;
  const canRedo = state.future.length > 0;
  
  const undo = useCallback(() => dispatch({ type: 'UNDO' }), []);
  const redo = useCallback(() => dispatch({ type: 'REDO' }), []);
  const set = useCallback((newState) => 
    dispatch({ type: 'SET', payload: newState }), []);
  const reset = useCallback((newState) => 
    dispatch({ type: 'RESET', payload: newState }), []);
  
  return {
    state: state.present,
    set,
    undo,
    redo,
    canUndo,
    canRedo,
    reset
  };
}

// Usage in drawing app
function DrawingCanvas() {
  const { state: canvas, set, undo, redo, canUndo, canRedo } = 
    useUndoable({ shapes: [] });
  
  const addShape = (shape) => {
    set({ shapes: [...canvas.shapes, shape] });
  };
  
  return (
    <>
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
      <Canvas shapes={canvas.shapes} onAddShape={addShape} />
    </>
  );
}

Example: Debounced history for text input (grouped actions)

function useUndoableText(initialText) {
  const { state, set, undo, redo, canUndo, canRedo } = 
    useUndoable(initialText);
  const [tempText, setTempText] = useState(state);
  
  // Save to history after 500ms of no typing
  useEffect(() => {
    if (tempText !== state) {
      const timeout = setTimeout(() => set(tempText), 500);
      return () => clearTimeout(timeout);
    }
  }, [tempText, state, set]);
  
  return {
    text: tempText,
    setText: setTempText,
    undo: () => { setTempText(state); undo(); },
    redo: () => { redo(); setTempText(state); },
    canUndo,
    canRedo
  };
}
Warning: Deep cloning large state objects can be expensive. Consider storing diffs/patches instead of full snapshots for large datasets. Libraries like Immer can help with structural sharing.

13.3 Optimistic UI Updates and Rollback Patterns

Pattern Implementation Description Use Case
Optimistic Update updateUI() → API call Update UI immediately before server confirms Like buttons, post creation, todo completion - instant feedback
Rollback on Error catch(error) => revertState() Restore previous state if API fails Handle network failures, validation errors, conflicts
Temporary ID tempId: 'temp-' + Date.now() Client-side ID replaced by server ID on success Add items to list before server confirmation
Pending State { id, data, status: 'pending' } Mark items as unconfirmed with visual indicator Show loading spinner or opacity on pending items
Retry Logic retry(fn, attempts, delay) Automatically retry failed requests Transient network errors, rate limits
Conflict Resolution merge(local, server) Handle concurrent modifications Multiple users editing same data, last-write-wins vs merge
Version/Etag If-Match: version Detect stale updates with versioning Prevent overwriting newer server data with old local changes
Undo Message showToast('Saved', { undo: rollback }) Allow manual rollback with toast/snackbar Delete operations, bulk actions - give user undo option

Example: Optimistic todo creation with rollback

function useTodos() {
  const [todos, setTodos] = useState([]);
  
  const addTodo = async (text) => {
    // Generate temporary ID
    const tempId = `temp-${Date.now()}`;
    const newTodo = { id: tempId, text, completed: false, pending: true };
    
    // Optimistic update
    setTodos(prev => [...prev, newTodo]);
    
    try {
      // API call
      const response = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text })
      });
      const savedTodo = await response.json();
      
      // Replace temp with real todo
      setTodos(prev => prev.map(t => 
        t.id === tempId 
          ? { ...savedTodo, pending: false }
          : t
      ));
      
    } catch (error) {
      // Rollback on error
      setTodos(prev => prev.filter(t => t.id !== tempId));
      toast.error('Failed to add todo: ' + error.message);
    }
  };
  
  const toggleTodo = async (id) => {
    // Save previous state for rollback
    const previous = todos.find(t => t.id === id);
    
    // Optimistic update
    setTodos(prev => prev.map(t =>
      t.id === id ? { ...t, completed: !t.completed, pending: true } : t
    ));
    
    try {
      await fetch(`/api/todos/${id}`, {
        method: 'PATCH',
        body: JSON.stringify({ completed: !previous.completed })
      });
      
      // Confirm success
      setTodos(prev => prev.map(t =>
        t.id === id ? { ...t, pending: false } : t
      ));
      
    } catch (error) {
      // Rollback
      setTodos(prev => prev.map(t =>
        t.id === id ? previous : t
      ));
      toast.error('Update failed', { 
        action: { label: 'Retry', onClick: () => toggleTodo(id) }
      });
    }
  };
  
  return { todos, addTodo, toggleTodo };
}

Example: Optimistic delete with undo toast

function useOptimisticDelete() {
  const [items, setItems] = useState([]);
  const timeoutRef = useRef(null);
  
  const deleteItem = (id) => {
    // Save for rollback
    const deleted = items.find(item => item.id === id);
    
    // Optimistic removal
    setItems(prev => prev.filter(item => item.id !== id));
    
    // Clear any pending deletes
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
    
    // Show undo toast
    const undo = () => {
      clearTimeout(timeoutRef.current);
      setItems(prev => [...prev, deleted].sort((a, b) => a.id - b.id));
      toast.dismiss();
    };
    
    toast.success('Item deleted', {
      duration: 5000,
      action: { label: 'Undo', onClick: undo }
    });
    
    // Actual delete after 5 seconds
    timeoutRef.current = setTimeout(async () => {
      try {
        await fetch(`/api/items/${id}`, { method: 'DELETE' });
      } catch (error) {
        // Rollback on error
        setItems(prev => [...prev, deleted]);
        toast.error('Delete failed: ' + error.message);
      }
    }, 5000);
  };
  
  return { items, deleteItem };
}
Note: Optimistic updates improve perceived performance but require careful error handling. Always show pending state visually, provide rollback mechanisms, and handle conflicts gracefully. Use version numbers or ETags to detect stale updates.

13.4 State Normalization for Relational Data

Concept Structure Benefits Implementation
Normalized State { entities: {}, ids: [] } Flat structure, no duplication, easy updates Store entities in objects keyed by ID, separate array of IDs for order
Entities by ID { users: { '1': {...}, '2': {...} } } O(1) lookups, no array iteration Object/Map keyed by entity ID
ID Arrays { userIds: ['1', '2', '3'] } Preserve order, maintain relationships Separate arrays for ordering and filtering
Relationships { postId: '123', authorId: '456' } Reference by ID instead of nesting Store IDs instead of nested objects
Selectors selectUser(state, id) Derive denormalized data for views Functions to join normalized data for rendering
normalizr Library normalize(data, schema) Automatic normalization from API responses Define entity schemas, normalize nested JSON
Redux Toolkit createEntityAdapter() Built-in CRUD reducers for normalized state Auto-generates selectors and reducers for entities
Update Performance Update single entity in O(1) No need to search/update nested arrays Direct object key access for updates

Example: Normalizing nested blog post data

// Nested API response (denormalized)
const apiResponse = {
  posts: [
    {
      id: '1',
      title: 'Post 1',
      author: { id: '10', name: 'Alice' },
      comments: [
        { id: '100', text: 'Great!', author: { id: '20', name: 'Bob' } },
        { id: '101', text: 'Thanks', author: { id: '10', name: 'Alice' } }
      ]
    },
    {
      id: '2',
      title: 'Post 2',
      author: { id: '20', name: 'Bob' },
      comments: []
    }
  ]
};

// Normalized state structure
const normalizedState = {
  users: {
    '10': { id: '10', name: 'Alice' },
    '20': { id: '20', name: 'Bob' }
  },
  posts: {
    '1': { id: '1', title: 'Post 1', authorId: '10', commentIds: ['100', '101'] },
    '2': { id: '2', title: 'Post 2', authorId: '20', commentIds: [] }
  },
  comments: {
    '100': { id: '100', text: 'Great!', authorId: '20', postId: '1' },
    '101': { id: '101', text: 'Thanks', authorId: '10', postId: '1' }
  },
  postIds: ['1', '2']
};

// Normalize function
function normalizePosts(posts) {
  const users = {};
  const postsById = {};
  const comments = {};
  const postIds = [];
  
  posts.forEach(post => {
    // Add user
    users[post.author.id] = post.author;
    
    // Add comments
    const commentIds = post.comments.map(comment => {
      users[comment.author.id] = comment.author;
      comments[comment.id] = {
        id: comment.id,
        text: comment.text,
        authorId: comment.author.id,
        postId: post.id
      };
      return comment.id;
    });
    
    // Add post
    postsById[post.id] = {
      id: post.id,
      title: post.title,
      authorId: post.author.id,
      commentIds
    };
    postIds.push(post.id);
  });
  
  return { users, posts: postsById, comments, postIds };
}

Example: Selectors to denormalize for rendering

// Selector to get post with author and comments
function selectPostWithDetails(state, postId) {
  const post = state.posts[postId];
  if (!post) return null;
  
  return {
    ...post,
    author: state.users[post.authorId],
    comments: post.commentIds.map(id => ({
      ...state.comments[id],
      author: state.users[state.comments[id].authorId]
    }))
  };
}

// Usage in component
function PostDetail({ postId }) {
  const [state, setState] = useState(normalizedState);
  const post = useMemo(
    () => selectPostWithDetails(state, postId),
    [state, postId]
  );
  
  if (!post) return <div>Not found</div>;
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>By {post.author.name}</p>
      <div>
        {post.comments.map(c => (
          <div key={c.id}>
            {c.text} - {c.author.name}
          </div>
        ))}
      </div>
    </article>
  );
}

// Easy updates with normalized structure
function updateUsername(state, userId, newName) {
  return {
    ...state,
    users: {
      ...state.users,
      [userId]: { ...state.users[userId], name: newName }
    }
  };
  // Name updated everywhere it's referenced!
}

Example: Using Redux Toolkit's createEntityAdapter

import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';

// Create adapter with auto-generated reducers
const postsAdapter = createEntityAdapter({
  selectId: (post) => post.id,
  sortComparer: (a, b) => b.createdAt - a.createdAt
});

const postsSlice = createSlice({
  name: 'posts',
  initialState: postsAdapter.getInitialState(),
  reducers: {
    // Auto-generated: addOne, addMany, setAll, updateOne, removeOne, etc.
    postAdded: postsAdapter.addOne,
    postsReceived: postsAdapter.setAll,
    postUpdated: postsAdapter.updateOne,
    postRemoved: postsAdapter.removeOne
  }
});

// Auto-generated selectors
const postsSelectors = postsAdapter.getSelectors(state => state.posts);
// Provides: selectIds, selectEntities, selectAll, selectTotal, selectById

// Usage
dispatch(postsSlice.actions.postAdded({ id: '1', title: 'New Post' }));
const allPosts = postsSelectors.selectAll(state);
const post = postsSelectors.selectById(state, '1');
Note: Normalize when you have relational data, frequent updates, or need to display same entity in multiple places. Denormalize in selectors for rendering. Libraries like normalizr and Redux Toolkit simplify normalization significantly.

13.5 Event Sourcing Patterns in React State

Concept Description Implementation Benefits
Event Sourcing Store sequence of events instead of current state events = [{ type, payload, timestamp }] Complete audit trail, time travel, replay capability
Event Store Append-only log of all events const events = useState([]) Immutable history, never delete/modify events
Event Replay Reconstruct state by replaying events events.reduce(reducer, initialState) Derive current state from event history
Projections Different views of same event stream Multiple reducers consuming same events Create multiple state representations from one source
Snapshots Cached state at specific points in time { snapshot, eventsAfter } Performance optimization, avoid replaying all events
Event Metadata Timestamp, user, version, correlation ID { ...event, meta: { timestamp, userId } } Audit trail, debugging, analytics
Event Versioning Handle schema changes over time { version: 2, type: 'UserCreated' } Backward compatibility, event migration
Temporal Queries Query state at any point in time replayUntil(timestamp) Historical analysis, debugging past states

Example: Event-sourced shopping cart

// Event definitions
const events = {
  ITEM_ADDED: 'ITEM_ADDED',
  ITEM_REMOVED: 'ITEM_REMOVED',
  QUANTITY_CHANGED: 'QUANTITY_CHANGED',
  CART_CLEARED: 'CART_CLEARED'
};

// Event reducer to replay events
function cartReducer(state, event) {
  switch (event.type) {
    case events.ITEM_ADDED:
      return {
        ...state,
        items: { 
          ...state.items, 
          [event.payload.id]: event.payload 
        }
      };
      
    case events.ITEM_REMOVED:
      const { [event.payload.id]: removed, ...remaining } = state.items;
      return { ...state, items: remaining };
      
    case events.QUANTITY_CHANGED:
      return {
        ...state,
        items: {
          ...state.items,
          [event.payload.id]: {
            ...state.items[event.payload.id],
            quantity: event.payload.quantity
          }
        }
      };
      
    case events.CART_CLEARED:
      return { ...state, items: {} };
      
    default:
      return state;
  }
}

function useEventSourcedCart() {
  const [eventStore, setEventStore] = useState([]);
  const [snapshot, setSnapshot] = useState({ items: {} });
  
  // Replay events to get current state
  const currentState = useMemo(() => {
    return eventStore.reduce(cartReducer, snapshot);
  }, [eventStore, snapshot]);
  
  // Add event to store
  const dispatch = useCallback((event) => {
    const enrichedEvent = {
      ...event,
      meta: {
        timestamp: Date.now(),
        userId: getCurrentUserId()
      }
    };
    
    setEventStore(prev => [...prev, enrichedEvent]);
    
    // Create snapshot every 50 events
    if (eventStore.length % 50 === 0) {
      setSnapshot(currentState);
      setEventStore([]);
    }
  }, [eventStore.length, currentState]);
  
  // Get state at specific time
  const getStateAt = useCallback((timestamp) => {
    const eventsUntil = eventStore.filter(e => 
      e.meta.timestamp <= timestamp
    );
    return eventsUntil.reduce(cartReducer, snapshot);
  }, [eventStore, snapshot]);
  
  return {
    state: currentState,
    dispatch,
    events: eventStore,
    getStateAt
  };
}

// Usage
function ShoppingCart() {
  const { state, dispatch, getStateAt } = useEventSourcedCart();
  
  const addItem = (item) => {
    dispatch({
      type: events.ITEM_ADDED,
      payload: { id: item.id, name: item.name, quantity: 1, price: item.price }
    });
  };
  
  const viewYesterdaysCart = () => {
    const yesterday = Date.now() - 24 * 60 * 60 * 1000;
    const historicalState = getStateAt(yesterday);
    console.log('Cart 24h ago:', historicalState);
  };
  
  return (
    <div>
      {Object.values(state.items).map(item => (
        <CartItem key={item.id} item={item} />
      ))}
    </div>
  );
}

Example: Event projections for analytics

// Different projections from same event stream
function useEventProjections(eventStore) {
  // Projection 1: Total revenue
  const totalRevenue = useMemo(() => {
    return eventStore
      .filter(e => e.type === 'ORDER_COMPLETED')
      .reduce((sum, e) => sum + e.payload.amount, 0);
  }, [eventStore]);
  
  // Projection 2: Popular products
  const popularProducts = useMemo(() => {
    const counts = {};
    eventStore
      .filter(e => e.type === 'ITEM_ADDED')
      .forEach(e => {
        counts[e.payload.id] = (counts[e.payload.id] || 0) + 1;
      });
    return Object.entries(counts)
      .sort(([,a], [,b]) => b - a)
      .slice(0, 10);
  }, [eventStore]);
  
  // Projection 3: User activity timeline
  const userActivity = useMemo(() => {
    return eventStore
      .filter(e => e.meta.userId === currentUserId)
      .map(e => ({
        action: e.type,
        timestamp: e.meta.timestamp,
        details: e.payload
      }));
  }, [eventStore]);
  
  return { totalRevenue, popularProducts, userActivity };
}
Warning: Event sourcing increases complexity and memory usage. Use snapshots to limit event replay overhead. Consider persisting events to backend for durability. Best for audit-critical applications like financial systems, collaborative editors, or analytics platforms.

13.6 CQRS (Command Query Responsibility Segregation) Patterns

Concept Description Implementation Use Case
CQRS Pattern Separate read and write models Different structures for commands vs queries Complex domains where read/write needs differ significantly
Command Model Handles state mutations (write) dispatch({ type: 'CREATE_USER', payload }) Business logic, validation, side effects
Query Model Optimized for reading data useQuery('users', filters) Denormalized views, pre-computed aggregates
Command Handler Process commands with validation execute(command) → events Enforce business rules before state changes
Query Handler Fetch and transform data for views query(criteria) → viewModel Efficient data retrieval with projections
Eventual Consistency Query model updates asynchronously Commands succeed, reads may lag slightly High-performance systems with acceptable staleness
Event Bus Commands emit events, queries subscribe eventBus.emit('UserCreated', data) Decouple command and query sides
Materialized Views Pre-computed query results Update views when events occur Fast reads for complex aggregations/joins

Example: CQRS pattern for task management

// Command side - handles writes
const commandHandlers = {
  CREATE_TASK: async (command) => {
    // Validation
    if (!command.payload.title) {
      throw new Error('Title required');
    }
    
    // Business logic
    const task = {
      id: generateId(),
      title: command.payload.title,
      status: 'pending',
      createdAt: Date.now(),
      createdBy: command.meta.userId
    };
    
    // Persist and emit event
    await saveTask(task);
    eventBus.emit('TaskCreated', task);
    
    return { success: true, taskId: task.id };
  },
  
  COMPLETE_TASK: async (command) => {
    const task = await getTask(command.payload.id);
    
    if (!task) throw new Error('Task not found');
    if (task.status === 'completed') throw new Error('Already completed');
    
    await updateTaskStatus(task.id, 'completed');
    eventBus.emit('TaskCompleted', { 
      id: task.id, 
      completedAt: Date.now() 
    });
    
    return { success: true };
  }
};

// Query side - optimized for reads
const queryHandlers = {
  GET_TASKS_BY_STATUS: (status) => {
    // Read from optimized view
    return queryViews.tasksByStatus[status] || [];
  },
  
  GET_USER_TASK_STATS: (userId) => {
    // Pre-computed aggregates
    return {
      total: queryViews.userTaskCounts[userId]?.total || 0,
      completed: queryViews.userTaskCounts[userId]?.completed || 0,
      pending: queryViews.userTaskCounts[userId]?.pending || 0
    };
  },
  
  GET_TASK_TIMELINE: (taskId) => {
    // Materialized view of task history
    return queryViews.taskTimelines[taskId] || [];
  }
};

// Query views updated by events
const queryViews = {
  tasksByStatus: { pending: [], completed: [] },
  userTaskCounts: {},
  taskTimelines: {}
};

eventBus.on('TaskCreated', (task) => {
  // Update query views
  queryViews.tasksByStatus[task.status].push(task);
  
  if (!queryViews.userTaskCounts[task.createdBy]) {
    queryViews.userTaskCounts[task.createdBy] = { total: 0, completed: 0, pending: 0 };
  }
  queryViews.userTaskCounts[task.createdBy].total++;
  queryViews.userTaskCounts[task.createdBy][task.status]++;
  
  queryViews.taskTimelines[task.id] = [
    { event: 'Created', timestamp: task.createdAt }
  ];
});

eventBus.on('TaskCompleted', (data) => {
  // Move task between status views
  const taskIndex = queryViews.tasksByStatus.pending
    .findIndex(t => t.id === data.id);
  const task = queryViews.tasksByStatus.pending.splice(taskIndex, 1)[0];
  task.status = 'completed';
  queryViews.tasksByStatus.completed.push(task);
  
  // Update stats
  const userId = task.createdBy;
  queryViews.userTaskCounts[userId].pending--;
  queryViews.userTaskCounts[userId].completed++;
  
  // Update timeline
  queryViews.taskTimelines[data.id].push({
    event: 'Completed',
    timestamp: data.completedAt
  });
});

Example: React hook for CQRS pattern

function useCQRS() {
  const [queryState, setQueryState] = useState(queryViews);
  
  // Command dispatcher
  const executeCommand = useCallback(async (command) => {
    const handler = commandHandlers[command.type];
    if (!handler) throw new Error(`Unknown command: ${command.type}`);
    
    const enrichedCommand = {
      ...command,
      meta: {
        timestamp: Date.now(),
        userId: getCurrentUserId()
      }
    };
    
    return await handler(enrichedCommand);
  }, []);
  
  // Query executor
  const executeQuery = useCallback((queryName, ...args) => {
    const handler = queryHandlers[queryName];
    if (!handler) throw new Error(`Unknown query: ${queryName}`);
    
    return handler(...args);
  }, []);
  
  // Subscribe to query view updates
  useEffect(() => {
    const updateViews = () => setQueryState({...queryViews});
    
    eventBus.on('TaskCreated', updateViews);
    eventBus.on('TaskCompleted', updateViews);
    
    return () => {
      eventBus.off('TaskCreated', updateViews);
      eventBus.off('TaskCompleted', updateViews);
    };
  }, []);
  
  return { executeCommand, executeQuery };
}

// Usage in component
function TaskManager() {
  const { executeCommand, executeQuery } = useCQRS();
  
  const createTask = async (title) => {
    await executeCommand({ 
      type: 'CREATE_TASK', 
      payload: { title } 
    });
  };
  
  const pendingTasks = executeQuery('GET_TASKS_BY_STATUS', 'pending');
  const userStats = executeQuery('GET_USER_TASK_STATS', currentUserId);
  
  return (
    <div>
      <div>Pending: {userStats.pending}, Completed: {userStats.completed}</div>
      {pendingTasks.map(task => (
        <TaskCard key={task.id} task={task} />
      ))}
    </div>
  );
}
Note: CQRS adds complexity but enables independent scaling and optimization of reads vs writes. Use when read/write patterns differ significantly or when you need specialized views of the same data. Often combined with event sourcing for complete audit trails.

Section 13 Key Takeaways

  • State machines prevent impossible states with explicit transitions - use XState for complex flows
  • Undo/redo requires history stack (past/present/future) with memory limits and action grouping
  • Optimistic updates improve UX but need rollback, pending states, and conflict resolution
  • Normalization eliminates duplication, enables O(1) updates - use for relational data
  • Event sourcing stores events not state - enables time travel, audit trails, multiple projections
  • CQRS separates read/write models for independent optimization and scaling

14. External State Management Library Integration

14.1 Redux Toolkit (RTK) Integration Patterns

Feature API Description Use Case
configureStore configureStore({ reducer }) Create Redux store with good defaults (DevTools, middleware) Single store setup with automatic configuration
createSlice createSlice({ name, initialState, reducers }) Generate action creators and reducers automatically Reduce boilerplate, use Immer for immutability
createAsyncThunk createAsyncThunk('name', async (arg) => ...) Handle async logic with pending/fulfilled/rejected actions API calls with automatic loading/error state
createEntityAdapter createEntityAdapter() Normalized state with CRUD reducers and selectors Managing collections of entities (users, posts, etc.)
createSelector createSelector([input], (data) => result) Memoized selectors for derived state Compute derived data without re-renders
RTK Query createApi({ endpoints }) Data fetching and caching solution Replace manual async thunks with auto-caching API layer
useSelector useSelector(state => state.slice) Extract data from Redux store in components Subscribe to specific state slices
useDispatch const dispatch = useDispatch() Get dispatch function for triggering actions Update state via action creators

Example: Complete Redux Toolkit setup with slice

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, history: [] },
  reducers: {
    increment: (state) => {
      state.value += 1; // Immer allows "mutations"
      state.history.push({ action: 'increment', timestamp: Date.now() });
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
    reset: (state) => {
      state.value = 0;
      state.history = [];
    }
  }
});

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

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});

// App.js
import { Provider } from 'react-redux';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <Counter />
    </Provider>
  );
}

// Counter.js
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from './counterSlice';

function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
    </div>
  );
}

Example: Async thunk for API calls

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// Async thunk
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (page, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users?page=${page}`);
      return await response.json();
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: [],
    loading: false,
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.entities = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  }
});

// Component usage
function UserList() {
  const { entities, loading, error } = useSelector(state => state.users);
  const dispatch = useDispatch();
  
  useEffect(() => {
    dispatch(fetchUsers(1));
  }, [dispatch]);
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  
  return (
    <ul>
      {entities.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Note: Redux Toolkit is the recommended way to use Redux. It includes Immer for immutable updates, Redux Thunk for async, and Redux DevTools by default. Use RTK Query for data fetching instead of manually writing thunks.

14.2 Zustand Store Implementation and Usage

Feature API Description Benefits
create create((set, get) => ({ ... })) Create a hook-based store No providers, minimal boilerplate, auto-generates hooks
set set({ field: value }) Merge updates into state Partial updates, can be used with functions
get get() Read current state in actions Access state without React rendering
Selectors useStore(state => state.field) Subscribe to specific state slices Automatic re-render optimization
Middleware persist, devtools, immer Extend store with additional functionality Persist to storage, debug, immutable updates
Transient Updates set({ ... }, true) Update without triggering subscribers Temporary state that doesn't cause re-renders
subscribe store.subscribe(listener) Listen to state changes outside React Integration with non-React code
Shallow Equality useStore(selector, shallow) Shallow comparison for object selectors Prevent re-renders when object content unchanged

Example: Complete Zustand store with actions

import { create } from 'zustand';

// Create store
const useStore = create((set, get) => ({
  // State
  bears: 0,
  fish: 0,
  
  // Actions
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
  
  // Action using get() to read current state
  eatFish: () => set((state) => ({ 
    fish: state.fish - 1,
    bears: state.bears + 0.1 
  })),
  
  // Async action
  fetchInitialData: async () => {
    const response = await fetch('/api/wildlife');
    const data = await response.json();
    set({ bears: data.bears, fish: data.fish });
  },
  
  // Computed getter
  getTotalAnimals: () => {
    const state = get();
    return state.bears + state.fish;
  }
}));

// Component usage - subscribe to entire store
function AllCounts() {
  const { bears, fish } = useStore();
  return <div>Bears: {bears}, Fish: {fish}</div>;
}

// Component usage - subscribe to specific field
function BearCounter() {
  const bears = useStore(state => state.bears);
  return <h1>{bears} bears</h1>;
}

// Component usage - access actions
function Controls() {
  const increasePopulation = useStore(state => state.increasePopulation);
  const removeAllBears = useStore(state => state.removeAllBears);
  
  return (
    <div>
      <button onClick={increasePopulation}>Add Bear</button>
      <button onClick={removeAllBears}>Remove All</button>
    </div>
  );
}

Example: Zustand with persistence and DevTools

import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

const useStore = create(
  devtools(
    persist(
      immer((set) => ({
        todos: [],
        
        addTodo: (text) => set((state) => {
          // Immer allows "mutations"
          state.todos.push({ 
            id: Date.now(), 
            text, 
            completed: false 
          });
        }),
        
        toggleTodo: (id) => set((state) => {
          const todo = state.todos.find(t => t.id === id);
          if (todo) todo.completed = !todo.completed;
        }),
        
        removeTodo: (id) => set((state) => {
          state.todos = state.todos.filter(t => t.id !== id);
        })
      })),
      { name: 'todo-storage' } // localStorage key
    )
  )
);

// Slice pattern - multiple stores
const useUserStore = create((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  logout: () => set({ user: null })
}));

const useCartStore = create((set) => ({
  items: [],
  addItem: (item) => set((state) => ({ 
    items: [...state.items, item] 
  }))
}));

// Use multiple stores in component
function App() {
  const user = useUserStore(state => state.user);
  const cartItems = useCartStore(state => state.items);
  
  return <div>{user?.name} has {cartItems.length} items</div>;
}
Note: Zustand is lightweight (~1KB), requires no Provider, and supports TypeScript excellently. Use selectors to optimize re-renders. Combine with middleware for persistence, DevTools, and Immer integration.

14.3 Jotai Atomic State Management

Concept API Description Use Case
Atom atom(initialValue) Primitive unit of state (like React state) Small, independent pieces of state
useAtom const [value, setValue] = useAtom(atom) Read and write atom value useState-like API for atoms
useAtomValue const value = useAtomValue(atom) Read-only access to atom Subscribe to value without write capability
useSetAtom const setValue = useSetAtom(atom) Write-only access to atom Update without subscribing to value
Derived Atoms atom((get) => get(otherAtom) * 2) Compute value from other atoms Derived/computed state with automatic dependencies
Async Atoms atom(async (get) => await fetch(...)) Atoms with async read function Data fetching with Suspense integration
Write-only Atoms atom(null, (get, set, arg) => ...) Atoms for actions without state Action creators that update other atoms
atomFamily atomFamily((param) => atom(...)) Create atoms dynamically based on parameters Parameterized atoms (e.g., user atoms by ID)

Example: Basic Jotai atoms and derived state

import { atom, useAtom, useAtomValue } from 'jotai';

// Primitive atoms
const countAtom = atom(0);
const nameAtom = atom('Guest');

// Derived atom (read-only)
const doubleCountAtom = atom((get) => get(countAtom) * 2);

// Derived atom with dependencies
const greetingAtom = atom((get) => {
  const name = get(nameAtom);
  const count = get(countAtom);
  return `Hello ${name}, count is ${count}`;
});

// Write-only action atom
const incrementAtom = atom(
  null, // no read
  (get, set, amount) => {
    set(countAtom, get(countAtom) + amount);
  }
);

// Component usage
function Counter() {
  const [count, setCount] = useAtom(countAtom);
  const doubleCount = useAtomValue(doubleCountAtom);
  const increment = useSetAtom(incrementAtom);
  
  return (
    <div>
      <p>Count: {count}, Double: {doubleCount}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => increment(5)}>+5</button>
    </div>
  );
}

function Greeting() {
  const greeting = useAtomValue(greetingAtom);
  return <h1>{greeting}</h1>;
}

// Provider (optional, for scoping)
import { Provider } from 'jotai';

function App() {
  return (
    <Provider>
      <Counter />
      <Greeting />
    </Provider>
  );
}

Example: Async atoms with Suspense

import { atom, useAtomValue } from 'jotai';
import { Suspense } from 'react';

// Async atom for fetching user
const userIdAtom = atom(1);
const userAtom = atom(async (get) => {
  const userId = get(userIdAtom);
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
});

// Derived async atom
const userPostsAtom = atom(async (get) => {
  const user = await get(userAtom);
  const response = await fetch(`/api/users/${user.id}/posts`);
  return response.json();
});

function UserProfile() {
  const user = useAtomValue(userAtom); // Suspends while loading
  return <div>{user.name}</div>;
}

function UserPosts() {
  const posts = useAtomValue(userPostsAtom); // Suspends
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
      <UserPosts />
    </Suspense>
  );
}

// atomFamily for parameterized atoms
import { atomFamily } from 'jotai/utils';

const todoAtomFamily = atomFamily((id) =>
  atom(async () => {
    const res = await fetch(`/api/todos/${id}`);
    return res.json();
  })
);

function Todo({ id }) {
  const todo = useAtomValue(todoAtomFamily(id));
  return <div>{todo.title}</div>;
}
Note: Jotai provides bottom-up atomic state management. Atoms are defined globally but values are stored per Provider scope. Excellent TypeScript support, built-in Suspense integration, and minimal boilerplate.

14.4 Valtio Proxy-based State Management

Feature API Description Characteristics
proxy const state = proxy({ ... }) Create mutable proxy state object Mutate directly, auto-detects changes with Proxies
useSnapshot const snap = useSnapshot(state) Get immutable snapshot for rendering Automatic re-render on used properties
subscribe subscribe(state, callback) Listen to state changes React to mutations outside components
snapshot const snap = snapshot(state) Get snapshot outside React Read immutable version of mutable state
derive derive({ computed: (get) => ... }) Create derived/computed properties Automatically recompute when dependencies change
ref ref(value) Mark value as non-reactive Store objects that shouldn't trigger updates
Nested Proxies Automatic for objects/arrays Deep reactivity for nested structures No need to spread or copy nested objects
Render Optimization Auto-tracks property access Re-render only when accessed properties change Fine-grained reactivity without selectors

Example: Basic Valtio state with mutations

import { proxy, useSnapshot } from 'valtio';

// Create mutable state
const state = proxy({
  count: 0,
  text: 'Hello',
  nested: {
    value: 42
  },
  todos: []
});

// Mutate state directly (outside React)
function increment() {
  state.count++; // Direct mutation!
}

function addTodo(text) {
  state.todos.push({ // Array mutation
    id: Date.now(),
    text,
    completed: false
  });
}

function toggleTodo(id) {
  const todo = state.todos.find(t => t.id === id);
  if (todo) {
    todo.completed = !todo.completed; // Nested mutation
  }
}

// Component usage
function Counter() {
  const snap = useSnapshot(state);
  
  return (
    <div>
      <p>{snap.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

function TodoList() {
  const snap = useSnapshot(state);
  
  return (
    <ul>
      {snap.todos.map(todo => (
        <li key={todo.id} onClick={() => toggleTodo(todo.id)}>
          {todo.text} {todo.completed ? '✓' : ''}
        </li>
      ))}
    </ul>
  );
}

// Only re-renders when count changes (not when todos change)
function CountDisplay() {
  const snap = useSnapshot(state);
  return <div>Count: {snap.count}</div>;
}

Example: Derived state and computed properties

import { proxy, useSnapshot, derive } from 'valtio';

const state = proxy({
  items: [
    { id: 1, name: 'Apple', price: 1.2, quantity: 5 },
    { id: 2, name: 'Banana', price: 0.8, quantity: 3 }
  ],
  taxRate: 0.1
});

// Add derived/computed properties
const derived = derive({
  subtotal: (get) => {
    const items = get(state).items;
    return items.reduce((sum, item) => 
      sum + item.price * item.quantity, 0
    );
  },
  tax: (get) => {
    return get(derived).subtotal * get(state).taxRate;
  },
  total: (get) => {
    return get(derived).subtotal + get(derived).tax;
  }
});

function Cart() {
  const snap = useSnapshot(state);
  const derivedSnap = useSnapshot(derived);
  
  return (
    <div>
      {snap.items.map(item => (
        <div key={item.id}>
          {item.name}: ${item.price} × {item.quantity}
        </div>
      ))}
      <hr />
      <div>Subtotal: ${derivedSnap.subtotal.toFixed(2)}</div>
      <div>Tax: ${derivedSnap.tax.toFixed(2)}</div>
      <div>Total: ${derivedSnap.total.toFixed(2)}</div>
    </div>
  );
}

// Update quantity
function updateQuantity(id, quantity) {
  const item = state.items.find(i => i.id === id);
  if (item) {
    item.quantity = quantity;
    // derived values automatically recompute
  }
}
Note: Valtio enables mutable-style updates with immutable snapshots for React. Use for TypeScript projects needing simple, direct mutations. Automatic fine-grained reactivity without manual selectors. Consider Vue-like developer experience.

14.5 Recoil Experimental State Management

Concept API Description Status
atom atom({ key, default }) Unit of state with unique key BETA Requires unique string key
selector selector({ key, get }) Derived/computed state from atoms Pure function of atoms/selectors
useRecoilState const [value, setValue] = useRecoilState(atom) Read and write atom (like useState) Similar to React hooks API
useRecoilValue const value = useRecoilValue(atom) Read-only access to atom/selector Subscribe without write capability
useSetRecoilState const setValue = useSetRecoilState(atom) Write-only access to atom Update without subscribing
atomFamily atomFamily({ key, default: (param) => ... }) Create atoms dynamically Parameterized atoms for collections
selectorFamily selectorFamily({ key, get: (param) => ... }) Parameterized selectors Derived state with parameters
Async Selectors get: async ({ get }) => await ... Async data fetching in selectors BETA Experimental feature

Example: Recoil atoms and selectors

import { 
  atom, 
  selector, 
  useRecoilState, 
  useRecoilValue,
  RecoilRoot 
} from 'recoil';

// Atoms (must have unique keys)
const textState = atom({
  key: 'textState',
  default: ''
});

const listState = atom({
  key: 'listState',
  default: []
});

// Selector (derived state)
const charCountState = selector({
  key: 'charCountState',
  get: ({ get }) => {
    const text = get(textState);
    return text.length;
  }
});

// Filtered list selector
const filteredListState = selector({
  key: 'filteredListState',
  get: ({ get }) => {
    const list = get(listState);
    const filter = get(textState);
    return list.filter(item => 
      item.toLowerCase().includes(filter.toLowerCase())
    );
  }
});

// Component usage
function TextInput() {
  const [text, setText] = useRecoilState(textState);
  const charCount = useRecoilValue(charCountState);
  
  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <div>Character Count: {charCount}</div>
    </div>
  );
}

function FilteredList() {
  const filteredItems = useRecoilValue(filteredListState);
  
  return (
    <ul>
      {filteredItems.map((item, i) => (
        <li key={i}>{item}</li>
      ))}
    </ul>
  );
}

// App requires RecoilRoot
function App() {
  return (
    <RecoilRoot>
      <TextInput />
      <FilteredList />
    </RecoilRoot>
  );
}

Example: Async selectors and atom families

import { atom, selector, selectorFamily, atomFamily } from 'recoil';

// Async selector for current user
const currentUserQuery = selector({
  key: 'currentUserQuery',
  get: async () => {
    const response = await fetch('/api/current-user');
    return response.json();
  }
});

// Atom family for user data by ID
const userState = atomFamily({
  key: 'userState',
  default: selectorFamily({
    key: 'userState/default',
    get: (userId) => async () => {
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    }
  })
});

// Selector family for user posts
const userPostsQuery = selectorFamily({
  key: 'userPostsQuery',
  get: (userId) => async ({ get }) => {
    // Can depend on other atoms/selectors
    const user = get(userState(userId));
    const response = await fetch(`/api/users/${userId}/posts`);
    return response.json();
  }
});

// Component usage with Suspense
import { Suspense } from 'react';

function CurrentUser() {
  const user = useRecoilValue(currentUserQuery);
  return <div>{user.name}</div>;
}

function UserProfile({ userId }) {
  const user = useRecoilValue(userState(userId));
  const posts = useRecoilValue(userPostsQuery(userId));
  
  return (
    <div>
      <h1>{user.name}</h1>
      <ul>
        {posts.map(p => <li key={p.id}>{p.title}</li>)}
      </ul>
    </div>
  );
}

function App() {
  return (
    <RecoilRoot>
      <Suspense fallback={<div>Loading...</div>}>
        <CurrentUser />
        <UserProfile userId={1} />
      </Suspense>
    </RecoilRoot>
  );
}
Warning: Recoil is experimental and development has slowed. Consider Jotai as a more actively maintained alternative with similar atomic state patterns. Recoil requires unique string keys for all atoms which can be cumbersome.

14.6 Custom State Management Library Creation

Approach Implementation Complexity Features
useSyncExternalStore useSyncExternalStore(subscribe, getSnapshot) Low - React 18 built-in Sync external state with React, concurrent-safe
Context + useReducer createContext + Provider Low - Uses React primitives Simple global state, requires Provider wrapping
Observable Pattern subscribe/unsubscribe listeners Medium - Manual subscription management Pub/sub system, works outside React
Proxy-based new Proxy(target, handler) Medium - Requires Proxy knowledge Auto-tracking, mutable-style updates
Immer Integration produce(state, draft => ...) Low - Library dependency Immutable updates with mutable syntax
Middleware Support compose(middleware1, middleware2) High - Complex composition Extensibility via plugins (logging, persist, devtools)
Selector System createSelector(deps, compute) Medium - Memoization logic Derived state with dependency tracking
Time Travel history = [states], pointer High - History management Undo/redo, debugging, replay

Example: Custom store with useSyncExternalStore

import { useSyncExternalStore } from 'react';

// Create simple store
function createStore(initialState) {
  let state = initialState;
  const listeners = new Set();
  
  const getState = () => state;
  
  const setState = (newState) => {
    state = typeof newState === 'function' 
      ? newState(state) 
      : newState;
    listeners.forEach(listener => listener());
  };
  
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  
  return { getState, setState, subscribe };
}

// Create store instance
const counterStore = createStore({ count: 0 });

// Custom hook
function useCounter() {
  const state = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState
  );
  
  const increment = () => {
    counterStore.setState(s => ({ count: s.count + 1 }));
  };
  
  const decrement = () => {
    counterStore.setState(s => ({ count: s.count - 1 }));
  };
  
  return { count: state.count, increment, decrement };
}

// Component usage
function Counter() {
  const { count, increment, decrement } = useCounter();
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

Example: Store with selectors and computed values

function createStoreWithSelectors(initialState) {
  let state = initialState;
  const listeners = new Set();
  const selectorCache = new Map();
  
  const getState = () => state;
  
  const setState = (updater) => {
    const newState = typeof updater === 'function' 
      ? updater(state) 
      : updater;
    
    if (newState !== state) {
      state = newState;
      selectorCache.clear(); // Invalidate cache
      listeners.forEach(listener => listener());
    }
  };
  
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  
  // Selector with memoization
  const createSelector = (selector) => {
    return () => {
      const cacheKey = selector.toString();
      
      if (!selectorCache.has(cacheKey)) {
        selectorCache.set(cacheKey, selector(state));
      }
      
      return selectorCache.get(cacheKey);
    };
  };
  
  return { getState, setState, subscribe, createSelector };
}

// Usage
const store = createStoreWithSelectors({
  users: [
    { id: 1, name: 'Alice', age: 30 },
    { id: 2, name: 'Bob', age: 25 }
  ]
});

// Define selectors
const selectAdultUsers = store.createSelector(
  state => state.users.filter(u => u.age >= 18)
);

const selectUserCount = store.createSelector(
  state => state.users.length
);

// Custom hook with selector
function useStoreSelector(selector) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState())
  );
}

function UserList() {
  const adults = useStoreSelector(selectAdultUsers);
  const count = useStoreSelector(selectUserCount);
  
  return (
    <div>
      <p>Total users: {count}</p>
      {adults.map(u => <div key={u.id}>{u.name}</div>)}
    </div>
  );
}

Example: Store with middleware support

function createStoreWithMiddleware(initialState, middlewares = []) {
  let state = initialState;
  const listeners = new Set();
  
  // Compose middleware
  const composedMiddleware = middlewares.reduceRight(
    (next, middleware) => middleware(next),
    (newState) => {
      state = newState;
      listeners.forEach(l => l());
    }
  );
  
  const getState = () => state;
  
  const setState = (updater) => {
    const newState = typeof updater === 'function'
      ? updater(state)
      : updater;
    composedMiddleware(newState);
  };
  
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  
  return { getState, setState, subscribe };
}

// Middleware examples
const logger = (next) => (state) => {
  console.log('Previous state:', state);
  next(state);
  console.log('New state:', state);
};

const persist = (key) => (next) => (state) => {
  next(state);
  localStorage.setItem(key, JSON.stringify(state));
};

const devtools = (next) => (state) => {
  if (window.__REDUX_DEVTOOLS_EXTENSION__) {
    window.__REDUX_DEVTOOLS_EXTENSION__.send('UPDATE', state);
  }
  next(state);
};

// Create store with middleware
const store = createStoreWithMiddleware(
  { count: 0 },
  [logger, persist('app-state'), devtools]
);

// Hook
function useStore() {
  return useSyncExternalStore(
    store.subscribe,
    store.getState
  );
}
Note: Custom stores are useful for learning or specific needs. Use useSyncExternalStore (React 18+) for concurrent-safe external state. For production, prefer established libraries (Zustand, Jotai) unless you have unique requirements.

Section 14 Key Takeaways

  • Redux Toolkit - Official Redux with minimal boilerplate, Immer, and RTK Query for data fetching
  • Zustand - Lightweight (1KB), no Provider, hook-based with middleware support
  • Jotai - Atomic bottom-up state, Suspense support, minimal API similar to Recoil
  • Valtio - Proxy-based mutable updates with immutable snapshots, fine-grained reactivity
  • Recoil - Experimental atomic state from Meta, consider Jotai as alternative
  • Custom stores - Use useSyncExternalStore for concurrent-safe external state integration

15. Server State Management and Data Fetching

15.1 React Query (TanStack Query) Integration

Feature API Description Use Case
useQuery useQuery({ queryKey, queryFn }) Fetch and cache data with automatic refetching GET requests, read-only data fetching
useMutation useMutation({ mutationFn }) Perform create/update/delete operations POST/PUT/DELETE requests, data mutations
Query Keys ['users', id, filters] Unique identifier for cached queries Cache management, invalidation, dependencies
Stale Time staleTime: 5 * 60 * 1000 Duration data considered fresh Reduce unnecessary refetches, improve performance
Cache Time cacheTime: 10 * 60 * 1000 How long inactive data stays in cache Memory management, garbage collection
Refetch Strategies refetchOnWindowFocus, refetchOnMount Auto-refetch triggers Keep data fresh on user interactions
Pagination useInfiniteQuery, keepPreviousData Infinite scroll and paginated queries Lists with load more, pagination UIs
Optimistic Updates onMutate, onError, onSettled Update UI before server confirmation Instant feedback, rollback on error
Query Invalidation queryClient.invalidateQueries() Mark queries as stale, trigger refetch Update related data after mutations
Prefetching queryClient.prefetchQuery() Load data before component mounts Hover states, route transitions

Example: Complete React Query setup

import { QueryClient, QueryClientProvider, useQuery, useMutation } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

// Create query client
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      refetchOnWindowFocus: true,
      retry: 1
    }
  }
});

// App setup
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <UserList />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

// Fetch users
function UserList() {
  const { 
    data: users, 
    isLoading, 
    isError, 
    error,
    refetch 
  } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users');
      if (!response.ok) throw new Error('Failed to fetch');
      return response.json();
    }
  });
  
  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error: {error.message}</div>;
  
  return (
    <div>
      <button onClick={refetch}>Refresh</button>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
}

Example: Mutations with optimistic updates

import { useQueryClient, useMutation } from '@tanstack/react-query';

function TodoList() {
  const queryClient = useQueryClient();
  
  // Fetch todos
  const { data: todos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos
  });
  
  // Add todo mutation
  const addTodoMutation = useMutation({
    mutationFn: (newTodo) => 
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo)
      }).then(res => res.json()),
    
    // Optimistic update
    onMutate: async (newTodo) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // Snapshot previous value
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // Optimistically update
      queryClient.setQueryData(['todos'], (old) => [
        ...old,
        { ...newTodo, id: 'temp-' + Date.now() }
      ]);
      
      // Return context for rollback
      return { previousTodos };
    },
    
    // Rollback on error
    onError: (err, newTodo, context) => {
      queryClient.setQueryData(['todos'], context.previousTodos);
      toast.error('Failed to add todo');
    },
    
    // Refetch on success
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    }
  });
  
  const handleAdd = (text) => {
    addTodoMutation.mutate({ text, completed: false });
  };
  
  return (
    <div>
      <AddTodoForm onAdd={handleAdd} />
      {todos?.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </div>
  );
}

Example: Infinite scroll with useInfiniteQuery

import { useInfiniteQuery } from '@tanstack/react-query';

function InfiniteList() {
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: async ({ pageParam = 1 }) => {
      const response = await fetch(`/api/posts?page=${pageParam}`);
      return response.json();
    },
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    }
  });
  
  return (
    <div>
      {data?.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => (
            <PostCard key={post.id} post={post} />
          ))}
        </div>
      ))}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()} 
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
Note: React Query excels at server state management with automatic caching, background updates, and deduplication. Use query keys as dependencies - when they change, queries refetch. Combine with optimistic updates for instant UX.

15.2 SWR (Stale-While-Revalidate) Patterns

Feature API Description Benefits
useSWR useSWR(key, fetcher, options) Fetch data with automatic revalidation Simple API, built-in cache, auto-refetch
Key-based Cache '/api/user/' + id String/array key for caching Automatic deduplication, global cache
Revalidation revalidateOnFocus, revalidateOnReconnect Auto-refetch triggers Keep data fresh without manual intervention
mutate mutate(key, data, options) Programmatically update cache Optimistic updates, cache invalidation
useSWRMutation useSWRMutation(key, fetcher) Trigger mutations manually POST/PUT/DELETE operations
Suspense Mode suspense: true Use with React Suspense Declarative loading states
Pagination useSWRInfinite Infinite loading and pagination Load more, cursor-based pagination
Prefetch preload(key, fetcher) Load data before rendering Instant page transitions

Example: Basic SWR usage with revalidation

import useSWR from 'swr';

// Fetcher function
const fetcher = (url) => fetch(url).then(res => res.json());

function Profile() {
  const { data, error, isLoading, mutate } = useSWR(
    '/api/user',
    fetcher,
    {
      revalidateOnFocus: true,
      revalidateOnReconnect: true,
      refreshInterval: 30000 // Refresh every 30s
    }
  );
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Failed to load</div>;
  
  return (
    <div>
      <h1>{data.name}</h1>
      <button onClick={() => mutate()}>Refresh</button>
    </div>
  );
}

// Conditional fetching
function User({ id }) {
  const { data } = useSWR(
    id ? `/api/user/${id}` : null, // null = don't fetch
    fetcher
  );
  
  return data ? <div>{data.name}</div> : null;
}

// Dependent queries
function UserPosts({ userId }) {
  const { data: user } = useSWR(`/api/user/${userId}`, fetcher);
  const { data: posts } = useSWR(
    user ? `/api/user/${user.id}/posts` : null,
    fetcher
  );
  
  return posts?.map(p => <Post key={p.id} post={p} />);
}

Example: Optimistic updates with mutate

import useSWR, { useSWRConfig } from 'swr';

function TodoList() {
  const { data: todos } = useSWR('/api/todos', fetcher);
  const { mutate } = useSWRConfig();
  
  const addTodo = async (text) => {
    const newTodo = { id: Date.now(), text, completed: false };
    
    // Optimistic update - update cache immediately
    mutate(
      '/api/todos',
      [...todos, newTodo],
      false // Don't revalidate yet
    );
    
    try {
      // Send to server
      await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo)
      });
      
      // Revalidate to get server data
      mutate('/api/todos');
    } catch (error) {
      // Rollback on error
      mutate('/api/todos', todos);
      toast.error('Failed to add todo');
    }
  };
  
  const toggleTodo = async (id) => {
    // Optimistic update
    const updated = todos.map(t =>
      t.id === id ? { ...t, completed: !t.completed } : t
    );
    
    mutate('/api/todos', updated, false);
    
    try {
      await fetch(`/api/todos/${id}`, { method: 'PATCH' });
      mutate('/api/todos');
    } catch (error) {
      mutate('/api/todos', todos);
    }
  };
  
  return (
    <div>
      {todos?.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={() => toggleTodo(todo.id)} 
        />
      ))}
    </div>
  );
}

Example: Infinite scroll with useSWRInfinite

import { useSWRInfinite } from 'swr';

function PostList() {
  const getKey = (pageIndex, previousPageData) => {
    // Reached the end
    if (previousPageData && !previousPageData.hasMore) return null;
    
    // First page
    return `/api/posts?page=${pageIndex + 1}`;
  };
  
  const { 
    data, 
    size, 
    setSize, 
    isLoading,
    isValidating 
  } = useSWRInfinite(getKey, fetcher);
  
  const posts = data ? data.flatMap(page => page.posts) : [];
  const hasMore = data?.[data.length - 1]?.hasMore;
  
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
      
      {hasMore && (
        <button 
          onClick={() => setSize(size + 1)}
          disabled={isValidating}
        >
          {isValidating ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}
Note: SWR is lightweight (~5KB) with excellent TypeScript support. The "stale-while-revalidate" strategy shows cached data instantly while fetching fresh data in background. Great for Next.js apps with built-in integration.

15.3 Apollo Client GraphQL State Management

Feature API Description Use Case
useQuery useQuery(QUERY, { variables }) Execute GraphQL query and cache result Fetch data with automatic caching
useMutation useMutation(MUTATION) Execute GraphQL mutation Create/update/delete operations
useLazyQuery useLazyQuery(QUERY) Query triggered manually (not on mount) Search, on-demand fetching
Cache Policies cache-first, network-only, cache-and-network Control cache behavior Balance freshness vs performance
Normalized Cache InMemoryCache with typePolicies Automatic normalization by ID Efficient updates, deduplication
Optimistic Response optimisticResponse: { ... } Instant UI update before server response Fast feedback on mutations
refetchQueries refetchQueries: ['GetUsers'] Auto-refetch queries after mutation Keep related data in sync
Local State @client directive, reactive variables Manage local client state UI state alongside server data

Example: Apollo Client setup and queries

import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '@apollo/client';

// Create Apollo Client
const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache({
    typePolicies: {
      User: {
        keyFields: ['id']
      }
    }
  })
});

// App setup
function App() {
  return (
    <ApolloProvider client={client}>
      <UserList />
    </ApolloProvider>
  );
}

// Define query
const GET_USERS = gql`
  query GetUsers($limit: Int!) {
    users(limit: $limit) {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

// Component with query
function UserList() {
  const { loading, error, data, refetch } = useQuery(GET_USERS, {
    variables: { limit: 10 },
    fetchPolicy: 'cache-first' // Try cache first
  });
  
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  
  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      {data.users.map(user => (
        <div key={user.id}>
          {user.name} - {user.posts.length} posts
        </div>
      ))}
    </div>
  );
}

Example: Mutations with optimistic updates

import { useMutation, gql } from '@apollo/client';

const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
      text
      completed
    }
  }
`;

function TodoForm() {
  const [addTodo] = useMutation(ADD_TODO, {
    // Optimistic response
    optimisticResponse: {
      addTodo: {
        __typename: 'Todo',
        id: 'temp-id',
        text: inputValue,
        completed: false
      }
    },
    
    // Update cache manually
    update(cache, { data: { addTodo } }) {
      const existing = cache.readQuery({ query: GET_TODOS });
      cache.writeQuery({
        query: GET_TODOS,
        data: {
          todos: [...existing.todos, addTodo]
        }
      });
    },
    
    // Or refetch queries
    refetchQueries: [{ query: GET_TODOS }]
  });
  
  const handleSubmit = (text) => {
    addTodo({ variables: { text } });
  };
  
  return <form onSubmit={handleSubmit}>...</form>;
}

Example: Local state with reactive variables

import { makeVar, useReactiveVar } from '@apollo/client';

// Create reactive variable for local state
const cartItemsVar = makeVar([]);
const isLoggedInVar = makeVar(false);

// Read and write reactive variables
function ShoppingCart() {
  const cartItems = useReactiveVar(cartItemsVar);
  
  const addItem = (item) => {
    cartItemsVar([...cartItemsVar(), item]);
  };
  
  const removeItem = (id) => {
    cartItemsVar(cartItemsVar().filter(item => item.id !== id));
  };
  
  return (
    <div>
      {cartItems.map(item => (
        <CartItem 
          key={item.id} 
          item={item} 
          onRemove={() => removeItem(item.id)} 
        />
      ))}
    </div>
  );
}

// Use in queries with @client directive
const GET_CART = gql`
  query GetCart {
    cartItems @client
    isLoggedIn @client
  }
`;

function Cart() {
  const { data } = useQuery(GET_CART);
  
  return (
    <div>
      {data.isLoggedIn ? (
        <div>{data.cartItems.length} items</div>
      ) : (
        <div>Please log in</div>
      )}
    </div>
  );
}
Note: Apollo Client provides comprehensive GraphQL state management with normalized caching, optimistic updates, and local state. Automatic cache updates for many cases. Use reactive variables for UI state alongside server data.

15.4 Server State vs Client State Separation

State Type Characteristics Management Examples
Server State Persisted remotely, asynchronous, shared, can be stale React Query, SWR, Apollo Client User data, posts, products, API responses
Client State Local to browser, synchronous, not shared, always fresh useState, useReducer, Context, Zustand UI state, form inputs, modals, theme, filters
Hybrid State Derived from server, modified locally Combine both approaches Filtered/sorted server data, draft edits
Ownership Server owns server state, client owns UI state Clear boundaries prevent conflicts Don't duplicate server data in client state
Persistence Server state persists, client state ephemeral Server state survives refreshes User profile (server) vs sidebar open (client)
Sync Strategy Server state needs sync, client state doesn't Polling, websockets, invalidation Real-time updates, stale data handling
Cache Strategy Server state cached, client state in memory Different cache policies Stale-while-revalidate vs direct access
Error Handling Server state can fail, client state reliable Network errors, retries, fallbacks Loading/error states for server data

Example: Clear separation of server and client state

import { useQuery } from '@tanstack/react-query';
import { create } from 'zustand';

// SERVER STATE - managed by React Query
function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const res = await fetch('/api/users');
      return res.json();
    }
  });
}

// CLIENT STATE - managed by Zustand
const useUIStore = create((set) => ({
  sidebarOpen: false,
  theme: 'light',
  searchQuery: '',
  selectedUserId: null,
  
  toggleSidebar: () => set(s => ({ sidebarOpen: !s.sidebarOpen })),
  setTheme: (theme) => set({ theme }),
  setSearchQuery: (query) => set({ searchQuery: query }),
  selectUser: (id) => set({ selectedUserId: id })
}));

// Component combines both
function UserDashboard() {
  // Server state
  const { data: users, isLoading } = useUsers();
  
  // Client state
  const { 
    searchQuery, 
    selectedUserId, 
    setSearchQuery, 
    selectUser 
  } = useUIStore();
  
  // Derived from both (hybrid)
  const filteredUsers = users?.filter(u =>
    u.name.toLowerCase().includes(searchQuery.toLowerCase())
  ) || [];
  
  const selectedUser = users?.find(u => u.id === selectedUserId);
  
  return (
    <div>
      <input 
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="Search users..."
      />
      
      {isLoading ? (
        <Spinner />
      ) : (
        <>
          <UserList 
            users={filteredUsers} 
            onSelect={selectUser} 
          />
          
          {selectedUser && (
            <UserDetails user={selectedUser} />
          )}
        </>
      )}
    </div>
  );
}

Example: Anti-pattern - duplicating server state in client state

// ❌ ANTI-PATTERN - Don't do this
function BadExample() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true);
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data); // Duplicating server data
        setLoading(false);
      });
  }, []);
  
  // Problem: No caching, no auto-refetch, manual loading state
  return <div>...</div>;
}

// ✅ BETTER - Use server state library
function GoodExample() {
  const { data: users, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });
  
  // Automatic caching, refetching, error handling
  return <div>...</div>;
}

// ✅ CORRECT - Client state for UI, server state for data
function BestExample() {
  // Server state
  const { data: users } = useQuery(['users'], fetchUsers);
  
  // Client state
  const [selectedId, setSelectedId] = useState(null);
  const [sortOrder, setSortOrder] = useState('asc');
  
  // Derive combined state
  const sortedUsers = useMemo(() => {
    if (!users) return [];
    return [...users].sort((a, b) =>
      sortOrder === 'asc' 
        ? a.name.localeCompare(b.name)
        : b.name.localeCompare(a.name)
    );
  }, [users, sortOrder]);
  
  return <div>...</div>;
}
Warning: Don't duplicate server state in client state (useState/Redux). Use specialized libraries (React Query/SWR) for server data. Keep client state for UI concerns only. This prevents sync issues and reduces code complexity.

15.5 Cache Invalidation and State Synchronization

Strategy Implementation Use Case Trade-offs
Manual Invalidation queryClient.invalidateQueries(['key']) Invalidate after mutations Precise control, requires manual management
Auto-refetch refetchQueries: ['users'] Automatic after mutations Simple but may over-fetch
Time-based staleTime, cacheTime, refetchInterval Periodic updates Predictable but may be unnecessary
Event-based refetchOnWindowFocus, refetchOnReconnect User interaction triggers Fresh on user return, can be aggressive
WebSocket Sync socket.on('update', () => invalidate()) Real-time updates Always fresh, requires WebSocket infrastructure
Optimistic Update setQueryData + rollback on error Instant UI updates Fast UX but complex error handling
Partial Update setQueryData with selective merge Update specific fields Efficient but needs careful merging
Polling refetchInterval: 5000 Continuous sync Simple but resource-intensive

Example: Cache invalidation patterns

import { useQueryClient, useMutation } from '@tanstack/react-query';

function UserManagement() {
  const queryClient = useQueryClient();
  
  // Create user mutation
  const createUser = useMutation({
    mutationFn: (newUser) => fetch('/api/users', {
      method: 'POST',
      body: JSON.stringify(newUser)
    }).then(r => r.json()),
    
    onSuccess: () => {
      // Invalidate all user queries
      queryClient.invalidateQueries({ queryKey: ['users'] });
      
      // Or invalidate specific queries
      queryClient.invalidateQueries({ 
        queryKey: ['users', 'list'] 
      });
    }
  });
  
  // Update user with optimistic update
  const updateUser = useMutation({
    mutationFn: ({ id, updates }) => 
      fetch(`/api/users/${id}`, {
        method: 'PATCH',
        body: JSON.stringify(updates)
      }).then(r => r.json()),
    
    onMutate: async ({ id, updates }) => {
      // Cancel ongoing queries
      await queryClient.cancelQueries({ queryKey: ['users', id] });
      
      // Snapshot for rollback
      const previous = queryClient.getQueryData(['users', id]);
      
      // Optimistic update
      queryClient.setQueryData(['users', id], (old) => ({
        ...old,
        ...updates
      }));
      
      return { previous };
    },
    
    onError: (err, { id }, context) => {
      // Rollback on error
      queryClient.setQueryData(['users', id], context.previous);
    },
    
    onSettled: (data, error, { id }) => {
      // Refetch to ensure sync
      queryClient.invalidateQueries({ queryKey: ['users', id] });
    }
  });
  
  // Delete user
  const deleteUser = useMutation({
    mutationFn: (id) => fetch(`/api/users/${id}`, { 
      method: 'DELETE' 
    }),
    
    onSuccess: (data, id) => {
      // Remove from cache
      queryClient.removeQueries({ queryKey: ['users', id] });
      
      // Update list cache
      queryClient.setQueryData(['users', 'list'], (old) =>
        old.filter(u => u.id !== id)
      );
    }
  });
  
  return <div>...</div>;
}

Example: WebSocket-based real-time sync

import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';

function useRealtimeSync() {
  const queryClient = useQueryClient();
  
  useEffect(() => {
    const socket = new WebSocket('ws://api.example.com');
    
    socket.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      switch (message.type) {
        case 'USER_CREATED':
          // Invalidate user list
          queryClient.invalidateQueries({ queryKey: ['users'] });
          break;
          
        case 'USER_UPDATED':
          // Update specific user
          queryClient.setQueryData(
            ['users', message.userId],
            message.data
          );
          // Also update in list
          queryClient.setQueryData(['users', 'list'], (old) =>
            old.map(u => 
              u.id === message.userId ? message.data : u
            )
          );
          break;
          
        case 'USER_DELETED':
          // Remove from cache
          queryClient.removeQueries({ 
            queryKey: ['users', message.userId] 
          });
          queryClient.setQueryData(['users', 'list'], (old) =>
            old.filter(u => u.id !== message.userId)
          );
          break;
      }
    };
    
    return () => socket.close();
  }, [queryClient]);
}

function App() {
  useRealtimeSync();
  return <UserDashboard />;
}
Note: Cache invalidation is hard but crucial for data consistency. Use invalidateQueries after mutations, implement optimistic updates for instant feedback, and consider WebSockets for real-time apps. Always provide rollback mechanisms.

15.6 Offline State Management with Service Workers

Pattern Implementation Description Benefits
Service Worker Cache caches.match(), caches.put() Cache API responses for offline access Work without network, instant loading
Cache Strategies Cache First, Network First, Stale While Revalidate Different strategies for different resources Balance freshness vs offline availability
Background Sync registration.sync.register() Queue actions while offline, sync when online Reliable data submission despite connectivity
IndexedDB Queue store pending mutations in IndexedDB Persist failed requests locally Survive page refreshes, manual retry
Online/Offline Detection navigator.onLine, online/offline events Track network status Adapt UI based on connectivity
Conflict Resolution Last-write-wins, merge, manual Handle concurrent offline edits Data integrity when syncing
Version Vectors Track edit history per client Detect conflicts in distributed edits Sophisticated conflict detection
Optimistic Sync Update UI immediately, sync in background Assume success, handle failures gracefully Responsive offline-first UX

Example: Offline-first with React Query and service worker

import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect, useState } from 'react';

// Track online status
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);
  
  useEffect(() => {
    const handleOnline = () => setIsOnline(true);
    const handleOffline = () => setIsOnline(false);
    
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  
  return isOnline;
}

// Offline-aware query
function useOfflineQuery(key, fetcher) {
  const isOnline = useOnlineStatus();
  
  return useQuery({
    queryKey: key,
    queryFn: fetcher,
    staleTime: Infinity, // Cache forever
    cacheTime: Infinity,
    refetchOnWindowFocus: isOnline,
    refetchOnReconnect: isOnline,
    retry: isOnline ? 3 : 0 // Don't retry when offline
  });
}

// Queue mutations when offline
function useOfflineMutation(mutationFn) {
  const queryClient = useQueryClient();
  const isOnline = useOnlineStatus();
  const [queue, setQueue] = useState([]);
  
  const mutation = useMutation({
    mutationFn,
    onSuccess: () => {
      queryClient.invalidateQueries();
    },
    onError: (error, variables) => {
      if (!isOnline) {
        // Queue for later
        setQueue(q => [...q, variables]);
        saveToIndexedDB('mutation-queue', variables);
      }
    }
  });
  
  // Process queue when back online
  useEffect(() => {
    if (isOnline && queue.length > 0) {
      queue.forEach(variables => {
        mutation.mutate(variables);
      });
      setQueue([]);
      clearIndexedDB('mutation-queue');
    }
  }, [isOnline, queue]);
  
  return mutation;
}

// Component usage
function TodoApp() {
  const isOnline = useOnlineStatus();
  
  const { data: todos } = useOfflineQuery(
    ['todos'],
    async () => {
      const res = await fetch('/api/todos');
      return res.json();
    }
  );
  
  const addTodo = useOfflineMutation(
    async (newTodo) => {
      const res = await fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo)
      });
      return res.json();
    }
  );
  
  return (
    <div>
      {!isOnline && (
        <div className="offline-banner">
          You're offline. Changes will sync when back online.
        </div>
      )}
      
      <TodoList todos={todos} />
      <button onClick={() => addTodo.mutate({ text: 'New todo' })}>
        Add Todo
      </button>
    </div>
  );
}

Example: Service worker with cache strategies

// service-worker.js
const CACHE_NAME = 'app-v1';
const DYNAMIC_CACHE = 'dynamic-v1';

// Install - cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/app.js',
        '/styles.css'
      ]);
    })
  );
});

// Fetch - different strategies for different requests
self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
  
  // API requests - Network First (fresh data when online)
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(request)
        .then((response) => {
          // Cache successful responses
          const clonedResponse = response.clone();
          caches.open(DYNAMIC_CACHE).then((cache) => {
            cache.put(request, clonedResponse);
          });
          return response;
        })
        .catch(() => {
          // Fallback to cache when offline
          return caches.match(request);
        })
    );
    return;
  }
  
  // Static assets - Cache First (instant loading)
  event.respondWith(
    caches.match(request).then((cachedResponse) => {
      return cachedResponse || fetch(request).then((response) => {
        return caches.open(DYNAMIC_CACHE).then((cache) => {
          cache.put(request, response.clone());
          return response;
        });
      });
    })
  );
});

// Background Sync - retry failed requests
self.addEventListener('sync', (event) => {
  if (event.tag === 'sync-mutations') {
    event.waitUntil(
      getMutationQueue().then((mutations) => {
        return Promise.all(
          mutations.map((mutation) =>
            fetch(mutation.url, mutation.options)
              .then(() => removeMutation(mutation.id))
          )
        );
      })
    );
  }
});
Warning: Offline-first apps require careful conflict resolution. Consider using CRDTs (Conflict-free Replicated Data Types) for complex scenarios. Always show offline indicators and sync status to users. Test thoroughly with throttled/offline network conditions.

Section 15 Key Takeaways

  • React Query - Powerful server state with caching, refetching, optimistic updates, and pagination
  • SWR - Lightweight stale-while-revalidate pattern, great for Next.js, simple API
  • Apollo Client - GraphQL state management with normalized cache and local state
  • Separation - Keep server state (React Query/SWR) separate from client state (Zustand/Context)
  • Cache invalidation - Use after mutations, implement optimistic updates, WebSockets for real-time
  • Offline-first - Service workers, background sync, queue mutations, handle conflicts gracefully

16. React 18+ Concurrent Features and State

16.1 useTransition Hook for Non-blocking State Updates

Feature API Description Use Case
useTransition const [isPending, startTransition] = useTransition() Mark state updates as non-urgent transitions Keep UI responsive during heavy updates (search, filtering, tabs)
isPending boolean Indicates if transition is in progress Show loading indicators during transition
startTransition startTransition(() => setState(...)) Wrap low-priority state updates Allow urgent updates (typing) to interrupt slow updates (filtering)
Priority System Urgent vs Transition updates React prioritizes urgent updates over transitions Input responsiveness remains high during background work
Interruptible Transitions can be abandoned New urgent updates cancel ongoing transitions No stale results when user types quickly
No Suspense Needed Works without Suspense boundaries Simpler than Suspense for CPU-bound work Heavy computations, large list rendering
Batching All updates in startTransition batched Single re-render for multiple state updates Performance optimization for complex updates
Concurrent Rendering Enables React 18 concurrent features Render can be paused and resumed Smooth UI even with expensive renders

Example: Search with transitions for responsive input

import { useState, useTransition } from 'react';

function SearchList({ items }) {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState(items);
  const [isPending, startTransition] = useTransition();
  
  const handleSearch = (value) => {
    // Urgent update - input stays responsive
    setQuery(value);
    
    // Non-urgent update - can be interrupted
    startTransition(() => {
      // Heavy filtering operation
      const filtered = items.filter(item =>
        item.title.toLowerCase().includes(value.toLowerCase()) ||
        item.description.toLowerCase().includes(value.toLowerCase())
      );
      setFilteredItems(filtered);
    });
  };
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => handleSearch(e.target.value)}
        placeholder="Search..."
      />
      
      {isPending && <span>Searching...</span>}
      
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        {filteredItems.map(item => (
          <ItemCard key={item.id} item={item} />
        ))}
      </div>
    </div>
  );
}

// Without useTransition - input becomes sluggish
function SlowSearch({ items }) {
  const [query, setQuery] = useState('');
  
  // Both updates are urgent - blocks input
  const filteredItems = items.filter(item =>
    item.title.toLowerCase().includes(query.toLowerCase())
  );
  
  return <div>...</div>;
}

Example: Tab switching with smooth transitions

import { useState, useTransition } from 'react';

function TabContainer() {
  const [activeTab, setActiveTab] = useState('about');
  const [isPending, startTransition] = useTransition();
  
  const switchTab = (tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };
  
  return (
    <div>
      <div className="tabs">
        {['about', 'posts', 'contact'].map(tab => (
          <button
            key={tab}
            onClick={() => switchTab(tab)}
            className={activeTab === tab ? 'active' : ''}
            disabled={isPending}
          >
            {tab}
          </button>
        ))}
      </div>
      
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        {activeTab === 'about' && <AboutTab />}
        {activeTab === 'posts' && <PostsTab />} {/* Expensive */}
        {activeTab === 'contact' && <ContactTab />}
      </div>
    </div>
  );
}

// Complex tab that benefits from transition
function PostsTab() {
  // Expensive rendering - thousands of items
  const posts = useMemo(() => 
    generatePosts(10000), []
  );
  
  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}
Note: useTransition keeps input responsive by deprioritizing expensive updates. Use for filtering, searching, tab switching, or any CPU-intensive state changes. The isPending flag lets you show loading states or reduce opacity during transitions.

16.2 useDeferredValue for State Value Deferring

Feature API Description Difference from useTransition
useDeferredValue const deferredValue = useDeferredValue(value) Defer rendering with stale value For values you don't control (props), useTransition for state you own
Debouncing Effect Shows previous value during update Similar to debouncing but integrated with React Automatic unlike manual setTimeout debouncing
Memoization Combine with useMemo/memo Prevent re-render of expensive components Deferred value triggers re-render only when ready
No isPending No loading state flag Compare value !== deferredValue for pending useTransition provides isPending flag
Background Update Deferred update happens in background UI stays responsive during update Similar priority to startTransition
Initial Render First render uses actual value Only subsequent updates deferred No delay on initial mount
Cancellation New value cancels pending deferred update Prevents stale results Interruptible like transitions
Use Cases Expensive computations, large lists When you receive value from parent useTransition when you control the state

Example: Deferred search results

import { useState, useDeferredValue, memo } from 'react';

function SearchApp() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  
  // Check if deferred value is stale
  const isStale = query !== deferredQuery;
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      
      {/* Input updates immediately */}
      <div>Searching for: {query}</div>
      
      {/* Results update with delay */}
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <SearchResults query={deferredQuery} />
      </div>
    </div>
  );
}

// Expensive component that benefits from deferred value
const SearchResults = memo(function SearchResults({ query }) {
  const items = useMemo(() => {
    // Expensive filtering/search operation
    return hugeList.filter(item =>
      item.toLowerCase().includes(query.toLowerCase())
    );
  }, [query]);
  
  return (
    <ul>
      {items.map(item => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
});

Example: Deferred value vs regular value comparison

import { useState, useDeferredValue, memo } from 'react';

function SliderDemo() {
  const [value, setValue] = useState(0);
  const deferredValue = useDeferredValue(value);
  
  return (
    <div>
      <input
        type="range"
        min="0"
        max="100"
        value={value}
        onChange={(e) => setValue(Number(e.target.value))}
      />
      
      <div>
        <div>Current: {value}</div>
        <div>Deferred: {deferredValue}</div>
      </div>
      
      {/* Slider updates immediately */}
      <FastComponent value={value} />
      
      {/* Chart updates with delay, stays responsive */}
      <SlowChart value={deferredValue} />
    </div>
  );
}

const SlowChart = memo(function SlowChart({ value }) {
  // Simulate expensive render
  const chartData = useMemo(() => {
    const data = [];
    for (let i = 0; i < 10000; i++) {
      data.push(Math.sin(i / 100) * value);
    }
    return data;
  }, [value]);
  
  return <Canvas data={chartData} />;
});

// Without useDeferredValue - slider becomes laggy
function LaggySlider() {
  const [value, setValue] = useState(0);
  
  return (
    <div>
      <input
        type="range"
        value={value}
        onChange={(e) => setValue(Number(e.target.value))}
      />
      {/* Both update together - slider lags */}
      <SlowChart value={value} />
    </div>
  );
}
Note: useDeferredValue is ideal when you receive a frequently changing value from props. Use with memo() to prevent re-renders until deferred value updates. For state you control directly, prefer useTransition with startTransition.

16.3 Concurrent Rendering and State Consistency

Concept Description Implications Best Practices
Concurrent Rendering React can pause, resume, or abandon renders Renders may happen multiple times before commit Render functions must be pure, no side effects
Tearing Different parts showing different state snapshots Can occur with external stores in concurrent mode Use useSyncExternalStore to prevent tearing
State Snapshots Each render gets consistent state snapshot State doesn't change mid-render Rely on this for consistent derived values
Automatic Batching All state updates batched (even in async) Fewer re-renders in React 18 No manual batching needed in most cases
flushSync flushSync(() => setState(...)) Force synchronous update (opt-out batching) Use sparingly, only when absolutely needed
Strict Mode Double-invokes functions in development Catches impure render functions Keep renders pure, no mutations or side effects
Time Slicing Long renders split into chunks Browser stays responsive during heavy work Enabled automatically with transitions
Work Prioritization High priority work interrupts low priority User interactions stay responsive Mark background updates as transitions

Example: Automatic batching in React 18

import { useState } from 'react';
import { flushSync } from 'react-dom';

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  console.log('Render count:', count, 'flag:', flag);
  
  // React 18: Single re-render (batched)
  const handleClick = () => {
    setCount(c => c + 1);
    setFlag(f => !f);
    // Only ONE render, not two
  };
  
  // React 18: Even async updates are batched
  const handleAsync = async () => {
    await fetch('/api/data');
    setCount(c => c + 1);
    setFlag(f => !f);
    // Still only ONE render!
  };
  
  // React 18: Timeouts also batched
  const handleTimeout = () => {
    setTimeout(() => {
      setCount(c => c + 1);
      setFlag(f => !f);
      // Still batched!
    }, 1000);
  };
  
  // Opt-out of batching with flushSync (rarely needed)
  const handleFlushSync = () => {
    flushSync(() => {
      setCount(c => c + 1); // Render immediately
    });
    setFlag(f => !f); // Second render
    // TWO renders
  };
  
  return (
    <div>
      <p>Count: {count}, Flag: {flag ? 'true' : 'false'}</p>
      <button onClick={handleClick}>Sync Update</button>
      <button onClick={handleAsync}>Async Update</button>
      <button onClick={handleTimeout}>Timeout Update</button>
      <button onClick={handleFlushSync}>FlushSync (not batched)</button>
    </div>
  );
}

Example: Pure render functions for concurrent mode

import { useState } from 'react';

// ❌ IMPURE - Will cause issues in concurrent mode
let renderCount = 0;
function ImpureComponent() {
  renderCount++; // Side effect during render!
  const [state, setState] = useState(0);
  
  // Double counting in StrictMode/concurrent renders
  return <div>Renders: {renderCount}</div>;
}

// ✅ PURE - Safe for concurrent mode
function PureComponent() {
  const [state, setState] = useState(0);
  const [renderCount, setRenderCount] = useState(0);
  
  // Track renders in effect, not during render
  useEffect(() => {
    setRenderCount(c => c + 1);
  });
  
  return <div>Renders: {renderCount}</div>;
}

// ❌ IMPURE - Mutating external object
const cache = { data: null };
function MutatingComponent({ id }) {
  cache.data = fetchData(id); // Mutation during render!
  return <div>{cache.data}</div>;
}

// ✅ PURE - Use state or refs for caching
function NonMutatingComponent({ id }) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetchData(id).then(setData);
  }, [id]);
  
  return <div>{data}</div>;
}

// ✅ PURE - Derived values calculated during render
function DerivedComponent({ items }) {
  // Pure computation - no side effects
  const total = items.reduce((sum, item) => sum + item.price, 0);
  const average = total / items.length;
  
  return <div>Average: {average}</div>;
}
Warning: Concurrent rendering can call render functions multiple times before committing. Keep renders pure - no mutations, no side effects, no external state changes. Use useEffect for side effects, not render phase.

16.4 useSyncExternalStore for External State Integration

Feature API Description Purpose
useSyncExternalStore useSyncExternalStore(subscribe, getSnapshot) Subscribe to external stores safely Prevent tearing in concurrent mode
subscribe (callback) => unsubscribe Register listener for store changes React re-renders when store updates
getSnapshot () => currentValue Return current store value Must return same value for same store state
getServerSnapshot () => serverValue Optional server-side value SSR hydration without mismatch
Tearing Prevention Ensures consistent state across tree All components see same snapshot Critical for concurrent rendering
External Stores Redux, Zustand, custom stores, browser APIs Any non-React state source Safe integration with React 18
Selector Function getSnapshot: () => store.getState().slice Select specific state slice Optimize re-renders, only subscribe to needed data
Memoization getSnapshot must be stable or memoized Prevent infinite loops Return same reference for same value

Example: Custom store with useSyncExternalStore

import { useSyncExternalStore } from 'react';

// External store (not React state)
const createStore = (initialState) => {
  let state = initialState;
  const listeners = new Set();
  
  return {
    getState: () => state,
    setState: (newState) => {
      state = newState;
      listeners.forEach(listener => listener());
    },
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    }
  };
};

const counterStore = createStore(0);

// Hook to use external store
function useCounter() {
  const count = useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getState
  );
  
  return count;
}

// Component usage
function Counter() {
  const count = useCounter();
  
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => counterStore.setState(count + 1)}>
        Increment
      </button>
    </div>
  );
}

// Multiple components stay in sync
function AnotherCounter() {
  const count = useCounter();
  return <div>Count: {count}</div>;
}

Example: Browser API integration with selectors

import { useSyncExternalStore } from 'react';

// Online status hook
function useOnlineStatus() {
  const isOnline = useSyncExternalStore(
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    () => navigator.onLine,
    () => true // Server snapshot
  );
  
  return isOnline;
}

// Window size hook
function useWindowSize() {
  const size = useSyncExternalStore(
    (callback) => {
      window.addEventListener('resize', callback);
      return () => window.removeEventListener('resize', callback);
    },
    () => ({ 
      width: window.innerWidth, 
      height: window.innerHeight 
    }),
    () => ({ width: 0, height: 0 }) // Server
  );
  
  return size;
}

// Media query hook
function useMediaQuery(query) {
  const matches = useSyncExternalStore(
    (callback) => {
      const mediaQuery = window.matchMedia(query);
      mediaQuery.addEventListener('change', callback);
      return () => mediaQuery.removeEventListener('change', callback);
    },
    () => window.matchMedia(query).matches,
    () => false // Server
  );
  
  return matches;
}

// Component usage
function ResponsiveComponent() {
  const isOnline = useOnlineStatus();
  const { width } = useWindowSize();
  const isMobile = useMediaQuery('(max-width: 768px)');
  
  return (
    <div>
      <div>Status: {isOnline ? 'Online' : 'Offline'}</div>
      <div>Width: {width}px</div>
      <div>Mobile: {isMobile ? 'Yes' : 'No'}</div>
    </div>
  );
}

Example: Redux integration with selectors

import { useSyncExternalStore } from 'react';

// Custom hook for Redux with selector
function useSelector(selector) {
  return useSyncExternalStore(
    store.subscribe,
    () => selector(store.getState()),
    () => selector(store.getState()) // Server
  );
}

// Component usage
function UserProfile() {
  const user = useSelector(state => state.user);
  const theme = useSelector(state => state.ui.theme);
  
  return (
    <div className={theme}>
      <h1>{user.name}</h1>
    </div>
  );
}

// Memoized selector to prevent infinite loops
function TodoList() {
  const selectTodos = useMemo(
    () => (state) => state.todos.filter(t => !t.completed),
    []
  );
  
  const activeTodos = useSelector(selectTodos);
  
  return (
    <ul>
      {activeTodos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
Note: useSyncExternalStore is essential for library authors and when integrating external stores (Redux, browser APIs). It prevents tearing in concurrent mode by ensuring all components read the same snapshot. Most app developers won't use it directly.

16.5 State Prioritization and Update Scheduling

Priority Level Type Examples Behavior
Urgent/Discrete User interactions, controlled inputs Clicks, typing, hover, focus Executed immediately, highest priority
Transition Non-urgent UI updates Search results, filtering, navigation Can be interrupted, lower priority
Deferred Background updates Analytics, logging, prefetch Lowest priority, runs when idle
Lane System Internal React priority mechanism 32 priority lanes for scheduling Fine-grained control of update order
Interruption Higher priority cancels lower Typing cancels pending search update Ensures UI responsiveness
Batching Same priority updates grouped Multiple setState in handler Single re-render per batch
Starvation Prevention Low priority eventually executes Transitions complete when idle No infinite deferral
Time Slicing Split work into chunks Render 1000s of items without blocking Browser remains responsive

Example: Priority comparison in action

import { useState, useTransition, startTransition } from 'react';

function PriorityDemo() {
  const [urgentCount, setUrgentCount] = useState(0);
  const [transitionCount, setTransitionCount] = useState(0);
  const [isPending, startTransition] = useTransition();
  
  const handleClick = () => {
    // Urgent update - executes immediately
    setUrgentCount(c => c + 1);
    
    // Transition update - can be interrupted
    startTransition(() => {
      setTransitionCount(c => c + 1);
    });
    
    // If user clicks rapidly, urgent updates always go through
    // but transition updates may be skipped/batched
  };
  
  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      
      {/* Always up-to-date */}
      <div>Urgent: {urgentCount}</div>
      
      {/* May lag behind during rapid clicks */}
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        Transition: {transitionCount}
      </div>
    </div>
  );
}

// Practical example: Search with priority
function SearchWithPriority() {
  const [input, setInput] = useState('');
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (value) => {
    // Urgent: Input stays responsive
    setInput(value);
    
    // Transition: Expensive search update
    startTransition(() => {
      setQuery(value);
    });
  };
  
  return (
    <div>
      {/* Always responsive */}
      <input 
        value={input}
        onChange={(e) => handleChange(e.target.value)}
      />
      
      {/* Updates in background */}
      <ExpensiveSearchResults query={query} isPending={isPending} />
    </div>
  );
}

Example: Multiple priority levels in complex UI

import { useState, useTransition, useEffect } from 'react';

function Dashboard() {
  // Urgent state - user interactions
  const [selectedTab, setSelectedTab] = useState('overview');
  const [sidebarOpen, setSidebarOpen] = useState(true);
  
  // Transition state - heavy renders
  const [chartData, setChartData] = useState([]);
  const [tableData, setTableData] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  // Deferred/background state - analytics
  const [analytics, setAnalytics] = useState({});
  
  const switchTab = (tab) => {
    // Urgent: Tab highlights immediately
    setSelectedTab(tab);
    
    // Transition: Heavy data processing
    startTransition(() => {
      const processed = processHeavyData(tab);
      setChartData(processed.charts);
      setTableData(processed.tables);
    });
    
    // Deferred: Track analytics (lowest priority)
    setTimeout(() => {
      setAnalytics(prev => ({
        ...prev,
        [tab]: (prev[tab] || 0) + 1
      }));
    }, 0);
  };
  
  return (
    <div>
      {/* Urgent updates - always instant */}
      <Tabs 
        active={selectedTab} 
        onSelect={switchTab} 
      />
      
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>
        Toggle Sidebar
      </button>
      
      {/* Transition updates - may be delayed */}
      <div style={{ opacity: isPending ? 0.7 : 1 }}>
        <Charts data={chartData} />
        <Table data={tableData} />
      </div>
      
      {/* Background analytics - invisible to user */}
      <AnalyticsTracker data={analytics} />
    </div>
  );
}
Note: React 18's priority system ensures user interactions always feel responsive. Mark expensive updates as transitions, keep direct user feedback (clicks, typing) as urgent. The scheduler handles the rest automatically.

16.6 Streaming SSR and State Hydration

Feature Description Benefits Considerations
Streaming SSR Send HTML in chunks as components render Faster TTFB, progressive page load Requires React 18 + supporting framework
Selective Hydration Hydrate components on-demand Interactive sooner, prioritize visible content User interactions trigger hydration
Suspense SSR <Suspense> works on server Stream fallback, replace when ready Slow components don't block entire page
Progressive Hydration Hydrate critical parts first Above-fold content interactive immediately Below-fold waits until needed
State Serialization Serialize server state to HTML Client picks up where server left off Avoid hydration mismatches
Hydration Mismatch Server/client HTML differs Console warnings, potential bugs Use same data, avoid client-only code in render
useId const id = useId() Generate stable IDs for SSR Prevents mismatch from random IDs
Partial Hydration Some components remain static Save JS bundle size and hydration time Mark non-interactive content as static

Example: Streaming SSR with Suspense boundaries

import { Suspense } from 'react';

// Server-side rendering with streaming
function App() {
  return (
    <html>
      <body>
        {/* Critical content - renders first */}
        <Header />
        <Navigation />
        
        {/* Slow component - streams later */}
        <Suspense fallback={<SkeletonComments />}>
          <Comments /> {/* Fetches data on server */}
        </Suspense>
        
        {/* Another slow component */}
        <Suspense fallback={<SkeletonRecommendations />}>
          <Recommendations />
        </Suspense>
        
        <Footer />
      </body>
    </html>
  );
}

// Component with async data fetching
async function Comments() {
  const comments = await fetchComments(); // Suspends
  
  return (
    <div>
      {comments.map(c => (
        <Comment key={c.id} data={c} />
      ))}
    </div>
  );
}

// Streaming sequence:
// 1. Server sends: Header, Nav, SkeletonComments, SkeletonRecs, Footer
// 2. Page visible immediately with skeletons
// 3. Comments finish -> streamed, replace skeleton
// 4. Recommendations finish -> streamed, replace skeleton
// 5. Progressive hydration makes interactive on-demand

Example: Avoiding hydration mismatches

import { useState, useEffect, useId } from 'react';

// ❌ WRONG - Hydration mismatch
function BadComponent() {
  // Different on server vs client!
  const timestamp = Date.now();
  const random = Math.random();
  
  return (
    <div>
      <div>Time: {timestamp}</div>
      <div>Random: {random}</div>
    </div>
  );
}

// ✅ CORRECT - Same on server and client
function GoodComponent() {
  const [timestamp, setTimestamp] = useState(null);
  const [random, setRandom] = useState(null);
  
  useEffect(() => {
    // Client-only values set after hydration
    setTimestamp(Date.now());
    setRandom(Math.random());
  }, []);
  
  return (
    <div>
      {timestamp && <div>Time: {timestamp}</div>}
      {random && <div>Random: {random}</div>}
    </div>
  );
}

// ❌ WRONG - Random ID causes mismatch
function BadFormField() {
  const id = 'field-' + Math.random();
  return (
    <div>
      <label htmlFor={id}>Name:</label>
      <input id={id} />
    </div>
  );
}

// ✅ CORRECT - useId generates stable IDs
function GoodFormField() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>Name:</label>
      <input id={id} />
    </div>
  );
}

// ❌ WRONG - Conditional rendering based on client-only API
function BadResponsive() {
  const isMobile = window.innerWidth < 768; // window undefined on server!
  return isMobile ? <MobileView /> : <DesktopView />;
}

// ✅ CORRECT - Handle SSR gracefully
function GoodResponsive() {
  const [isMobile, setIsMobile] = useState(false);
  
  useEffect(() => {
    setIsMobile(window.innerWidth < 768);
    
    const handler = () => setIsMobile(window.innerWidth < 768);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  
  // Server renders desktop, hydrates correctly
  return isMobile ? <MobileView /> : <DesktopView />;
}

Example: State serialization for SSR

// Server-side: Serialize initial state
import { renderToString } from 'react-dom/server';

async function handleRequest(req, res) {
  const initialData = await fetchDataForPage(req.url);
  
  const html = renderToString(<App initialData={initialData} />);
  
  const fullHtml = `
    <!DOCTYPE html>
    <html>
      <head><title>My App</title></head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};
        </script>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `;
  
  res.send(fullHtml);
}

// Client-side: Hydrate with same data
import { hydrateRoot } from 'react-dom/client';

const initialData = window.__INITIAL_DATA__;
const root = document.getElementById('root');

hydrateRoot(root, <App initialData={initialData} />);

// Component uses serialized data
function App({ initialData }) {
  const [data, setData] = useState(initialData);
  
  // Both server and client render same initial content
  return (
    <div>
      {data.items.map(item => (
        <Item key={item.id} data={item} />
      ))}
    </div>
  );
}
Warning: Hydration mismatches cause React to discard server HTML and re-render on client (expensive). Avoid Date.now(), Math.random(), window/document in render. Use useEffect for client-only code and useId for stable IDs.

Section 16 Key Takeaways

  • useTransition - Keep input responsive during expensive updates, mark non-urgent updates as transitions
  • useDeferredValue - Defer expensive renders based on prop values, similar to debouncing
  • Concurrent rendering - Renders can pause/resume, keep renders pure, no side effects
  • useSyncExternalStore - Prevent tearing when integrating external stores in concurrent mode
  • Priority system - Urgent updates (clicks, typing) interrupt transitions (searches, filtering)
  • Streaming SSR - Progressive HTML delivery, selective hydration, avoid hydration mismatches

17. State Testing and Quality Assurance

17.1 Unit Testing State Logic with React Testing Library

Testing state logic ensures component behavior is correct across state changes and user interactions.

Testing Pattern Description Use Case
render() Render component into testing DOM Component initialization with state
screen.getByRole() Query elements by accessibility role Find buttons, inputs, headings
fireEvent.click() Simulate user click events Test state changes on interaction
userEvent.type() Simulate realistic user typing Test form input state updates
waitFor() Wait for async state updates Async operations, API calls
act() Wrap state updates in act Manual state updates in tests

Example: Testing Counter State with useState

// Counter.jsx
import { useState } from 'react';

export function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);

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

// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  test('renders with initial count', () => {
    render(<Counter initialCount={5} />);
    expect(screen.getByText('Count: 5')).toBeInTheDocument();
  });

  test('increments count when increment button clicked', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    const incrementBtn = screen.getByRole('button', { name: /increment/i });
    await user.click(incrementBtn);
    
    expect(screen.getByText('Count: 1')).toBeInTheDocument();
  });

  test('decrements count when decrement button clicked', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={5} />);
    
    const decrementBtn = screen.getByRole('button', { name: /decrement/i });
    await user.click(decrementBtn);
    
    expect(screen.getByText('Count: 4')).toBeInTheDocument();
  });

  test('resets count to zero', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={10} />);
    
    const resetBtn = screen.getByRole('button', { name: /reset/i });
    await user.click(resetBtn);
    
    expect(screen.getByText('Count: 0')).toBeInTheDocument();
  });
});

Example: Testing Async State Updates

// UserProfile.jsx
import { useState, useEffect } from 'react';

export function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>User: {user?.name}</div>;
}

// UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { UserProfile } from './UserProfile';

global.fetch = jest.fn();

describe('UserProfile', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('displays loading state initially', () => {
    fetch.mockImplementation(() => new Promise(() => {})); // Never resolves
    render(<UserProfile userId={1} />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('displays user data after successful fetch', async () => {
    const mockUser = { id: 1, name: 'John Doe' };
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser
    });

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText('User: John Doe')).toBeInTheDocument();
    });
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });

  test('displays error message on fetch failure', async () => {
    fetch.mockResolvedValueOnce({
      ok: false
    });

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/Error:/)).toBeInTheDocument();
    });
  });
});
Note: Use userEvent instead of fireEvent for more realistic user interactions. Always use async/await with userEvent methods.

17.2 Testing useReducer and Action Dispatching

Test reducer logic in isolation and verify action dispatching updates state correctly.

Testing Strategy Description Use Case
Reducer unit tests Test reducer function independently Pure logic testing without React
Action dispatching Test dispatch calls update state Component integration testing
Initial state Verify initial state setup Component initialization
Multiple actions Test action sequences Complex state transitions
Invalid actions Test unknown action types Error handling validation

Example: Testing Reducer Function in Isolation

// todoReducer.js
export const initialState = { todos: [], filter: 'all' };

export function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, { id: Date.now(), text: action.payload, completed: false }]
      };
    case 'TOGGLE_TODO':
      return {
        ...state,
        todos: state.todos.map(todo =>
          todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
        )
      };
    case 'DELETE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

// todoReducer.test.js
import { todoReducer, initialState } from './todoReducer';

describe('todoReducer', () => {
  test('returns initial state for unknown action', () => {
    const result = todoReducer(initialState, { type: 'UNKNOWN' });
    expect(result).toEqual(initialState);
  });

  test('adds todo on ADD_TODO action', () => {
    const action = { type: 'ADD_TODO', payload: 'Learn testing' };
    const result = todoReducer(initialState, action);
    
    expect(result.todos).toHaveLength(1);
    expect(result.todos[0].text).toBe('Learn testing');
    expect(result.todos[0].completed).toBe(false);
  });

  test('toggles todo completion on TOGGLE_TODO action', () => {
    const stateWithTodo = {
      todos: [{ id: 1, text: 'Test', completed: false }],
      filter: 'all'
    };
    const action = { type: 'TOGGLE_TODO', payload: 1 };
    const result = todoReducer(stateWithTodo, action);
    
    expect(result.todos[0].completed).toBe(true);
  });

  test('deletes todo on DELETE_TODO action', () => {
    const stateWithTodo = {
      todos: [
        { id: 1, text: 'First', completed: false },
        { id: 2, text: 'Second', completed: false }
      ],
      filter: 'all'
    };
    const action = { type: 'DELETE_TODO', payload: 1 };
    const result = todoReducer(stateWithTodo, action);
    
    expect(result.todos).toHaveLength(1);
    expect(result.todos[0].id).toBe(2);
  });

  test('sets filter on SET_FILTER action', () => {
    const action = { type: 'SET_FILTER', payload: 'completed' };
    const result = todoReducer(initialState, action);
    
    expect(result.filter).toBe('completed');
  });
});

Example: Testing useReducer Component Integration

// TodoList.jsx
import { useReducer } from 'react';
import { todoReducer, initialState } from './todoReducer';

export function TodoList() {
  const [state, dispatch] = useReducer(todoReducer, initialState);
  const [input, setInput] = useState('');

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

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add todo"
      />
      <button onClick={handleAdd}>Add</button>
      <ul>
        {state.todos.map(todo => (
          <li key={todo.id}>
            <span>{todo.text}</span>
            <button onClick={() => dispatch({ type: 'TOGGLE_TODO', payload: todo.id })}>
              Toggle
            </button>
            <button onClick={() => dispatch({ type: 'DELETE_TODO', payload: todo.id })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

// TodoList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';

describe('TodoList', () => {
  test('adds todo when Add button clicked', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    const input = screen.getByPlaceholderText('Add todo');
    const addBtn = screen.getByRole('button', { name: /add/i });
    
    await user.type(input, 'Learn React Testing');
    await user.click(addBtn);
    
    expect(screen.getByText('Learn React Testing')).toBeInTheDocument();
    expect(input).toHaveValue(''); // Input cleared
  });

  test('toggles todo completion', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    // Add a todo first
    await user.type(screen.getByPlaceholderText('Add todo'), 'Test todo');
    await user.click(screen.getByRole('button', { name: /add/i }));
    
    // Toggle it
    const toggleBtn = screen.getByRole('button', { name: /toggle/i });
    await user.click(toggleBtn);
    
    // Verify state changed (implementation-dependent)
  });

  test('deletes todo', async () => {
    const user = userEvent.setup();
    render(<TodoList />);
    
    // Add a todo
    await user.type(screen.getByPlaceholderText('Add todo'), 'Delete me');
    await user.click(screen.getByRole('button', { name: /add/i }));
    
    // Delete it
    const deleteBtn = screen.getByRole('button', { name: /delete/i });
    await user.click(deleteBtn);
    
    expect(screen.queryByText('Delete me')).not.toBeInTheDocument();
  });
});
Note: Test reducer functions separately from components for easier debugging and better test coverage. Pure reducer functions don't require React Testing Library.

17.3 Testing Context Providers and Consumer Components

Test context providers supply correct values and consumers react to context changes.

Testing Pattern Description Use Case
Custom wrapper Wrap components with providers Test components consuming context
Mock context values Provide test-specific values Control context state in tests
Context updates Test context value changes Verify consumer re-renders
Multiple consumers Test multiple context users Shared state consistency

Example: Testing Context Provider and Consumer

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

const ThemeContext = createContext();

export 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>
  );
}

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

// ThemeDisplay.jsx
export function ThemeDisplay() {
  const { theme, toggleTheme } = useTheme();

  return (
    <div>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

// ThemeContext.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from './ThemeContext';
import { ThemeDisplay } from './ThemeDisplay';

describe('ThemeContext', () => {
  test('provides default theme value', () => {
    render(
      <ThemeProvider>
        <ThemeDisplay />
      </ThemeProvider>
    );

    expect(screen.getByText('Current theme: light')).toBeInTheDocument();
  });

  test('toggles theme when button clicked', async () => {
    const user = userEvent.setup();
    render(
      <ThemeProvider>
        <ThemeDisplay />
      </ThemeProvider>
    );

    const toggleBtn = screen.getByRole('button', { name: /toggle theme/i });
    await user.click(toggleBtn);

    expect(screen.getByText('Current theme: dark')).toBeInTheDocument();

    await user.click(toggleBtn);
    expect(screen.getByText('Current theme: light')).toBeInTheDocument();
  });

  test('throws error when useTheme used outside provider', () => {
    // Suppress console.error for this test
    const consoleError = jest.spyOn(console, 'error').mockImplementation();

    const TestComponent = () => {
      useTheme();
      return null;
    };

    expect(() => render(<TestComponent />)).toThrow(
      'useTheme must be used within ThemeProvider'
    );

    consoleError.mockRestore();
  });

  test('multiple consumers share same context', async () => {
    const user = userEvent.setup();
    
    function MultipleDisplays() {
      return (
        <ThemeProvider>
          <ThemeDisplay />
          <ThemeDisplay />
        </ThemeProvider>
      );
    }

    render(<MultipleDisplays />);

    const displays = screen.getAllByText('Current theme: light');
    expect(displays).toHaveLength(2);

    const toggleBtn = screen.getAllByRole('button', { name: /toggle theme/i })[0];
    await user.click(toggleBtn);

    const darkDisplays = screen.getAllByText('Current theme: dark');
    expect(darkDisplays).toHaveLength(2);
  });
});

Example: Custom Render with Context Wrapper

// test-utils.jsx
import { render } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { AuthProvider } from './AuthContext';

export function renderWithProviders(
  ui,
  {
    themeValue = 'light',
    authValue = { user: null, login: jest.fn(), logout: jest.fn() },
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return (
      <ThemeProvider initialTheme={themeValue}>
        <AuthProvider initialAuth={authValue}>
          {children}
        </AuthProvider>
      </ThemeProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...renderOptions });
}

// Re-export everything from React Testing Library
export * from '@testing-library/react';

// App.test.jsx
import { screen } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import { App } from './App';

describe('App', () => {
  test('renders with default providers', () => {
    renderWithProviders(<App />);
    // Test assertions
  });

  test('renders with custom auth state', () => {
    const mockUser = { id: 1, name: 'Test User' };
    renderWithProviders(<App />, {
      authValue: { user: mockUser, login: jest.fn(), logout: jest.fn() }
    });

    expect(screen.getByText('Test User')).toBeInTheDocument();
  });
});
Note: Create custom render utilities with common providers to reduce test boilerplate and ensure consistent test setup across your test suite.

17.4 Mock State for Component Testing

Mock state management hooks and libraries to test components in isolation.

Mocking Strategy Description Use Case
jest.mock() Mock entire modules Replace state management libraries
jest.spyOn() Spy on specific functions Track hook calls, verify usage
Mock return values Control hook return values Test component with specific state
Mock implementations Provide custom hook behavior Simulate complex state scenarios

Example: Mocking useState and useEffect

// DataFetcher.jsx
import { useState, useEffect } from 'react';

export function DataFetcher({ url }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, [url]);

  if (loading) return <div>Loading...</div>;
  return <div>{data?.message}</div>;
}

// DataFetcher.test.jsx
import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { DataFetcher } from './DataFetcher';

describe('DataFetcher with mocked hooks', () => {
  test('shows loading state', () => {
    // Mock useState to return loading state
    const mockSetData = jest.fn();
    const mockSetLoading = jest.fn();
    
    jest.spyOn(React, 'useState')
      .mockReturnValueOnce([null, mockSetData])      // data state
      .mockReturnValueOnce([true, mockSetLoading]);  // loading state
    
    jest.spyOn(React, 'useEffect').mockImplementation(f => f());

    render(<DataFetcher url="/api/data" />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('shows data after loading', () => {
    // Mock useState to return loaded state
    const mockData = { message: 'Test Message' };
    
    jest.spyOn(React, 'useState')
      .mockReturnValueOnce([mockData, jest.fn()])    // data state
      .mockReturnValueOnce([false, jest.fn()]);      // loading state
    
    jest.spyOn(React, 'useEffect').mockImplementation(() => {});

    render(<DataFetcher url="/api/data" />);
    expect(screen.getByText('Test Message')).toBeInTheDocument();
  });
});

Example: Mocking Redux/Zustand Store

// ProductList.jsx
import { useStore } from './store';

export function ProductList() {
  const products = useStore(state => state.products);
  const addToCart = useStore(state => state.addToCart);

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name}
          <button onClick={() => addToCart(product)}>Add to Cart</button>
        </li>
      ))}
    </ul>
  );
}

// ProductList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ProductList } from './ProductList';
import * as store from './store';

jest.mock('./store');

describe('ProductList', () => {
  test('renders products from store', () => {
    const mockProducts = [
      { id: 1, name: 'Product 1' },
      { id: 2, name: 'Product 2' }
    ];

    store.useStore.mockImplementation(selector => 
      selector({ products: mockProducts, addToCart: jest.fn() })
    );

    render(<ProductList />);

    expect(screen.getByText('Product 1')).toBeInTheDocument();
    expect(screen.getByText('Product 2')).toBeInTheDocument();
  });

  test('calls addToCart when button clicked', async () => {
    const user = userEvent.setup();
    const mockAddToCart = jest.fn();
    const mockProducts = [{ id: 1, name: 'Product 1' }];

    store.useStore.mockImplementation(selector => 
      selector({ products: mockProducts, addToCart: mockAddToCart })
    );

    render(<ProductList />);

    const addBtn = screen.getByRole('button', { name: /add to cart/i });
    await user.click(addBtn);

    expect(mockAddToCart).toHaveBeenCalledWith(mockProducts[0]);
  });
});
Warning: Mocking React hooks can lead to brittle tests. Prefer integration testing with real hooks when possible. Use mocking primarily for external dependencies or complex state scenarios.

17.5 Integration Testing with State Management Libraries

Test complete state management flows including store initialization, actions, and selectors.

Library Testing Approach Key Considerations
Redux Test store, reducers, actions separately Use real store in tests, mock API calls
Zustand Create test store instances Reset store between tests
Jotai Use Provider in tests Isolated atom state per test
React Query Wrap with QueryClientProvider Clear cache between tests

Example: Redux Integration Testing

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

export const createStore = (preloadedState) => {
  return configureStore({
    reducer: { counter: counterReducer },
    preloadedState
  });
};

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

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

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

// Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { createStore } from './store';
import { Counter } from './Counter';

describe('Counter with Redux', () => {
  let store;

  beforeEach(() => {
    store = createStore();
  });

  const renderWithStore = (component) => {
    return render(<Provider store={store}>{component}</Provider>);
  };

  test('increments counter value', async () => {
    const user = userEvent.setup();
    renderWithStore(<Counter />);

    const incrementBtn = screen.getByRole('button', { name: /increment/i });
    await user.click(incrementBtn);

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });

  test('uses preloaded state', () => {
    store = createStore({ counter: { value: 10 } });
    renderWithStore(<Counter />);

    expect(screen.getByText(/count: 10/i)).toBeInTheDocument();
  });

  test('multiple actions update state correctly', async () => {
    const user = userEvent.setup();
    renderWithStore(<Counter />);

    await user.click(screen.getByRole('button', { name: /increment/i }));
    await user.click(screen.getByRole('button', { name: /increment/i }));
    await user.click(screen.getByRole('button', { name: /decrement/i }));

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
});

Example: React Query Integration Testing

// UserList.jsx
import { useQuery } from '@tanstack/react-query';

function fetchUsers() {
  return fetch('/api/users').then(res => res.json());
}

export function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// UserList.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { UserList } from './UserList';

global.fetch = jest.fn();

describe('UserList with React Query', () => {
  let queryClient;

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          retry: false, // Disable retries for tests
        },
      },
    });
    jest.clearAllMocks();
  });

  const renderWithClient = (component) => {
    return render(
      <QueryClientProvider client={queryClient}>
        {component}
      </QueryClientProvider>
    );
  };

  test('displays loading state', () => {
    fetch.mockImplementation(() => new Promise(() => {}));
    renderWithClient(<UserList />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  test('displays users after successful fetch', async () => {
    const mockUsers = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ];

    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => mockUsers
    });

    renderWithClient(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Alice')).toBeInTheDocument();
      expect(screen.getByText('Bob')).toBeInTheDocument();
    });
  });

  test('displays error message on fetch failure', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'));

    renderWithClient(<UserList />);

    await waitFor(() => {
      expect(screen.getByText(/Error: Network error/i)).toBeInTheDocument();
    });
  });
});
Note: Create fresh store/client instances for each test to ensure test isolation. Configure libraries to disable retries and caching behaviors that might cause flaky tests.

17.6 End-to-End State Testing with Cypress/Playwright

Test complete user workflows including state persistence, navigation, and multi-page state management.

E2E Pattern Description Use Case
User flows Test complete user journeys Login, checkout, multi-step forms
State persistence Verify localStorage/sessionStorage Shopping cart, user preferences
Navigation state Test state across page transitions Wizard forms, checkout flows
API interactions Test real/mocked API calls Data fetching, mutations
Visual regression Test UI based on state changes Theme switching, conditional rendering

Example: Cypress E2E State Testing

// cypress/e2e/shopping-cart.cy.js
describe('Shopping Cart State Management', () => {
  beforeEach(() => {
    cy.visit('/products');
    cy.clearLocalStorage();
  });

  it('adds products to cart and persists state', () => {
    // Add first product
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Verify cart count updated
    cy.get('[data-testid="cart-count"]').should('contain', '1');

    // Add second product
    cy.get('[data-testid="product-2"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    cy.get('[data-testid="cart-count"]').should('contain', '2');

    // Verify localStorage persisted cart state
    cy.window().then(win => {
      const cartState = JSON.parse(win.localStorage.getItem('cart'));
      expect(cartState.items).to.have.length(2);
    });

    // Navigate to cart page
    cy.get('[data-testid="view-cart"]').click();
    cy.url().should('include', '/cart');

    // Verify cart items displayed
    cy.get('[data-testid="cart-item"]').should('have.length', 2);
  });

  it('persists cart across page refresh', () => {
    // Add product to cart
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Reload page
    cy.reload();

    // Verify cart persisted
    cy.get('[data-testid="cart-count"]').should('contain', '1');
  });

  it('clears cart on logout', () => {
    // Setup: Login and add items
    cy.login('test@example.com', 'password');
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Logout
    cy.get('[data-testid="logout"]').click();

    // Verify cart cleared
    cy.get('[data-testid="cart-count"]').should('contain', '0');
    cy.window().then(win => {
      expect(win.localStorage.getItem('cart')).to.be.null;
    });
  });

  it('syncs cart across multiple tabs', () => {
    // Add to cart in first tab
    cy.get('[data-testid="product-1"]').within(() => {
      cy.contains('Add to Cart').click();
    });

    // Open new tab (simulate with new visit)
    cy.visit('/products');

    // Verify cart synced
    cy.get('[data-testid="cart-count"]').should('contain', '1');
  });
});

Example: Playwright E2E State Testing

// tests/auth-state.spec.js
import { test, expect } from '@playwright/test';

test.describe('Authentication State Flow', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('maintains auth state after login', async ({ page }) => {
    // Fill login form
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');

    // Wait for redirect
    await page.waitForURL('/dashboard');

    // Verify auth state in UI
    await expect(page.locator('[data-testid="user-name"]')).toHaveText('Test User');

    // Verify auth token in localStorage
    const token = await page.evaluate(() => localStorage.getItem('authToken'));
    expect(token).toBeTruthy();

    // Navigate to different page
    await page.goto('/profile');

    // Verify still authenticated
    await expect(page.locator('[data-testid="user-name"]')).toHaveText('Test User');
  });

  test('redirects to login when accessing protected route without auth', async ({ page }) => {
    await page.goto('/dashboard');
    await page.waitForURL('/login');
    await expect(page.locator('h1')).toHaveText('Login');
  });

  test('clears auth state on logout', async ({ page, context }) => {
    // Login first
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('/dashboard');

    // Logout
    await page.click('[data-testid="logout-button"]');
    await page.waitForURL('/login');

    // Verify auth state cleared
    const token = await page.evaluate(() => localStorage.getItem('authToken'));
    expect(token).toBeNull();

    // Verify redirected when trying to access protected route
    await page.goto('/dashboard');
    await page.waitForURL('/login');
  });

  test('handles concurrent auth state in multiple tabs', async ({ context }) => {
    // Create two pages (tabs)
    const page1 = await context.newPage();
    const page2 = await context.newPage();

    // Login in first tab
    await page1.goto('/login');
    await page1.fill('[data-testid="email-input"]', 'test@example.com');
    await page1.fill('[data-testid="password-input"]', 'password123');
    await page1.click('[data-testid="login-button"]');
    await page1.waitForURL('/dashboard');

    // Navigate in second tab
    await page2.goto('/dashboard');

    // Verify authenticated in both tabs
    await expect(page1.locator('[data-testid="user-name"]')).toHaveText('Test User');
    await expect(page2.locator('[data-testid="user-name"]')).toHaveText('Test User');

    // Logout in first tab
    await page1.click('[data-testid="logout-button"]');

    // Verify second tab also logged out (if cross-tab sync implemented)
    await page2.reload();
    await page2.waitForURL('/login');
  });
});
Note: E2E tests are slower and more expensive than unit tests. Focus on critical user flows and state persistence scenarios. Use API mocking strategically to improve test speed and reliability.

Section 17 Key Takeaways

  • React Testing Library - Test state logic through user interactions, use userEvent for realistic testing
  • Reducer testing - Test reducer functions in isolation as pure functions, then integration test with components
  • Context testing - Create custom render utilities with providers, test error handling when context missing
  • State mocking - Mock external state libraries, prefer real hooks when possible to avoid brittle tests
  • Integration testing - Use real store instances in tests, clear state between tests for isolation
  • E2E testing - Test complete user flows including state persistence, navigation, and cross-tab synchronization

18. State Debugging and Development Tools

18.1 React DevTools for State Inspection

React DevTools provides powerful state inspection, component hierarchy visualization, and performance profiling capabilities.

DevTools Feature Description Use Case
Components Tab Inspect component tree and props/state View current component state values
State Editing Modify state values in real-time Test component behavior with different states
Hooks Inspection View all hooks and their values Debug useState, useReducer, useContext
Profiler Tab Record and analyze render performance Identify unnecessary re-renders
Component Highlighting Visual feedback for component updates See which components re-render
Search & Filter Find components by name or type Navigate large component trees

Example: Inspecting Component State with DevTools

// TodoApp.jsx
import { useState, useReducer } from 'react';

function todoReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'TOGGLE':
      return state.map(t => 
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    default:
      return state;
  }
}

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

  // In React DevTools Components tab, you can:
  // 1. Select TodoApp component
  // 2. View hooks section showing:
  //    - State: input (string)
  //    - Reducer: todos (array)
  //    - State: filter (string)
  // 3. Edit values directly to test different scenarios
  // 4. See props passed to child components

  return (
    <div>
      <input 
        value={input} 
        onChange={(e) => setInput(e.target.value)}
        placeholder="Add todo"
      />
      <button onClick={() => {
        dispatch({ type: 'ADD', text: input });
        setInput('');
      }}>Add</button>
      
      <select value={filter} onChange={(e) => setFilter(e.target.value)}>
        <option value="all">All</option>
        <option value="active">Active</option>
        <option value="completed">Completed</option>
      </select>

      <ul>
        {todos
          .filter(t => {
            if (filter === 'active') return !t.done;
            if (filter === 'completed') return t.done;
            return true;
          })
          .map(todo => (
            <li key={todo.id}>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => dispatch({ type: 'TOGGLE', id: todo.id })}
              />
              {todo.text}
            </li>
          ))}
      </ul>
    </div>
  );
}

Example: Using DevTools Component Displayname for Better Debugging

// Add display names to components for better DevTools experience
import { useState, memo } from 'react';

// Functional component with display name
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  // ... state logic
  return <div>{user?.name}</div>;
}
UserProfile.displayName = 'UserProfile';

// Memoized component with display name
const MemoizedItem = memo(function Item({ item }) {
  return <div>{item.name}</div>;
});
MemoizedItem.displayName = 'MemoizedItem';

// Custom hook - shows in DevTools hooks section
function useDebugValue(value, formatFn) {
  // useDebugValue shows custom label in DevTools
  useDebugValue(value, v => `User: ${v?.name || 'None'}`);
  return value;
}

// Higher-order component with proper naming
function withLoading(Component) {
  function WithLoading(props) {
    const [loading, setLoading] = useState(true);
    // ... loading logic
    return loading ? <div>Loading...</div> : <Component {...props} />;
  }
  
  WithLoading.displayName = `WithLoading(${Component.displayName || Component.name})`;
  return WithLoading;
}

// Usage in DevTools:
// - Components show meaningful names instead of "Anonymous"
// - Custom hooks display formatted debug values
// - HOCs show wrapped component name for easier debugging
Note: Enable "Highlight updates when components render" in DevTools settings to visually see which components re-render on state changes. This helps identify performance issues.

18.2 Redux DevTools Extension Integration

Redux DevTools Extension provides time-travel debugging, action history, and state diff visualization for Redux applications.

Feature Description Use Case
Action History View all dispatched actions chronologically Track state changes over time
State Diff See state changes for each action Understand action impact
Time Travel Jump to any previous state Debug specific state scenarios
Action Replay Replay actions from history Reproduce bugs
State Export/Import Save and load application state Share bug reproductions
Action Filtering Show/hide specific action types Focus on relevant actions

Example: Redux Toolkit with DevTools Integration

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
import userReducer from './userSlice';

// Redux Toolkit automatically enables Redux DevTools in development
export const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
  },
  // DevTools configuration (optional)
  devTools: process.env.NODE_ENV !== 'production' ? {
    name: 'My App',
    trace: true, // Enable action stack traces
    traceLimit: 25,
    features: {
      pause: true, // Pause recording
      lock: true, // Lock/unlock state changes
      persist: true, // Persist state in localStorage
      export: true, // Export state
      import: 'custom', // Import state
      jump: true, // Jump to action
      skip: true, // Skip actions
      reorder: true, // Reorder actions
      dispatch: true, // Dispatch custom actions
      test: true // Generate tests
    }
  } : false
});

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0, history: [] },
  reducers: {
    increment: (state) => {
      state.value += 1;
      // DevTools will show:
      // Action: counter/increment
      // Diff: +value: 1
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
      // DevTools shows payload value in action details
    },
    // Add action metadata for better DevTools display
    reset: {
      reducer: (state) => {
        state.value = 0;
        state.history = [];
      },
      prepare: () => ({
        payload: undefined,
        meta: { timestamp: Date.now(), reason: 'user_reset' }
      })
    }
  }
});

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

Example: Custom Redux DevTools Integration for Non-Redux State

// Custom hook with Redux DevTools integration
import { useReducer, useEffect, useRef } from 'react';

function useReducerWithDevTools(reducer, initialState, name = 'State') {
  const [state, dispatch] = useReducer(reducer, initialState);
  const devTools = useRef(null);

  useEffect(() => {
    // Connect to Redux DevTools Extension
    if (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
      devTools.current = window.__REDUX_DEVTOOLS_EXTENSION__.connect({
        name,
        features: { jump: true, skip: true }
      });

      // Send initial state
      devTools.current.init(initialState);
    }

    return () => {
      if (devTools.current) {
        devTools.current.unsubscribe();
      }
    };
  }, [name, initialState]);

  // Wrapper dispatch that notifies DevTools
  const dispatchWithDevTools = (action) => {
    dispatch(action);
    
    if (devTools.current) {
      // Send action to DevTools
      devTools.current.send(action, reducer(state, action));
    }
  };

  // Subscribe to DevTools messages (time travel)
  useEffect(() => {
    if (!devTools.current) return;

    const unsubscribe = devTools.current.subscribe((message) => {
      if (message.type === 'DISPATCH' && message.state) {
        // Handle time-travel: update state from DevTools
        const newState = JSON.parse(message.state);
        // Note: This requires modifying the reducer to accept a SET_STATE action
        dispatch({ type: '@@DEVTOOLS/SET_STATE', payload: newState });
      }
    });

    return unsubscribe;
  }, []);

  return [state, dispatchWithDevTools];
}

// Usage
function TodoApp() {
  const [state, dispatch] = useReducerWithDevTools(
    todoReducer,
    { todos: [], filter: 'all' },
    'TodoApp'
  );

  // Now your useReducer state appears in Redux DevTools!
  // You can time-travel, see action history, export state, etc.

  return (
    <div>
      <button onClick={() => dispatch({ type: 'ADD_TODO', text: 'Learn DevTools' })}>
        Add Todo
      </button>
      {/* ... */}
    </div>
  );
}
Note: Redux DevTools can integrate with any state management solution, not just Redux. Use the __REDUX_DEVTOOLS_EXTENSION__ API to connect custom state managers.

18.3 State Logging and Debug Patterns

Implement structured logging patterns to track state changes and debug issues in development and production.

Logging Pattern Description Use Case
Console logging Log state changes to browser console Development debugging
Middleware logging Redux middleware for action logging Track all state mutations
State snapshots Capture state at specific points Compare state before/after
Conditional logging Log only in specific conditions Filter noise, focus on issues
Performance logging Log render times and counts Identify performance bottlenecks

Example: Custom useDebugState Hook with Logging

// useDebugState.js
import { useState, useEffect, useRef } from 'react';

export function useDebugState(initialValue, name = 'State') {
  const [state, setState] = useState(initialValue);
  const previousState = useRef(initialValue);
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current += 1;
  });

  const setStateWithDebug = (newValue) => {
    const valueToSet = typeof newValue === 'function' 
      ? newValue(state) 
      : newValue;

    console.group(`🔵 ${name} State Update`);
    console.log('Previous:', previousState.current);
    console.log('New:', valueToSet);
    console.log('Render Count:', renderCount.current);
    console.log('Timestamp:', new Date().toISOString());
    console.trace('Update triggered from:');
    console.groupEnd();

    previousState.current = state;
    setState(valueToSet);
  };

  // Log on every render in development
  if (process.env.NODE_ENV === 'development') {
    console.log(`🔄 ${name} rendered:`, state, `(${renderCount.current} times)`);
  }

  return [state, setStateWithDebug];
}

// Usage
function Counter() {
  const [count, setCount] = useDebugState(0, 'Counter');
  const [step, setStep] = useDebugState(1, 'Step');

  return (
    <div>
      <p>Count: {count}</p>
      <p>Step: {step}</p>
      <button onClick={() => setCount(c => c + step)}>
        Increment by {step}
      </button>
      <button onClick={() => setStep(s => s + 1)}>
        Increase Step
      </button>
    </div>
  );
  
  // Console output when clicking Increment:
  // 🔄 Counter rendered: 0 (1 times)
  // 🔄 Step rendered: 1 (1 times)
  // 🔵 Counter State Update
  //   Previous: 0
  //   New: 1
  //   Render Count: 1
  //   Timestamp: 2025-12-20T10:30:45.123Z
  //   Update triggered from: [stack trace]
}

Example: Redux Logger Middleware

// Custom logger middleware
const loggerMiddleware = (store) => (next) => (action) => {
  const prevState = store.getState();
  const startTime = performance.now();

  console.group(`⚡ Action: ${action.type}`);
  console.log('Payload:', action.payload);
  console.log('Previous State:', prevState);
  
  // Call the next middleware or reducer
  const result = next(action);
  
  const nextState = store.getState();
  const endTime = performance.now();
  
  console.log('Next State:', nextState);
  console.log('State Diff:', getStateDiff(prevState, nextState));
  console.log(`⏱️ Time: ${(endTime - startTime).toFixed(2)}ms`);
  console.groupEnd();
  
  return result;
};

// Helper to show state differences
function getStateDiff(prev, next) {
  const diff = {};
  Object.keys(next).forEach(key => {
    if (prev[key] !== next[key]) {
      diff[key] = { from: prev[key], to: next[key] };
    }
  });
  return diff;
}

// Redux Toolkit setup with logger
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger'; // Or use custom logger above

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(
      process.env.NODE_ENV === 'development' ? logger : []
    )
});

// Conditional logging based on action type
const selectiveLogger = (store) => (next) => (action) => {
  // Only log specific actions
  const actionsToLog = ['user/login', 'cart/addItem', 'order/submit'];
  
  if (actionsToLog.some(type => action.type.includes(type))) {
    console.log('🎯 Important Action:', action);
    console.log('Current State:', store.getState());
  }
  
  return next(action);
};

Example: Production-Safe State Logging

// logger.js - Production-safe logging utility
class StateLogger {
  constructor(options = {}) {
    this.enabled = options.enabled ?? process.env.NODE_ENV === 'development';
    this.maxLogs = options.maxLogs ?? 100;
    this.logs = [];
    this.excludeActions = options.excludeActions ?? [];
  }

  log(type, data) {
    if (!this.enabled) return;

    const logEntry = {
      type,
      data,
      timestamp: Date.now(),
      url: window.location.href
    };

    this.logs.push(logEntry);

    // Keep only recent logs
    if (this.logs.length > this.maxLogs) {
      this.logs.shift();
    }

    // Console output in development only
    if (process.env.NODE_ENV === 'development') {
      console.log(`[${type}]`, data);
    }
  }

  logStateChange(actionType, prevState, nextState) {
    if (this.excludeActions.includes(actionType)) return;

    this.log('STATE_CHANGE', {
      action: actionType,
      before: this.sanitizeState(prevState),
      after: this.sanitizeState(nextState)
    });
  }

  // Remove sensitive data before logging
  sanitizeState(state) {
    const sanitized = { ...state };
    const sensitiveKeys = ['password', 'token', 'creditCard', 'ssn'];
    
    Object.keys(sanitized).forEach(key => {
      if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
        sanitized[key] = '[REDACTED]';
      }
    });
    
    return sanitized;
  }

  // Export logs for bug reports
  exportLogs() {
    return JSON.stringify(this.logs, null, 2);
  }

  // Download logs as file
  downloadLogs() {
    const blob = new Blob([this.exportLogs()], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `state-logs-${Date.now()}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }

  // Send logs to error tracking service
  async sendToErrorTracking(error) {
    if (process.env.NODE_ENV === 'production') {
      // Send to Sentry, LogRocket, etc.
      await fetch('/api/error-logs', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          error: error.message,
          stack: error.stack,
          stateLogs: this.logs
        })
      });
    }
  }
}

export const stateLogger = new StateLogger({
  enabled: true,
  excludeActions: ['@@INIT', 'persist/REHYDRATE']
});
Warning: Never log sensitive data (passwords, tokens, PII) in production. Always sanitize state before logging and disable verbose logging in production builds.

18.4 Time-travel Debugging for State Changes

Implement time-travel debugging to replay state changes and jump to any previous application state.

Technique Description Use Case
State history stack Store all state snapshots Navigate through past states
Action replay Re-execute actions from history Reproduce specific scenarios
State restoration Jump to any historical state Debug specific state configurations
Undo/Redo Navigate back and forward in time User-facing time travel features

Example: Custom Time-Travel Hook

// useTimeTravel.js
import { useReducer, useCallback, useRef } from 'react';

function timeTravelReducer(history, action) {
  const { past, present, future } = history;

  switch (action.type) {
    case 'UNDO':
      if (past.length === 0) return history;
      return {
        past: past.slice(0, past.length - 1),
        present: past[past.length - 1],
        future: [present, ...future]
      };

    case 'REDO':
      if (future.length === 0) return history;
      return {
        past: [...past, present],
        present: future[0],
        future: future.slice(1)
      };

    case 'SET':
      if (action.payload === present) return history;
      return {
        past: [...past, present],
        present: action.payload,
        future: []
      };

    case 'RESET':
      return {
        past: [],
        present: action.payload,
        future: []
      };

    case 'JUMP_TO':
      // Jump to specific index in history
      const index = action.payload;
      const allStates = [...past, present, ...future];
      if (index < 0 || index >= allStates.length) return history;
      
      return {
        past: allStates.slice(0, index),
        present: allStates[index],
        future: allStates.slice(index + 1)
      };

    default:
      return history;
  }
}

export function useTimeTravel(initialState, maxHistory = 50) {
  const [history, dispatch] = useReducer(timeTravelReducer, {
    past: [],
    present: initialState,
    future: []
  });

  const { past, present, future } = history;

  const setState = useCallback((newState) => {
    dispatch({ type: 'SET', payload: newState });
  }, []);

  const undo = useCallback(() => {
    dispatch({ type: 'UNDO' });
  }, []);

  const redo = useCallback(() => {
    dispatch({ type: 'REDO' });
  }, []);

  const reset = useCallback((state = initialState) => {
    dispatch({ type: 'RESET', payload: state });
  }, [initialState]);

  const jumpTo = useCallback((index) => {
    dispatch({ type: 'JUMP_TO', payload: index });
  }, []);

  const canUndo = past.length > 0;
  const canRedo = future.length > 0;
  
  // Get all history for visualization
  const allHistory = [...past, present, ...future];
  const currentIndex = past.length;

  return {
    state: present,
    setState,
    undo,
    redo,
    reset,
    jumpTo,
    canUndo,
    canRedo,
    history: allHistory,
    currentIndex
  };
}

// Usage
function DrawingApp() {
  const {
    state: canvas,
    setState: setCanvas,
    undo,
    redo,
    canUndo,
    canRedo,
    history,
    currentIndex,
    jumpTo
  } = useTimeTravel({ shapes: [] });

  const addShape = (shape) => {
    setCanvas({ shapes: [...canvas.shapes, shape] });
  };

  return (
    <div>
      <div>
        <button onClick={undo} disabled={!canUndo}>
          ⬅️ Undo
        </button>
        <button onClick={redo} disabled={!canRedo}>
          ➡️ Redo
        </button>
      </div>

      {/* Timeline visualization */}
      <div style={{ display: 'flex', gap: '4px' }}>
        {history.map((state, index) => (
          <button
            key={index}
            onClick={() => jumpTo(index)}
            style={{
              background: index === currentIndex ? 'blue' : 'gray',
              width: '20px',
              height: '20px'
            }}
            title={`State ${index}: ${state.shapes.length} shapes`}
          />
        ))}
      </div>

      <Canvas shapes={canvas.shapes} onAddShape={addShape} />
    </div>
  );
}

Example: Redux DevTools Time-Travel Integration

// Using Redux DevTools for automatic time-travel
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

export const store = configureStore({
  reducer: rootReducer,
  devTools: {
    name: 'My App',
    features: {
      jump: true, // Enable jumping to any action
      skip: true, // Skip (ignore) actions
      reorder: true, // Reorder actions
      pause: true // Pause action recording
    }
  }
});

// In your component, Redux DevTools provides:
// 1. Timeline slider to scrub through states
// 2. Jump to any previous action
// 3. Skip/ignore specific actions
// 4. Replay action sequences

// You can also access DevTools programmatically:
function DebugPanel() {
  const exportState = () => {
    const state = store.getState();
    const blob = new Blob([JSON.stringify(state, null, 2)], {
      type: 'application/json'
    });
    // Download state snapshot
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `app-state-${Date.now()}.json`;
    a.click();
  };

  const importState = (stateJson) => {
    const state = JSON.parse(stateJson);
    // Restore state through DevTools
    window.__REDUX_DEVTOOLS_EXTENSION__?.send(
      { type: '@@IMPORT_STATE' },
      state
    );
  };

  return (
    <div>
      <button onClick={exportState}>Export Current State</button>
      <input
        type="file"
        accept=".json"
        onChange={(e) => {
          const file = e.target.files[0];
          file.text().then(importState);
        }}
      />
    </div>
  );
}
Note: Time-travel debugging works best with immutable state updates. Ensure all state changes create new objects rather than mutating existing ones for accurate history tracking.

18.5 Performance Profiling for State Updates

Profile and measure state update performance to identify bottlenecks and optimize re-renders.

Profiling Tool Description Use Case
React Profiler Record component render times Identify slow components
Performance API Measure precise timing with marks Custom performance tracking
Why Did You Render Library to detect unnecessary renders Find optimization opportunities
Chrome DevTools Timeline recording and flame graphs Analyze render performance

Example: React Profiler Component for State Updates

// ProfiledComponent.jsx
import { Profiler } from 'react';

function onRenderCallback(
  id, // The "id" prop of the Profiler tree
  phase, // "mount" or "update"
  actualDuration, // Time spent rendering
  baseDuration, // Estimated time without memoization
  startTime, // When React began rendering
  commitTime, // When React committed the update
  interactions // Set of interactions for this update
) {
  console.log({
    id,
    phase,
    actualDuration,
    baseDuration,
    renderTime: commitTime - startTime,
    interactions
  });

  // Send to analytics in production
  if (actualDuration > 16) { // Slower than 60fps
    console.warn(`⚠️ Slow render in ${id}: ${actualDuration}ms`);
    // Send to monitoring service
    sendToAnalytics({
      type: 'slow_render',
      component: id,
      duration: actualDuration,
      phase
    });
  }
}

export function ProfiledApp() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <App />
    </Profiler>
  );
}

// Profile specific components with state
function TodoList() {
  const [todos, setTodos] = useState([]);

  return (
    <Profiler id="TodoList" onRender={onRenderCallback}>
      <div>
        {todos.map(todo => (
          <Profiler key={todo.id} id={`Todo-${todo.id}`} onRender={onRenderCallback}>
            <TodoItem todo={todo} />
          </Profiler>
        ))}
      </div>
    </Profiler>
  );
}

Example: Custom Performance Monitoring Hook

// usePerformanceMonitor.js
import { useEffect, useRef } from 'react';

export function usePerformanceMonitor(componentName, dependencies = []) {
  const renderCount = useRef(0);
  const renderTimes = useRef([]);
  const lastRenderTime = useRef(performance.now());

  useEffect(() => {
    renderCount.current += 1;
    const now = performance.now();
    const renderTime = now - lastRenderTime.current;
    renderTimes.current.push(renderTime);

    // Keep only last 100 render times
    if (renderTimes.current.length > 100) {
      renderTimes.current.shift();
    }

    lastRenderTime.current = now;

    // Calculate statistics
    const avgRenderTime =
      renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length;
    const maxRenderTime = Math.max(...renderTimes.current);

    console.log(`📊 ${componentName} Performance`, {
      renderCount: renderCount.current,
      lastRenderTime: renderTime.toFixed(2) + 'ms',
      avgRenderTime: avgRenderTime.toFixed(2) + 'ms',
      maxRenderTime: maxRenderTime.toFixed(2) + 'ms',
      dependencies
    });
  });

  return {
    renderCount: renderCount.current,
    avgRenderTime:
      renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length
  };
}

// useWhyDidYouUpdate - Track which props/state caused re-render
export function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const allKeys = Object.keys({ ...previousProps.current, ...props });
      const changedProps = {};

      allKeys.forEach((key) => {
        if (previousProps.current[key] !== props[key]) {
          changedProps[key] = {
            from: previousProps.current[key],
            to: props[key]
          };
        }
      });

      if (Object.keys(changedProps).length > 0) {
        console.log(`🔄 ${name} re-rendered due to:`, changedProps);
      }
    }

    previousProps.current = props;
  });
}

// Usage
function ExpensiveComponent({ data, filter, onUpdate }) {
  usePerformanceMonitor('ExpensiveComponent', [data, filter]);
  useWhyDidYouUpdate('ExpensiveComponent', { data, filter, onUpdate });

  const filteredData = useMemo(
    () => data.filter(filter),
    [data, filter]
  );

  return (
    <div>
      {filteredData.map(item => (
        <Item key={item.id} item={item} />
      ))}
    </div>
  );
}

Example: Redux Performance Monitoring Middleware

// performanceMiddleware.js
const performanceMiddleware = (store) => (next) => (action) => {
  const startTime = performance.now();
  const startMark = `action-${action.type}-start`;
  const endMark = `action-${action.type}-end`;

  // Mark start
  performance.mark(startMark);

  // Execute action
  const result = next(action);

  // Mark end
  performance.mark(endMark);

  // Measure duration
  const measureName = `action-${action.type}`;
  performance.measure(measureName, startMark, endMark);

  const endTime = performance.now();
  const duration = endTime - startTime;

  // Get performance entry
  const entries = performance.getEntriesByName(measureName);
  const entry = entries[entries.length - 1];

  // Log slow actions
  if (duration > 16) { // Slower than one frame (60fps)
    console.warn('⚠️ Slow action detected:', {
      action: action.type,
      duration: duration.toFixed(2) + 'ms',
      payload: action.payload
    });
  }

  // Aggregate statistics
  if (!window.__ACTION_STATS__) {
    window.__ACTION_STATS__ = {};
  }

  if (!window.__ACTION_STATS__[action.type]) {
    window.__ACTION_STATS__[action.type] = {
      count: 0,
      totalTime: 0,
      minTime: Infinity,
      maxTime: 0
    };
  }

  const stats = window.__ACTION_STATS__[action.type];
  stats.count += 1;
  stats.totalTime += duration;
  stats.minTime = Math.min(stats.minTime, duration);
  stats.maxTime = Math.max(stats.maxTime, duration);
  stats.avgTime = stats.totalTime / stats.count;

  // Clean up performance marks
  performance.clearMarks(startMark);
  performance.clearMarks(endMark);
  performance.clearMeasures(measureName);

  return result;
};

// View statistics in console
window.viewActionStats = () => {
  console.table(
    Object.entries(window.__ACTION_STATS__).map(([type, stats]) => ({
      Action: type,
      Count: stats.count,
      'Avg (ms)': stats.avgTime.toFixed(2),
      'Min (ms)': stats.minTime.toFixed(2),
      'Max (ms)': stats.maxTime.toFixed(2),
      'Total (ms)': stats.totalTime.toFixed(2)
    }))
  );
};

// Usage: Run viewActionStats() in console to see performance breakdown
Note: Use React DevTools Profiler tab to record and analyze render performance visually. Look for components with long render times or frequent unnecessary re-renders.

18.6 Custom Debug Hooks for State Monitoring

Create reusable debug hooks to monitor state changes, track renders, and identify performance issues.

Debug Hook Purpose Use Case
useDebugValue Label custom hooks in DevTools Display formatted debug info
useTraceUpdate Log which props/state changed Find cause of re-renders
useRenderCount Count component renders Detect excessive re-renders
useStateLogger Log all state changes Track state evolution

Example: Comprehensive Debug Hook Collection

// debugHooks.js
import { useRef, useEffect, useDebugValue } from 'react';

// 1. useTraceUpdate - Find what caused re-render
export function useTraceUpdate(props, componentName = 'Component') {
  const prev = useRef(props);

  useEffect(() => {
    const changedProps = Object.entries(props).reduce((acc, [key, value]) => {
      if (prev.current[key] !== value) {
        acc[key] = { from: prev.current[key], to: value };
      }
      return acc;
    }, {});

    if (Object.keys(changedProps).length > 0) {
      console.log(`[${componentName}] Changed props:`, changedProps);
    }

    prev.current = props;
  });
}

// 2. useRenderCount - Count renders
export function useRenderCount(componentName = 'Component') {
  const renders = useRef(0);
  renders.current += 1;

  useEffect(() => {
    console.log(`[${componentName}] Render #${renders.current}`);
  });

  useDebugValue(`Rendered ${renders.current} times`);

  return renders.current;
}

// 3. useStateWithHistory - State with history tracking
export function useStateWithHistory(initialValue, capacity = 10) {
  const [value, setValue] = useState(initialValue);
  const history = useRef([initialValue]);
  const pointer = useRef(0);

  const set = useCallback((newValue) => {
    const resolvedValue = typeof newValue === 'function' 
      ? newValue(value) 
      : newValue;

    if (history.current[pointer.current] !== resolvedValue) {
      // Remove future history if we're not at the end
      if (pointer.current < history.current.length - 1) {
        history.current.splice(pointer.current + 1);
      }

      history.current.push(resolvedValue);

      // Limit history size
      if (history.current.length > capacity) {
        history.current.shift();
      } else {
        pointer.current += 1;
      }

      setValue(resolvedValue);
    }
  }, [value, capacity]);

  const back = useCallback(() => {
    if (pointer.current > 0) {
      pointer.current -= 1;
      setValue(history.current[pointer.current]);
    }
  }, []);

  const forward = useCallback(() => {
    if (pointer.current < history.current.length - 1) {
      pointer.current += 1;
      setValue(history.current[pointer.current]);
    }
  }, []);

  const go = useCallback((index) => {
    if (index >= 0 && index < history.current.length) {
      pointer.current = index;
      setValue(history.current[index]);
    }
  }, []);

  useDebugValue(`History: ${pointer.current + 1}/${history.current.length}`);

  return {
    value,
    setValue: set,
    history: history.current,
    pointer: pointer.current,
    back,
    forward,
    go,
    canGoBack: pointer.current > 0,
    canGoForward: pointer.current < history.current.length - 1
  };
}

// 4. useDebugState - Enhanced useState with logging
export function useDebugState(initialValue, name = 'State') {
  const [value, setValue] = useState(initialValue);
  const renders = useRef(0);

  renders.current += 1;

  const debugSetValue = useCallback((newValue) => {
    const resolvedValue = typeof newValue === 'function' 
      ? newValue(value) 
      : newValue;

    console.log(`[${name}] State change:`, {
      from: value,
      to: resolvedValue,
      render: renders.current
    });

    setValue(resolvedValue);
  }, [value, name]);

  useDebugValue(`${name}: ${JSON.stringify(value)}`);

  return [value, debugSetValue];
}

// 5. useEffectDebugger - Debug useEffect dependencies
export function useEffectDebugger(effectFn, dependencies, name = 'Effect') {
  const previousDeps = useRef(dependencies);

  useEffect(() => {
    const changedDeps = dependencies.reduce((acc, dep, index) => {
      if (dep !== previousDeps.current[index]) {
        acc.push({
          index,
          before: previousDeps.current[index],
          after: dep
        });
      }
      return acc;
    }, []);

    if (changedDeps.length > 0) {
      console.log(`[${name}] Effect triggered by:`, changedDeps);
    }

    previousDeps.current = dependencies;

    return effectFn();
  }, dependencies);
}

// 6. useComponentDidMount - Track mount/unmount
export function useComponentDidMount(componentName = 'Component') {
  useEffect(() => {
    console.log(`✅ [${componentName}] Mounted`);
    return () => {
      console.log(`❌ [${componentName}] Unmounted`);
    };
  }, [componentName]);
}

Example: Using Debug Hooks in Components

// UserProfile.jsx
import {
  useTraceUpdate,
  useRenderCount,
  useDebugState,
  useEffectDebugger,
  useComponentDidMount
} from './debugHooks';

function UserProfile({ userId, theme, onUpdate }) {
  // Track what causes re-renders
  useTraceUpdate({ userId, theme, onUpdate }, 'UserProfile');

  // Count renders
  const renderCount = useRenderCount('UserProfile');

  // Track mount/unmount
  useComponentDidMount('UserProfile');

  // Debug state changes
  const [user, setUser] = useDebugState(null, 'User');
  const [loading, setLoading] = useDebugState(true, 'Loading');

  // Debug effect dependencies
  useEffectDebugger(
    () => {
      const fetchUser = async () => {
        setLoading(true);
        const data = await fetch(`/api/users/${userId}`).then(r => r.json());
        setUser(data);
        setLoading(false);
      };
      fetchUser();
    },
    [userId],
    'FetchUser'
  );

  if (loading) return <div>Loading...</div>;

  return (
    <div className={theme}>
      <h1>{user?.name}</h1>
      <p>Render count: {renderCount}</p>
    </div>
  );
}

// Console output when userId changes:
// [UserProfile] Changed props: { userId: { from: 1, to: 2 } }
// [UserProfile] Render #2
// [FetchUser] Effect triggered by: [{ index: 0, before: 1, after: 2 }]
// [Loading] State change: { from: false, to: true, render: 2 }
// [User] State change: { from: {...}, to: {...}, render: 3 }
// [Loading] State change: { from: true, to: false, render: 3 }

Example: Production-Safe Debug Wrapper

// Production-safe debug hooks that only run in development
const isDevelopment = process.env.NODE_ENV === 'development';

export const useDebugHooks = {
  useTraceUpdate: isDevelopment 
    ? useTraceUpdate 
    : () => {},
  
  useRenderCount: isDevelopment 
    ? useRenderCount 
    : () => 0,
  
  useDebugState: isDevelopment 
    ? useDebugState 
    : useState,
  
  useEffectDebugger: isDevelopment 
    ? useEffectDebugger 
    : useEffect,
  
  useComponentDidMount: isDevelopment 
    ? useComponentDidMount 
    : () => {}
};

// Usage - automatically disabled in production
import { useDebugHooks } from './debugHooks';

function MyComponent(props) {
  useDebugHooks.useTraceUpdate(props, 'MyComponent');
  const renderCount = useDebugHooks.useRenderCount('MyComponent');
  const [state, setState] = useDebugHooks.useDebugState(0, 'Counter');

  // These hooks do nothing in production builds
  return <div>Count: {state}</div>;
}
Note: Use useDebugValue in custom hooks to display formatted debug information in React DevTools. This helps identify hook values without console logging.

Section 18 Key Takeaways

  • React DevTools - Inspect component state, edit values in real-time, use Profiler to identify performance issues
  • Redux DevTools - Time-travel debugging, action history, state diff visualization, export/import state
  • State logging - Implement structured logging, sanitize sensitive data, disable verbose logging in production
  • Time-travel - Store state history, implement undo/redo, jump to any previous state for debugging
  • Performance profiling - Use React Profiler, track render times, identify slow actions with middleware
  • Debug hooks - Create reusable hooks for tracing updates, counting renders, logging state changes

19. State Security and Best Practices

19.1 Sensitive Data Handling in State

Implement secure patterns for handling sensitive data in application state to prevent exposure and leaks.

Security Practice Description Use Case
Minimize state storage Don't store sensitive data in state unnecessarily Passwords, credit cards, SSN
Memory cleanup Clear sensitive data on unmount Authentication tokens, PII
Avoid localStorage Don't persist sensitive data in browser storage Passwords, personal information
Use secure contexts Isolate sensitive state in protected contexts Payment info, user credentials
Redact in DevTools Prevent sensitive data in dev tools Development debugging

Example: Secure Sensitive Data Handling

// ❌ BAD: Storing sensitive data in state
function BadPaymentForm() {
  const [creditCard, setCreditCard] = useState({
    number: '',
    cvv: '',
    expiryDate: ''
  });

  // This data is visible in React DevTools and can be logged
  return (
    <form>
      <input
        value={creditCard.number}
        onChange={(e) => setCreditCard({ ...creditCard, number: e.target.value })}
      />
    </form>
  );
}

// ✅ GOOD: Using uncontrolled inputs for sensitive data
function GoodPaymentForm() {
  const cardNumberRef = useRef(null);
  const cvvRef = useRef(null);
  const expiryRef = useRef(null);

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Only access values when needed
    const sensitiveData = {
      cardNumber: cardNumberRef.current.value,
      cvv: cvvRef.current.value,
      expiry: expiryRef.current.value
    };

    // Send immediately without storing in state
    await processPayment(sensitiveData);

    // Clear immediately after use
    cardNumberRef.current.value = '';
    cvvRef.current.value = '';
    expiryRef.current.value = '';
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={cardNumberRef}
        type="text"
        autoComplete="cc-number"
        placeholder="Card Number"
      />
      <input
        ref={cvvRef}
        type="text"
        autoComplete="cc-csc"
        placeholder="CVV"
      />
      <input
        ref={expiryRef}
        type="text"
        autoComplete="cc-exp"
        placeholder="MM/YY"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Example: Secure Context for Sensitive State

// SecureStateContext.jsx
import { createContext, useContext, useState, useEffect, useRef } from 'react';

const SecureStateContext = createContext(null);

export function SecureStateProvider({ children }) {
  // Use ref to avoid DevTools exposure
  const secureDataRef = useRef(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);

  // Store sensitive data in closure, not state
  const setSensitiveData = (data) => {
    secureDataRef.current = data;
  };

  const getSensitiveData = () => {
    return secureDataRef.current;
  };

  const clearSensitiveData = () => {
    if (secureDataRef.current) {
      // Overwrite with zeros before clearing
      if (typeof secureDataRef.current === 'object') {
        Object.keys(secureDataRef.current).forEach(key => {
          secureDataRef.current[key] = null;
        });
      }
      secureDataRef.current = null;
    }
  };

  // Clear on unmount
  useEffect(() => {
    return () => {
      clearSensitiveData();
    };
  }, []);

  // Clear on page visibility change (tab switch)
  useEffect(() => {
    const handleVisibilityChange = () => {
      if (document.hidden) {
        // Optionally clear when tab is hidden
        // clearSensitiveData();
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
    };
  }, []);

  const value = {
    isAuthenticated,
    setIsAuthenticated,
    setSensitiveData,
    getSensitiveData,
    clearSensitiveData
  };

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

export function useSecureState() {
  const context = useContext(SecureStateContext);
  if (!context) {
    throw new Error('useSecureState must be used within SecureStateProvider');
  }
  return context;
}

// Usage
function LoginForm() {
  const { setSensitiveData, setIsAuthenticated } = useSecureState();
  const passwordRef = useRef(null);

  const handleLogin = async (e) => {
    e.preventDefault();
    const password = passwordRef.current.value;

    // Don't store password in state!
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ password })
    });

    if (response.ok) {
      const { token } = await response.json();
      setSensitiveData({ token }); // Store in ref, not state
      setIsAuthenticated(true);
      passwordRef.current.value = ''; // Clear immediately
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input ref={passwordRef} type="password" />
      <button>Login</button>
    </form>
  );
}
Warning: Never store passwords, credit card numbers, or other highly sensitive data in React state. Use uncontrolled inputs with refs, and transmit data directly to the server without storing it.

19.2 State Sanitization and Validation

Validate and sanitize all state data to prevent malicious input and ensure data integrity.

Validation Pattern Description Use Case
Input validation Validate before storing in state Forms, user input
Type checking Ensure correct data types TypeScript, PropTypes
Sanitization Remove dangerous characters/scripts HTML content, user text
Schema validation Validate against schema (Zod, Yup) Complex objects, API responses
Whitelisting Only allow known safe values Enums, dropdown selections

Example: State Validation with Zod

// userSchema.ts
import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(['user', 'admin', 'moderator']),
  bio: z.string().max(500).optional()
});

type User = z.infer<typeof userSchema>;

// UserForm.tsx
import { useState } from 'react';
import { userSchema } from './userSchema';

function UserForm() {
  const [user, setUser] = useState<Partial<User>>({});
  const [errors, setErrors] = useState<Record<string, string>>({});

  const handleChange = (field: keyof User, value: any) => {
    // Validate individual field
    const fieldSchema = userSchema.shape[field];
    const result = fieldSchema.safeParse(value);

    if (result.success) {
      setUser(prev => ({ ...prev, [field]: value }));
      setErrors(prev => {
        const { [field]: _, ...rest } = prev;
        return rest;
      });
    } else {
      setErrors(prev => ({
        ...prev,
        [field]: result.error.errors[0].message
      }));
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // Validate entire object before submission
    const result = userSchema.safeParse(user);

    if (result.success) {
      // Safe to use validated data
      await saveUser(result.data);
      setErrors({});
    } else {
      // Set all validation errors
      const fieldErrors: Record<string, string> = {};
      result.error.errors.forEach(err => {
        if (err.path[0]) {
          fieldErrors[err.path[0] as string] = err.message;
        }
      });
      setErrors(fieldErrors);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={user.name || ''}
        onChange={(e) => handleChange('name', e.target.value)}
      />
      {errors.name && <span className="error">{errors.name}</span>}

      <input
        value={user.email || ''}
        onChange={(e) => handleChange('email', e.target.value)}
      />
      {errors.email && <span className="error">{errors.email}</span>}

      <button type="submit">Save</button>
    </form>
  );
}

Example: Input Sanitization for HTML Content

// sanitization.ts
import DOMPurify from 'dompurify';

// Sanitize HTML content
export function sanitizeHTML(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target']
  });
}

// Sanitize plain text (remove HTML)
export function sanitizeText(input: string): string {
  return input
    .replace(/<[^>]*>/g, '') // Remove HTML tags
    .replace(/[<>"']/g, '') // Remove dangerous characters
    .trim()
    .slice(0, 1000); // Limit length
}

// Validate and sanitize URL
export function sanitizeURL(url: string): string | null {
  try {
    const parsed = new URL(url);
    // Only allow http and https protocols
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return null;
    }
    return parsed.href;
  } catch {
    return null;
  }
}

// RichTextEditor.tsx
import { useState } from 'react';
import { sanitizeHTML } from './sanitization';

function RichTextEditor() {
  const [content, setContent] = useState('');
  const [sanitizedContent, setSanitizedContent] = useState('');

  const handleChange = (rawHTML: string) => {
    // Always sanitize before storing in state
    const clean = sanitizeHTML(rawHTML);
    setContent(rawHTML); // Original for editing
    setSanitizedContent(clean); // Safe for rendering
  };

  const handleSave = async () => {
    // Use sanitized version for API calls
    await fetch('/api/content', {
      method: 'POST',
      body: JSON.stringify({ content: sanitizedContent })
    });
  };

  return (
    <div>
      <textarea
        value={content}
        onChange={(e) => handleChange(e.target.value)}
      />
      
      {/* Safe to render sanitized content */}
      <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
      
      <button onClick={handleSave}>Save</button>
    </div>
  );
}

Example: Custom Validation Hook

// useValidatedState.ts
import { useState, useCallback } from 'react';

type Validator<T> = (value: T) => string | null;

export function useValidatedState<T>(
  initialValue: T,
  validators: Validator<T>[]
) {
  const [value, setValue] = useState<T>(initialValue);
  const [error, setError] = useState<string | null>(null);
  const [touched, setTouched] = useState(false);

  const validate = useCallback((newValue: T): boolean => {
    for (const validator of validators) {
      const errorMessage = validator(newValue);
      if (errorMessage) {
        setError(errorMessage);
        return false;
      }
    }
    setError(null);
    return true;
  }, [validators]);

  const setValidatedValue = useCallback((newValue: T) => {
    const isValid = validate(newValue);
    if (isValid) {
      setValue(newValue);
    }
  }, [validate]);

  const forceSetValue = useCallback((newValue: T) => {
    setValue(newValue);
    validate(newValue);
  }, [validate]);

  const handleBlur = useCallback(() => {
    setTouched(true);
    validate(value);
  }, [value, validate]);

  return {
    value,
    setValue: setValidatedValue,
    forceSetValue,
    error: touched ? error : null,
    isValid: !error,
    touched,
    setTouched,
    handleBlur
  };
}

// Validators
const required = (value: string) => 
  value.trim() === '' ? 'This field is required' : null;

const minLength = (min: number) => (value: string) =>
  value.length < min ? `Must be at least ${min} characters` : null;

const maxLength = (max: number) => (value: string) =>
  value.length > max ? `Must be at most ${max} characters` : null;

const emailFormat = (value: string) =>
  !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'Invalid email format' : null;

// Usage
function SignupForm() {
  const email = useValidatedState('', [required, emailFormat]);
  const password = useValidatedState('', [required, minLength(8), maxLength(100)]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    if (email.isValid && password.isValid) {
      console.log('Valid!', { email: email.value, password: password.value });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email.value}
        onChange={(e) => email.forceSetValue(e.target.value)}
        onBlur={email.handleBlur}
      />
      {email.error && <span>{email.error}</span>}

      <input
        type="password"
        value={password.value}
        onChange={(e) => password.forceSetValue(e.target.value)}
        onBlur={password.handleBlur}
      />
      {password.error && <span>{password.error}</span>}

      <button type="submit">Sign Up</button>
    </form>
  );
}
Note: Always validate user input on both client and server. Client-side validation improves UX, but server-side validation is essential for security.

19.3 XSS Prevention in Dynamic State Rendering

Prevent cross-site scripting (XSS) attacks when rendering dynamic content from state.

XSS Prevention Description Use Case
Automatic escaping React escapes text content by default Most text rendering
Avoid dangerouslySetInnerHTML Don't use unless absolutely necessary Rich text, Markdown
Sanitize HTML Use DOMPurify before rendering HTML User-generated HTML content
CSP headers Content Security Policy headers Server-side protection
Validate URLs Check href/src before rendering Links, images from user input

Example: Safe vs Unsafe State Rendering

// ❌ UNSAFE: Direct rendering of user input in dangerouslySetInnerHTML
function UnsafeComment({ comment }) {
  // If comment.text contains: <script>alert('XSS')</script>
  // This will execute the script!
  return (
    <div dangerouslySetInnerHTML={{ __html: comment.text }} />
  );
}

// ✅ SAFE: React automatically escapes text content
function SafeComment({ comment }) {
  // React escapes HTML entities automatically
  // <script> becomes &lt;script&gt; and won't execute
  return <div>{comment.text}</div>;
}

// ✅ SAFE: Sanitize before using dangerouslySetInnerHTML
import DOMPurify from 'dompurify';

function SafeRichComment({ comment }) {
  const [sanitizedHTML, setSanitizedHTML] = useState('');

  useEffect(() => {
    const clean = DOMPurify.sanitize(comment.html, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
      ALLOWED_ATTR: ['href'],
      ALLOWED_URI_REGEXP: /^https?:\/\//
    });
    setSanitizedHTML(clean);
  }, [comment.html]);

  return (
    <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
  );
}

// ❌ UNSAFE: User-controlled href
function UnsafeLink({ url, text }) {
  // url could be: javascript:alert('XSS')
  return <a href={url}>{text}</a>;
}

// ✅ SAFE: Validate URL protocol
function SafeLink({ url, text }) {
  const [safeUrl, setSafeUrl] = useState('#');

  useEffect(() => {
    try {
      const parsed = new URL(url);
      if (['http:', 'https:'].includes(parsed.protocol)) {
        setSafeUrl(parsed.href);
      }
    } catch {
      setSafeUrl('#');
    }
  }, [url]);

  return (
    <a 
      href={safeUrl}
      target="_blank"
      rel="noopener noreferrer" // Prevent window.opener attacks
    >
      {text}
    </a>
  );
}

Example: Safe Markdown Rendering from State

// MarkdownRenderer.tsx
import { useState, useEffect } from 'react';
import { marked } from 'marked';
import DOMPurify from 'dompurify';

// Configure marked to be more secure
marked.setOptions({
  headerIds: false,
  mangle: false
});

function MarkdownRenderer({ content }) {
  const [safeHTML, setSafeHTML] = useState('');

  useEffect(() => {
    // Convert markdown to HTML
    const rawHTML = marked.parse(content);

    // Sanitize the HTML
    const clean = DOMPurify.sanitize(rawHTML, {
      ALLOWED_TAGS: [
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
        'p', 'br', 'strong', 'em', 'u',
        'ul', 'ol', 'li',
        'a', 'code', 'pre',
        'blockquote'
      ],
      ALLOWED_ATTR: ['href', 'title'],
      ALLOWED_URI_REGEXP: /^https?:\/\//,
      // Add hooks to modify links
      RETURN_DOM: false,
      RETURN_DOM_FRAGMENT: false
    });

    setSafeHTML(clean);
  }, [content]);

  return (
    <div 
      className="markdown-content"
      dangerouslySetInnerHTML={{ __html: safeHTML }}
    />
  );
}

// Usage with state
function BlogPost() {
  const [post, setPost] = useState({ markdown: '' });

  useEffect(() => {
    // Fetch post from API
    fetch('/api/posts/1')
      .then(r => r.json())
      .then(data => setPost(data));
  }, []);

  return (
    <div>
      <h1>{post.title}</h1>
      {/* Safely render user-generated markdown */}
      <MarkdownRenderer content={post.markdown} />
    </div>
  );
}

Example: CSP Integration with React State

// Create nonce for inline scripts
// server.js (Next.js, Express, etc.)
const crypto = require('crypto');

function generateCSPNonce() {
  return crypto.randomBytes(16).toString('base64');
}

app.use((req, res, next) => {
  const nonce = generateCSPNonce();
  res.locals.cspNonce = nonce;
  
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${nonce}'; ` +
    `style-src 'self' 'nonce-${nonce}'; ` +
    `img-src 'self' https:; ` +
    `connect-src 'self' https://api.example.com; ` +
    `default-src 'self'`
  );
  
  next();
});

// React component
function SecureComponent({ nonce }) {
  const [data, setData] = useState(null);

  useEffect(() => {
    // CSP allows this fetch because connect-src includes the domain
    fetch('https://api.example.com/data')
      .then(r => r.json())
      .then(setData);
  }, []);

  return (
    <div>
      {/* React automatically escapes - safe from XSS */}
      <p>{data?.userInput}</p>
      
      {/* Inline script requires nonce */}
      <script nonce={nonce}>
        {`console.log('This script has the correct nonce')`}
      </script>
    </div>
  );
}
Warning: Never trust user input. Always sanitize HTML content before rendering with dangerouslySetInnerHTML. Use DOMPurify or similar libraries to remove malicious code.

19.4 State Encryption for Client-side Storage

Encrypt sensitive state data before storing in localStorage, sessionStorage, or IndexedDB.

Encryption Technique Description Use Case
Web Crypto API Browser-native encryption Secure client-side encryption
AES-GCM Authenticated encryption algorithm Strong encryption standard
Key derivation Derive keys from user passwords Password-based encryption
Secure key storage Never store keys in localStorage Memory or session only

Example: Encrypted State Storage Hook

// encryption.ts
class StateEncryption {
  private key: CryptoKey | null = null;

  async generateKey(password: string): Promise<void> {
    const encoder = new TextEncoder();
    const passwordBuffer = encoder.encode(password);

    // Derive key from password using PBKDF2
    const keyMaterial = await crypto.subtle.importKey(
      'raw',
      passwordBuffer,
      'PBKDF2',
      false,
      ['deriveBits', 'deriveKey']
    );

    this.key = await crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: encoder.encode('unique-app-salt'), // Use unique salt per app
        iterations: 100000,
        hash: 'SHA-256'
      },
      keyMaterial,
      { name: 'AES-GCM', length: 256 },
      true,
      ['encrypt', 'decrypt']
    );
  }

  async encrypt(data: any): Promise<string> {
    if (!this.key) throw new Error('Key not initialized');

    const encoder = new TextEncoder();
    const plaintext = encoder.encode(JSON.stringify(data));

    // Generate random IV
    const iv = crypto.getRandomValues(new Uint8Array(12));

    const ciphertext = await crypto.subtle.encrypt(
      { name: 'AES-GCM', iv },
      this.key,
      plaintext
    );

    // Combine IV and ciphertext
    const combined = new Uint8Array(iv.length + ciphertext.byteLength);
    combined.set(iv, 0);
    combined.set(new Uint8Array(ciphertext), iv.length);

    // Convert to base64
    return btoa(String.fromCharCode(...combined));
  }

  async decrypt(encryptedData: string): Promise<any> {
    if (!this.key) throw new Error('Key not initialized');

    // Decode base64
    const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));

    // Extract IV and ciphertext
    const iv = combined.slice(0, 12);
    const ciphertext = combined.slice(12);

    const plaintext = await crypto.subtle.decrypt(
      { name: 'AES-GCM', iv },
      this.key,
      ciphertext
    );

    const decoder = new TextDecoder();
    return JSON.parse(decoder.decode(plaintext));
  }

  clearKey(): void {
    this.key = null;
  }
}

export const stateEncryption = new StateEncryption();

// useEncryptedStorage.ts
import { useState, useEffect, useCallback } from 'react';
import { stateEncryption } from './encryption';

export function useEncryptedStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T) => Promise<void>, () => void] {
  const [state, setState] = useState<T>(initialValue);

  // Load encrypted data on mount
  useEffect(() => {
    const loadEncryptedData = async () => {
      try {
        const encrypted = localStorage.getItem(key);
        if (encrypted) {
          const decrypted = await stateEncryption.decrypt(encrypted);
          setState(decrypted);
        }
      } catch (error) {
        console.error('Failed to decrypt data:', error);
        localStorage.removeItem(key);
      }
    };

    loadEncryptedData();
  }, [key]);

  // Save encrypted data
  const setEncryptedState = useCallback(async (value: T) => {
    setState(value);
    try {
      const encrypted = await stateEncryption.encrypt(value);
      localStorage.setItem(key, encrypted);
    } catch (error) {
      console.error('Failed to encrypt data:', error);
    }
  }, [key]);

  // Clear encrypted data
  const clearEncryptedState = useCallback(() => {
    setState(initialValue);
    localStorage.removeItem(key);
  }, [key, initialValue]);

  return [state, setEncryptedState, clearEncryptedState];
}

// Usage
function UserPreferences() {
  const [preferences, setPreferences, clearPreferences] = useEncryptedStorage(
    'user_preferences',
    { theme: 'light', notifications: true }
  );

  useEffect(() => {
    // Initialize encryption key from user password
    // (In real app, get this after user login)
    stateEncryption.generateKey('user-password-or-session-token');

    return () => {
      // Clear key on unmount
      stateEncryption.clearKey();
    };
  }, []);

  const handleSave = async () => {
    await setPreferences({
      ...preferences,
      theme: 'dark'
    });
  };

  return (
    <div>
      <p>Theme: {preferences.theme}</p>
      <button onClick={handleSave}>Save Encrypted</button>
      <button onClick={clearPreferences}>Clear</button>
    </div>
  );
}
Note: Client-side encryption provides defense-in-depth but is not a substitute for server-side security. The encryption key must be protected and should not be stored in localStorage.

19.5 Authentication State and Token Management

Securely manage authentication tokens and user session state in React applications.

Auth Pattern Description Use Case
HTTP-only cookies Store tokens in secure cookies Most secure for web apps
Memory storage Store tokens in state/memory only Single-page sessions
Token refresh Automatic token renewal Long-lived sessions
Secure token transmission HTTPS only, Authorization header API calls
Auto logout Clear state on inactivity/expiry Security timeout

Example: Secure Authentication State Management

// AuthContext.tsx
import { createContext, useContext, useState, useEffect, useRef } from 'react';

interface User {
  id: string;
  email: string;
  role: string;
}

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  logout: () => Promise<void>;
  refreshToken: () => Promise<void>;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  
  // Store token in memory (ref), NOT localStorage
  const accessTokenRef = useRef<string | null>(null);
  const refreshTimerRef = useRef<NodeJS.Timeout | null>(null);

  // Login function
  const login = async (email: string, password: string) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'include', // Send HTTP-only refresh token cookie
        body: JSON.stringify({ email, password })
      });

      if (!response.ok) throw new Error('Login failed');

      const { accessToken, user } = await response.json();

      // Store access token in memory only
      accessTokenRef.current = accessToken;
      setUser(user);
      setIsAuthenticated(true);

      // Schedule token refresh
      scheduleTokenRefresh();
    } catch (error) {
      console.error('Login error:', error);
      throw error;
    }
  };

  // Refresh access token using HTTP-only refresh token
  const refreshToken = async () => {
    try {
      const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        credentials: 'include' // Send refresh token cookie
      });

      if (!response.ok) {
        throw new Error('Token refresh failed');
      }

      const { accessToken } = await response.json();
      accessTokenRef.current = accessToken;

      scheduleTokenRefresh();
    } catch (error) {
      console.error('Token refresh error:', error);
      await logout();
    }
  };

  // Schedule automatic token refresh (before expiry)
  const scheduleTokenRefresh = () => {
    if (refreshTimerRef.current) {
      clearTimeout(refreshTimerRef.current);
    }

    // Refresh 5 minutes before expiry (15 minutes if token expires in 20)
    const refreshInterval = 15 * 60 * 1000; // 15 minutes
    refreshTimerRef.current = setTimeout(refreshToken, refreshInterval);
  };

  // Logout function
  const logout = async () => {
    try {
      await fetch('/api/auth/logout', {
        method: 'POST',
        credentials: 'include'
      });
    } catch (error) {
      console.error('Logout error:', error);
    } finally {
      // Clear all auth state
      accessTokenRef.current = null;
      setUser(null);
      setIsAuthenticated(false);

      if (refreshTimerRef.current) {
        clearTimeout(refreshTimerRef.current);
        refreshTimerRef.current = null;
      }
    }
  };

  // Auto logout on inactivity
  useEffect(() => {
    let inactivityTimer: NodeJS.Timeout;

    const resetInactivityTimer = () => {
      clearTimeout(inactivityTimer);
      if (isAuthenticated) {
        // Logout after 30 minutes of inactivity
        inactivityTimer = setTimeout(logout, 30 * 60 * 1000);
      }
    };

    const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
    events.forEach(event => {
      document.addEventListener(event, resetInactivityTimer);
    });

    resetInactivityTimer();

    return () => {
      events.forEach(event => {
        document.removeEventListener(event, resetInactivityTimer);
      });
      clearTimeout(inactivityTimer);
    };
  }, [isAuthenticated]);

  // Try to restore session on mount
  useEffect(() => {
    const restoreSession = async () => {
      try {
        // Try to refresh token using HTTP-only cookie
        await refreshToken();
        
        // Fetch user info
        const response = await fetch('/api/auth/me', {
          credentials: 'include'
        });
        
        if (response.ok) {
          const userData = await response.json();
          setUser(userData);
          setIsAuthenticated(true);
        }
      } catch (error) {
        console.error('Session restore failed:', error);
      }
    };

    restoreSession();
  }, []);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (refreshTimerRef.current) {
        clearTimeout(refreshTimerRef.current);
      }
    };
  }, []);

  // Create API client with automatic token injection
  const apiClient = {
    get: async (url: string) => {
      const response = await fetch(url, {
        headers: {
          'Authorization': `Bearer ${accessTokenRef.current}`
        },
        credentials: 'include'
      });

      // Handle token expiry
      if (response.status === 401) {
        await refreshToken();
        // Retry request
        return fetch(url, {
          headers: {
            'Authorization': `Bearer ${accessTokenRef.current}`
          },
          credentials: 'include'
        });
      }

      return response;
    }
  };

  const value = {
    user,
    isAuthenticated,
    login,
    logout,
    refreshToken
  };

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

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider');
  }
  return context;
}
Warning: Never store authentication tokens in localStorage or sessionStorage as they are vulnerable to XSS attacks. Use HTTP-only cookies or in-memory storage (with automatic refresh).

19.6 State Audit Logging and Compliance

Implement audit logging for state changes to meet compliance requirements and track user actions.

Audit Pattern Description Use Case
Action logging Record all state-changing actions Compliance, debugging
User tracking Associate changes with user IDs Accountability, forensics
Timestamp tracking Record when changes occurred Timeline reconstruction
Data retention Store audit logs securely GDPR, HIPAA compliance
Immutable logs Prevent log tampering Legal requirements

Example: Audit Logging Middleware for Redux

// auditMiddleware.ts
import { Middleware } from '@reduxjs/toolkit';

interface AuditLog {
  id: string;
  userId: string | null;
  action: string;
  payload: any;
  timestamp: string;
  previousState: any;
  nextState: any;
  ipAddress?: string;
  userAgent?: string;
}

const auditMiddleware: Middleware = (store) => (next) => async (action) => {
  const previousState = store.getState();
  const timestamp = new Date().toISOString();

  // Execute action
  const result = next(action);

  const nextState = store.getState();

  // Create audit log entry
  const auditLog: AuditLog = {
    id: crypto.randomUUID(),
    userId: previousState.auth?.user?.id || null,
    action: action.type,
    payload: sanitizePayload(action.payload),
    timestamp,
    previousState: sanitizeState(previousState),
    nextState: sanitizeState(nextState),
    ipAddress: await getClientIP(),
    userAgent: navigator.userAgent
  };

  // Send to audit logging service (async, non-blocking)
  sendAuditLog(auditLog).catch(error => {
    console.error('Failed to send audit log:', error);
  });

  // Store in local buffer for offline scenarios
  storeLocalAuditLog(auditLog);

  return result;
};

// Sanitize sensitive data before logging
function sanitizePayload(payload: any): any {
  if (!payload) return payload;

  const sanitized = { ...payload };
  const sensitiveKeys = ['password', 'token', 'creditCard', 'ssn', 'cvv'];

  Object.keys(sanitized).forEach(key => {
    if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
      sanitized[key] = '[REDACTED]';
    }
  });

  return sanitized;
}

function sanitizeState(state: any): any {
  // Only log relevant parts of state, redact sensitive info
  return {
    user: state.auth?.user ? { id: state.auth.user.id } : null,
    route: state.router?.location?.pathname,
    // Add other non-sensitive state slices
  };
}

async function sendAuditLog(log: AuditLog): Promise<void> {
  await fetch('/api/audit-logs', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(log)
  });
}

function storeLocalAuditLog(log: AuditLog): void {
  const logs = JSON.parse(localStorage.getItem('audit_logs_buffer') || '[]');
  logs.push(log);

  // Keep only last 100 logs locally
  if (logs.length > 100) {
    logs.shift();
  }

  localStorage.setItem('audit_logs_buffer', JSON.stringify(logs));
}

async function getClientIP(): Promise<string | undefined> {
  try {
    const response = await fetch('https://api.ipify.org?format=json');
    const data = await response.json();
    return data.ip;
  } catch {
    return undefined;
  }
}

export default auditMiddleware;

Example: GDPR-Compliant State Audit Hook

// useAuditedState.ts
import { useState, useCallback, useEffect } from 'react';

interface AuditEntry<T> {
  timestamp: string;
  action: 'SET' | 'RESET' | 'DELETE';
  previousValue: T | null;
  newValue: T | null;
  reason?: string;
}

interface AuditedStateOptions {
  userId?: string;
  component: string;
  sendToServer?: boolean;
  retentionDays?: number; // GDPR data retention
}

export function useAuditedState<T>(
  initialValue: T,
  options: AuditedStateOptions
) {
  const [state, setState] = useState<T>(initialValue);
  const [auditTrail, setAuditTrail] = useState<AuditEntry<T>[]>([]);

  const createAuditEntry = useCallback((
    action: AuditEntry<T>['action'],
    previousValue: T | null,
    newValue: T | null,
    reason?: string
  ): AuditEntry<T> => {
    return {
      timestamp: new Date().toISOString(),
      action,
      previousValue,
      newValue,
      reason
    };
  }, []);

  const setAuditedState = useCallback((
    newValue: T | ((prev: T) => T),
    reason?: string
  ) => {
    setState(prev => {
      const resolvedValue = typeof newValue === 'function'
        ? (newValue as Function)(prev)
        : newValue;

      const entry = createAuditEntry('SET', prev, resolvedValue, reason);
      setAuditTrail(trail => [...trail, entry]);

      // Send to server if configured
      if (options.sendToServer) {
        sendAuditToServer({
          userId: options.userId,
          component: options.component,
          entry
        });
      }

      return resolvedValue;
    });
  }, [createAuditEntry, options]);

  const resetState = useCallback((reason?: string) => {
    setState(prev => {
      const entry = createAuditEntry('RESET', prev, initialValue, reason);
      setAuditTrail(trail => [...trail, entry]);

      if (options.sendToServer) {
        sendAuditToServer({
          userId: options.userId,
          component: options.component,
          entry
        });
      }

      return initialValue;
    });
  }, [createAuditEntry, initialValue, options]);

  const deleteState = useCallback((reason?: string) => {
    setState(prev => {
      const entry = createAuditEntry('DELETE', prev, null, reason);
      setAuditTrail(trail => [...trail, entry]);

      if (options.sendToServer) {
        sendAuditToServer({
          userId: options.userId,
          component: options.component,
          entry
        });
      }

      return initialValue;
    });
  }, [createAuditEntry, initialValue, options]);

  // GDPR compliance: Auto-delete old audit entries
  useEffect(() => {
    if (!options.retentionDays) return;

    const retentionMs = options.retentionDays * 24 * 60 * 60 * 1000;
    const cutoffDate = new Date(Date.now() - retentionMs);

    setAuditTrail(trail =>
      trail.filter(entry => new Date(entry.timestamp) > cutoffDate)
    );
  }, [options.retentionDays]);

  // Export audit trail (for GDPR data export requests)
  const exportAuditTrail = useCallback(() => {
    return {
      component: options.component,
      userId: options.userId,
      entries: auditTrail,
      exportedAt: new Date().toISOString()
    };
  }, [auditTrail, options]);

  return {
    state,
    setState: setAuditedState,
    resetState,
    deleteState,
    auditTrail,
    exportAuditTrail
  };
}

async function sendAuditToServer(data: any): Promise<void> {
  try {
    await fetch('/api/audit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  } catch (error) {
    console.error('Failed to send audit log:', error);
  }
}

// Usage
function UserProfile() {
  const {
    state: profile,
    setState: setProfile,
    auditTrail,
    exportAuditTrail
  } = useAuditedState(
    { name: '', email: '' },
    {
      userId: 'user123',
      component: 'UserProfile',
      sendToServer: true,
      retentionDays: 90 // GDPR: Keep logs for 90 days
    }
  );

  const handleUpdate = () => {
    setProfile(
      { name: 'John Doe', email: 'john@example.com' },
      'User updated profile information'
    );
  };

  const handleExport = () => {
    const auditData = exportAuditTrail();
    console.log('Audit trail:', auditData);
    // Download or send to user for GDPR data export request
  };

  return (
    <div>
      <input
        value={profile.name}
        onChange={(e) => setProfile({ ...profile, name: e.target.value })}
      />
      <button onClick={handleUpdate}>Update</button>
      <button onClick={handleExport}>Export Audit Trail</button>
      
      <div>
        <h3>Audit Trail ({auditTrail.length} entries)</h3>
        {auditTrail.map((entry, i) => (
          <div key={i}>
            {entry.timestamp}: {entry.action} - {entry.reason}
          </div>
        ))}
      </div>
    </div>
  );
}
Note: Audit logs must comply with data protection regulations (GDPR, HIPAA, SOC 2). Implement proper retention policies, encrypt logs at rest, and provide data export/deletion capabilities.

Section 19 Key Takeaways

  • Sensitive data - Don't store passwords/credit cards in state, use refs for immediate transmission, clear on unmount
  • Validation - Validate all user input before storing, use Zod/Yup for schema validation, sanitize HTML content
  • XSS prevention - React auto-escapes text, sanitize before dangerouslySetInnerHTML, validate URLs, use CSP headers
  • Encryption - Use Web Crypto API for client-side encryption, never store keys in localStorage, AES-GCM for authenticated encryption
  • Auth tokens - Use HTTP-only cookies or memory storage, implement automatic token refresh, auto-logout on inactivity
  • Audit logging - Log state changes with user ID and timestamp, sanitize sensitive data, comply with GDPR retention policies

20. State Architecture and Design Patterns

20.1 Flux Architecture Patterns in React

Flux is a unidirectional data flow pattern that provides predictable state management through actions, dispatcher, stores, and views.

Flux Component Description Role
Actions Simple objects describing state changes Define what happened
Dispatcher Central hub for action distribution Route actions to stores
Stores Hold application state and logic Manage domain state
Views (Components) React components that render UI Display state, dispatch actions
Action Creators Helper functions to create actions Encapsulate action creation

Example: Classic Flux Pattern Implementation

// ActionTypes.ts - Constants for action types
export const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO',
  DELETE_TODO: 'DELETE_TODO',
  SET_FILTER: 'SET_FILTER'
} as const;

// Actions.ts - Action creators
import { ActionTypes } from './ActionTypes';

export const TodoActions = {
  addTodo: (text: string) => ({
    type: ActionTypes.ADD_TODO,
    payload: { text }
  }),

  toggleTodo: (id: string) => ({
    type: ActionTypes.TOGGLE_TODO,
    payload: { id }
  }),

  deleteTodo: (id: string) => ({
    type: ActionTypes.DELETE_TODO,
    payload: { id }
  }),

  setFilter: (filter: 'all' | 'active' | 'completed') => ({
    type: ActionTypes.SET_FILTER,
    payload: { filter }
  })
};

// Dispatcher.ts - Central dispatcher
import { EventEmitter } from 'events';

class AppDispatcher extends EventEmitter {
  dispatch(action: any) {
    this.emit('action', action);
  }

  register(callback: (action: any) => void) {
    this.on('action', callback);
    return () => this.off('action', callback);
  }
}

export const dispatcher = new AppDispatcher();

// TodoStore.ts - Store managing todo state
import { EventEmitter } from 'events';
import { dispatcher } from './Dispatcher';
import { ActionTypes } from './ActionTypes';

class TodoStore extends EventEmitter {
  private todos: Array<{ id: string; text: string; completed: boolean }> = [];
  private filter: 'all' | 'active' | 'completed' = 'all';

  constructor() {
    super();
    // Register with dispatcher
    dispatcher.register(this.handleAction.bind(this));
  }

  private handleAction(action: any) {
    switch (action.type) {
      case ActionTypes.ADD_TODO:
        this.todos.push({
          id: Date.now().toString(),
          text: action.payload.text,
          completed: false
        });
        this.emit('change');
        break;

      case ActionTypes.TOGGLE_TODO:
        this.todos = this.todos.map(todo =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        );
        this.emit('change');
        break;

      case ActionTypes.DELETE_TODO:
        this.todos = this.todos.filter(todo => todo.id !== action.payload.id);
        this.emit('change');
        break;

      case ActionTypes.SET_FILTER:
        this.filter = action.payload.filter;
        this.emit('change');
        break;
    }
  }

  getTodos() {
    return this.todos.filter(todo => {
      if (this.filter === 'active') return !todo.completed;
      if (this.filter === 'completed') return todo.completed;
      return true;
    });
  }

  getFilter() {
    return this.filter;
  }

  addChangeListener(callback: () => void) {
    this.on('change', callback);
  }

  removeChangeListener(callback: () => void) {
    this.off('change', callback);
  }
}

export const todoStore = new TodoStore();

// TodoList.tsx - React component (View)
import { useState, useEffect } from 'react';
import { todoStore } from './TodoStore';
import { TodoActions } from './Actions';
import { dispatcher } from './Dispatcher';

export function TodoList() {
  const [todos, setTodos] = useState(todoStore.getTodos());
  const [filter, setFilter] = useState(todoStore.getFilter());

  useEffect(() => {
    const handleChange = () => {
      setTodos(todoStore.getTodos());
      setFilter(todoStore.getFilter());
    };

    todoStore.addChangeListener(handleChange);
    return () => todoStore.removeChangeListener(handleChange);
  }, []);

  const handleAddTodo = (text: string) => {
    dispatcher.dispatch(TodoActions.addTodo(text));
  };

  const handleToggleTodo = (id: string) => {
    dispatcher.dispatch(TodoActions.toggleTodo(id));
  };

  return (
    <div>
      <input onKeyPress={(e) => {
        if (e.key === 'Enter') {
          handleAddTodo(e.currentTarget.value);
          e.currentTarget.value = '';
        }
      }} />

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggleTodo(todo.id)}
            />
            {todo.text}
          </li>
        ))}
      </ul>
    </div>
  );
}
Note: Modern Redux is based on Flux principles but simplifies the pattern by using a single store and removing the dispatcher. Redux Toolkit further simplifies Redux implementation.

20.2 Model-View-Update (MVU) Patterns

MVU (also known as The Elm Architecture) separates state (Model), rendering (View), and state transitions (Update) into distinct functions.

MVU Component Description Responsibility
Model Application state representation Define data structure
View Pure function: Model → UI Render state to UI
Update Pure function: (Model, Msg) → Model Handle state transitions
Messages Union type of all possible events Describe user actions
Commands Side effects to execute Async operations, API calls

Example: MVU Pattern in React with TypeScript

// Model - State definition
type Model = {
  count: number;
  status: 'idle' | 'loading' | 'success' | 'error';
  data: string | null;
  error: string | null;
};

const initialModel: Model = {
  count: 0,
  status: 'idle',
  data: null,
  error: null
};

// Messages - All possible user actions
type Msg =
  | { type: 'Increment' }
  | { type: 'Decrement' }
  | { type: 'Reset' }
  | { type: 'FetchData' }
  | { type: 'FetchSuccess'; data: string }
  | { type: 'FetchError'; error: string };

// Update - Pure state transition function
function update(model: Model, msg: Msg): [Model, Cmd?] {
  switch (msg.type) {
    case 'Increment':
      return [{ ...model, count: model.count + 1 }];

    case 'Decrement':
      return [{ ...model, count: model.count - 1 }];

    case 'Reset':
      return [{ ...model, count: 0 }];

    case 'FetchData':
      return [
        { ...model, status: 'loading' },
        fetchDataCmd() // Return command for side effect
      ];

    case 'FetchSuccess':
      return [{
        ...model,
        status: 'success',
        data: msg.data,
        error: null
      }];

    case 'FetchError':
      return [{
        ...model,
        status: 'error',
        data: null,
        error: msg.error
      }];

    default:
      return [model];
  }
}

// Commands - Side effect descriptions
type Cmd = () => Promise<Msg>;

function fetchDataCmd(): Cmd {
  return async () => {
    try {
      const response = await fetch('/api/data');
      const data = await response.text();
      return { type: 'FetchSuccess', data };
    } catch (error) {
      return { type: 'FetchError', error: (error as Error).message };
    }
  };
}

// View - Pure rendering function
function view(model: Model, dispatch: (msg: Msg) => void) {
  return (
    <div>
      <h1>Count: {model.count}</h1>
      <button onClick={() => dispatch({ type: 'Increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'Decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'Reset' })}>Reset</button>

      <hr />

      <button onClick={() => dispatch({ type: 'FetchData' })}>Fetch Data</button>
      
      {model.status === 'loading' && <p>Loading...</p>}
      {model.status === 'success' && <p>Data: {model.data}</p>}
      {model.status === 'error' && <p>Error: {model.error}</p>}
    </div>
  );
}

// Runtime - Connect Model, View, Update
function useMVU<TModel, TMsg>(
  initialModel: TModel,
  update: (model: TModel, msg: TMsg) => [TModel, Cmd?]
) {
  const [model, setModel] = useState(initialModel);

  const dispatch = useCallback((msg: TMsg) => {
    setModel(currentModel => {
      const [newModel, cmd] = update(currentModel, msg);
      
      // Execute command if present
      if (cmd) {
        cmd().then(resultMsg => dispatch(resultMsg as TMsg));
      }
      
      return newModel;
    });
  }, [update]);

  return [model, dispatch] as const;
}

// App component
export function Counter() {
  const [model, dispatch] = useMVU(initialModel, update);
  return view(model, dispatch);
}

Example: MVU with Effects System

// Enhanced MVU with proper effects handling
type Effect<Msg> = {
  type: 'http' | 'timer' | 'storage';
  execute: () => Promise<Msg>;
};

type UpdateResult<Model, Msg> = {
  model: Model;
  effects?: Effect<Msg>[];
};

// Example: Todo app with MVU + Effects
type TodoModel = {
  todos: Array<{ id: string; text: string; done: boolean }>;
  input: string;
};

type TodoMsg =
  | { type: 'UpdateInput'; text: string }
  | { type: 'AddTodo' }
  | { type: 'ToggleTodo'; id: string }
  | { type: 'LoadTodos' }
  | { type: 'TodosLoaded'; todos: any[] }
  | { type: 'SaveTodos' };

function todoUpdate(model: TodoModel, msg: TodoMsg): UpdateResult<TodoModel, TodoMsg> {
  switch (msg.type) {
    case 'UpdateInput':
      return { model: { ...model, input: msg.text } };

    case 'AddTodo':
      const newTodo = {
        id: Date.now().toString(),
        text: model.input,
        done: false
      };
      const newModel = {
        ...model,
        todos: [...model.todos, newTodo],
        input: ''
      };
      return {
        model: newModel,
        effects: [saveTodosEffect(newModel.todos)]
      };

    case 'ToggleTodo':
      const updatedModel = {
        ...model,
        todos: model.todos.map(todo =>
          todo.id === msg.id ? { ...todo, done: !todo.done } : todo
        )
      };
      return {
        model: updatedModel,
        effects: [saveTodosEffect(updatedModel.todos)]
      };

    case 'LoadTodos':
      return {
        model,
        effects: [loadTodosEffect()]
      };

    case 'TodosLoaded':
      return {
        model: { ...model, todos: msg.todos }
      };

    default:
      return { model };
  }
}

function saveTodosEffect(todos: any[]): Effect<TodoMsg> {
  return {
    type: 'storage',
    execute: async () => {
      localStorage.setItem('todos', JSON.stringify(todos));
      return { type: 'SaveTodos' } as TodoMsg;
    }
  };
}

function loadTodosEffect(): Effect<TodoMsg> {
  return {
    type: 'storage',
    execute: async () => {
      const stored = localStorage.getItem('todos');
      const todos = stored ? JSON.parse(stored) : [];
      return { type: 'TodosLoaded', todos };
    }
  };
}
Note: MVU pattern ensures predictable state management by making all state transitions explicit and testable. Update functions are pure, making them easy to test and reason about.

20.3 Unidirectional Data Flow Implementation

Implement strict unidirectional data flow to ensure predictable state updates and easier debugging.

Flow Step Description Data Direction
1. User Action User interacts with UI UI → Action
2. Dispatch Action Action dispatched to store Action → Store
3. Update State Store updates state immutably Store → New State
4. Notify Components Components receive new state State → Components
5. Re-render Components re-render with new state Components → UI

Example: Enforcing Unidirectional Flow with Custom Hook

// useUnidirectionalState.ts - Enforce one-way data flow
import { useState, useCallback, useRef } from 'react';

type Action<T> = {
  type: string;
  payload?: any;
};

type Reducer<T> = (state: T, action: Action<T>) => T;

type Middleware<T> = (
  action: Action<T>,
  state: T,
  next: (action: Action<T>) => void
) => void;

export function useUnidirectionalState<T>(
  initialState: T,
  reducer: Reducer<T>,
  middlewares: Middleware<T>[] = []
) {
  const [state, setState] = useState(initialState);
  const stateRef = useRef(state);
  const listenersRef = useRef<Set<(state: T) => void>>(new Set());

  // Update ref on every render
  stateRef.current = state;

  const dispatch = useCallback((action: Action<T>) => {
    console.log('📤 Action dispatched:', action.type, action.payload);

    // Apply middlewares
    let index = 0;
    const runMiddleware = (currentAction: Action<T>) => {
      if (index < middlewares.length) {
        const middleware = middlewares[index++];
        middleware(currentAction, stateRef.current, runMiddleware);
      } else {
        // All middlewares done, update state
        const currentState = stateRef.current;
        const newState = reducer(currentState, currentAction);

        if (newState !== currentState) {
          console.log('📥 State updated:', { from: currentState, to: newState });
          setState(newState);

          // Notify listeners
          listenersRef.current.forEach(listener => listener(newState));
        }
      }
    };

    runMiddleware(action);
  }, [reducer, middlewares]);

  const subscribe = useCallback((listener: (state: T) => void) => {
    listenersRef.current.add(listener);
    return () => listenersRef.current.delete(listener);
  }, []);

  // Prevent direct state mutation
  const protectedState = Object.freeze({ ...state });

  return {
    state: protectedState,
    dispatch,
    subscribe
  };
}

// Logging middleware
function loggingMiddleware<T>(
  action: Action<T>,
  state: T,
  next: (action: Action<T>) => void
) {
  console.group(`Action: ${action.type}`);
  console.log('Payload:', action.payload);
  console.log('Current State:', state);
  next(action);
  console.groupEnd();
}

// Validation middleware
function validationMiddleware<T>(
  action: Action<T>,
  state: T,
  next: (action: Action<T>) => void
) {
  // Example: Validate action structure
  if (!action.type) {
    console.error('Action must have a type');
    return;
  }
  next(action);
}

// Usage
type CounterState = { count: number; history: number[] };

function counterReducer(state: CounterState, action: Action<CounterState>) {
  switch (action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1,
        history: [...state.history, state.count + 1]
      };
    case 'DECREMENT':
      return {
        count: state.count - 1,
        history: [...state.history, state.count - 1]
      };
    case 'RESET':
      return { count: 0, history: [0] };
    default:
      return state;
  }
}

function Counter() {
  const { state, dispatch } = useUnidirectionalState(
    { count: 0, history: [0] },
    counterReducer,
    [loggingMiddleware, validationMiddleware]
  );

  // ❌ This will throw an error - state is frozen!
  // state.count = 5;

  // ✅ Must use dispatch for state changes
  return (
    <div>
      <h1>Count: {state.count}</h1>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      
      <div>History: {state.history.join(', ')}</div>
    </div>
  );
}
Note: Unidirectional data flow makes state changes predictable and traceable. Use tools like Redux DevTools to visualize the flow and debug state transitions.

20.4 State Composition and Module Boundaries

Structure state into composable modules with clear boundaries to improve maintainability and scalability.

Composition Pattern Description Use Case
Domain slicing Organize state by business domain Users, Products, Orders modules
Feature slicing Organize by feature/page Auth, Dashboard, Settings features
Layer slicing Separate by responsibility layer UI state, Domain state, API state
Combiner functions Merge multiple state slices Root reducer composition
Shared state Cross-module shared state Theme, locale, auth tokens

Example: Domain-Based State Composition

// Domain-based module structure
// src/domains/user/userSlice.ts
import { createSlice } from '@reduxjs/toolkit';

const userSlice = createSlice({
  name: 'user',
  initialState: {
    profile: null,
    preferences: {},
    loading: false
  },
  reducers: {
    setProfile: (state, action) => {
      state.profile = action.payload;
    },
    updatePreferences: (state, action) => {
      state.preferences = { ...state.preferences, ...action.payload };
    }
  }
});

export const userActions = userSlice.actions;
export const userReducer = userSlice.reducer;

// Selectors for this domain
export const userSelectors = {
  selectProfile: (state: RootState) => state.user.profile,
  selectPreferences: (state: RootState) => state.user.preferences,
  selectIsLoading: (state: RootState) => state.user.loading
};

// src/domains/product/productSlice.ts
const productSlice = createSlice({
  name: 'product',
  initialState: {
    items: [],
    selectedId: null,
    filters: {}
  },
  reducers: {
    setProducts: (state, action) => {
      state.items = action.payload;
    },
    selectProduct: (state, action) => {
      state.selectedId = action.payload;
    },
    setFilters: (state, action) => {
      state.filters = action.payload;
    }
  }
});

export const productActions = productSlice.actions;
export const productReducer = productSlice.reducer;

export const productSelectors = {
  selectAllProducts: (state: RootState) => state.product.items,
  selectSelectedProduct: (state: RootState) =>
    state.product.items.find(p => p.id === state.product.selectedId),
  selectFilters: (state: RootState) => state.product.filters
};

// src/domains/cart/cartSlice.ts
const cartSlice = createSlice({
  name: 'cart',
  initialState: {
    items: [],
    total: 0
  },
  reducers: {
    addItem: (state, action) => {
      state.items.push(action.payload);
      state.total = state.items.reduce((sum, item) => sum + item.price, 0);
    },
    removeItem: (state, action) => {
      state.items = state.items.filter(item => item.id !== action.payload);
      state.total = state.items.reduce((sum, item) => sum + item.price, 0);
    },
    clearCart: (state) => {
      state.items = [];
      state.total = 0;
    }
  }
});

export const cartActions = cartSlice.actions;
export const cartReducer = cartSlice.reducer;

export const cartSelectors = {
  selectCartItems: (state: RootState) => state.cart.items,
  selectCartTotal: (state: RootState) => state.cart.total,
  selectCartCount: (state: RootState) => state.cart.items.length
};

// src/store/rootReducer.ts - Compose all domain reducers
import { combineReducers } from '@reduxjs/toolkit';
import { userReducer } from '../domains/user/userSlice';
import { productReducer } from '../domains/product/productSlice';
import { cartReducer } from '../domains/cart/cartSlice';

export const rootReducer = combineReducers({
  user: userReducer,
  product: productReducer,
  cart: cartReducer
});

export type RootState = ReturnType<typeof rootReducer>;

// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { rootReducer } from './rootReducer';

export const store = configureStore({
  reducer: rootReducer
});

export type AppDispatch = typeof store.dispatch;

Example: Feature-Based State Modules with Context

// Feature-based organization using Context API
// src/features/auth/AuthContext.tsx
import { createContext, useContext, useState } from 'react';

type AuthState = {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
};

const AuthContext = createContext<AuthState & AuthActions | null>(null);

export function AuthProvider({ children }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    token: null,
    isAuthenticated: false
  });

  const actions = {
    login: async (credentials) => {
      const { user, token } = await api.login(credentials);
      setState({ user, token, isAuthenticated: true });
    },
    logout: () => {
      setState({ user: null, token: null, isAuthenticated: false });
    }
  };

  return (
    <AuthContext.Provider value={{ ...state, ...actions }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be within AuthProvider');
  return context;
};

// src/features/dashboard/DashboardContext.tsx
const DashboardContext = createContext(null);

export function DashboardProvider({ children }) {
  const [state, setState] = useState({
    widgets: [],
    layout: 'grid',
    filters: {}
  });

  const actions = {
    addWidget: (widget) => {
      setState(s => ({ ...s, widgets: [...s.widgets, widget] }));
    },
    setLayout: (layout) => {
      setState(s => ({ ...s, layout }));
    }
  };

  return (
    <DashboardContext.Provider value={{ ...state, ...actions }}>
      {children}
    </DashboardContext.Provider>
  );
}

export const useDashboard = () => useContext(DashboardContext);

// src/App.tsx - Compose feature providers
export function App() {
  return (
    <AuthProvider>
      <DashboardProvider>
        <Router>
          <Routes />
        </Router>
      </DashboardProvider>
    </AuthProvider>
  );
}
Note: Define clear boundaries between modules. Each module should own its state and expose only necessary selectors and actions. Avoid tight coupling between modules.

20.5 State Scaling Strategies for Large Applications

Implement strategies to scale state management as applications grow in size and complexity.

Scaling Strategy Description Benefit
Code splitting Load state modules on demand Reduce initial bundle size
Lazy reducers Dynamically inject reducers Load state logic when needed
State normalization Flat, normalized data structures Efficient updates, no duplication
Memoized selectors Cache derived state calculations Prevent unnecessary re-renders
State pagination Load data in chunks Handle large datasets efficiently
Virtual state Windowing for large lists Render only visible items

Example: Dynamic Reducer Injection

// store.ts - Store with dynamic reducer injection
import { configureStore, combineReducers } from '@reduxjs/toolkit';

const staticReducers = {
  auth: authReducer,
  ui: uiReducer
};

export function createStore() {
  const asyncReducers = {};

  const store = configureStore({
    reducer: createReducer(asyncReducers)
  });

  // Add method to inject reducers
  store.injectReducer = (key, reducer) => {
    if (asyncReducers[key]) return; // Already injected

    asyncReducers[key] = reducer;
    store.replaceReducer(createReducer(asyncReducers));
  };

  return store;
}

function createReducer(asyncReducers) {
  return combineReducers({
    ...staticReducers,
    ...asyncReducers
  });
}

export const store = createStore();

// Feature module - Lazy loaded
// src/features/analytics/index.tsx
import { lazy, useEffect } from 'react';
import { analyticsReducer } from './analyticsSlice';
import { store } from '../../store';

// Inject reducer when module loads
export function Analytics() {
  useEffect(() => {
    store.injectReducer('analytics', analyticsReducer);
  }, []);

  return <AnalyticsDashboard />;
}

// Route configuration with code splitting
const Analytics = lazy(() => import('./features/analytics'));

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/analytics" element={
          <Suspense fallback={<Loading />}>
            <Analytics />
          </Suspense>
        } />
      </Routes>
    </Router>
  );
}

Example: State Normalization for Scalability

// Normalized state structure
// ❌ BAD: Nested, denormalized data
type BadState = {
  posts: Array<{
    id: string;
    title: string;
    author: {
      id: string;
      name: string;
      email: string;
    };
    comments: Array<{
      id: string;
      text: string;
      author: {
        id: string;
        name: string;
      };
    }>;
  }>;
};

// ✅ GOOD: Normalized, flat structure
type GoodState = {
  posts: {
    byId: Record<string, Post>;
    allIds: string[];
  };
  users: {
    byId: Record<string, User>;
    allIds: string[];
  };
  comments: {
    byId: Record<string, Comment>;
    allIds: string[];
  };
};

// normalizr library usage
import { normalize, schema } from 'normalizr';

// Define schemas
const userSchema = new schema.Entity('users');
const commentSchema = new schema.Entity('comments', {
  author: userSchema
});
const postSchema = new schema.Entity('posts', {
  author: userSchema,
  comments: [commentSchema]
});

// Normalize API response
const apiResponse = {
  id: '1',
  title: 'Post 1',
  author: { id: 'u1', name: 'Alice' },
  comments: [
    { id: 'c1', text: 'Great!', author: { id: 'u2', name: 'Bob' } }
  ]
};

const normalized = normalize(apiResponse, postSchema);
// Result:
// {
//   entities: {
//     posts: { '1': { id: '1', title: 'Post 1', author: 'u1', comments: ['c1'] } },
//     users: { 'u1': { id: 'u1', name: 'Alice' }, 'u2': { id: 'u2', name: 'Bob' } },
//     comments: { 'c1': { id: 'c1', text: 'Great!', author: 'u2' } }
//   },
//   result: '1'
// }

// Redux slice with normalized data
const postsSlice = createSlice({
  name: 'posts',
  initialState: {
    byId: {},
    allIds: []
  },
  reducers: {
    addPosts: (state, action) => {
      const normalized = normalize(action.payload, [postSchema]);
      
      // Merge normalized entities
      Object.entries(normalized.entities.posts || {}).forEach(([id, post]) => {
        state.byId[id] = post;
        if (!state.allIds.includes(id)) {
          state.allIds.push(id);
        }
      });
    },
    updatePost: (state, action) => {
      const { id, changes } = action.payload;
      if (state.byId[id]) {
        state.byId[id] = { ...state.byId[id], ...changes };
      }
    }
  }
});

// Memoized selectors for derived data
import { createSelector } from '@reduxjs/toolkit';

const selectPostsById = (state) => state.posts.byId;
const selectUsersById = (state) => state.users.byId;
const selectCommentsById = (state) => state.comments.byId;

// Denormalize for display (cached with createSelector)
export const selectPostWithDetails = createSelector(
  [selectPostsById, selectUsersById, selectCommentsById, (_, postId) => postId],
  (posts, users, comments, postId) => {
    const post = posts[postId];
    if (!post) return null;

    return {
      ...post,
      author: users[post.author],
      comments: post.comments.map(commentId => ({
        ...comments[commentId],
        author: users[comments[commentId].author]
      }))
    };
  }
);

Example: Virtual Scrolling for Large State

// Virtual scrolling with react-window
import { FixedSizeList } from 'react-window';
import { useSelector } from 'react-redux';

function LargeList() {
  // Only store IDs in state, not full objects
  const itemIds = useSelector(state => state.items.allIds); // 10,000+ items
  const itemsById = useSelector(state => state.items.byId);

  // Render only visible rows
  const Row = ({ index, style }) => {
    const itemId = itemIds[index];
    const item = itemsById[itemId];

    return (
      <div style={style}>
        {item.name}
      </div>
    );
  };

  return (
    <FixedSizeList
      height={600}
      itemCount={itemIds.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
Note: For large applications, combine multiple scaling strategies: normalize data, use memoized selectors, implement code splitting, and virtualize large lists for optimal performance.

20.6 Micro-frontend State Management Strategies

Manage state across independent micro-frontends while maintaining isolation and enabling communication.

Strategy Description Use Case
Isolated state Each micro-app owns its state Independent features
Shared state bus Event bus for cross-app communication Loosely coupled apps
Global state shell Host app manages shared state Auth, theme, navigation
Module federation Share state libraries via webpack Consistent state management
Custom events Browser custom events for messaging Framework-agnostic communication

Example: Shared State Bus for Micro-frontends

// stateBus.ts - Central event bus for micro-frontends
class StateBus {
  private listeners = new Map<string, Set<Function>>();

  // Subscribe to state changes
  subscribe(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(callback);

    return () => {
      this.listeners.get(event)?.delete(callback);
    };
  }

  // Publish state changes
  publish(event: string, data: any) {
    console.log(`📢 Event published: ${event}`, data);
    this.listeners.get(event)?.forEach(callback => callback(data));
  }

  // Get current value (optional persistence)
  private storage = new Map<string, any>();

  getState(key: string) {
    return this.storage.get(key);
  }

  setState(key: string, value: any) {
    this.storage.set(key, value);
    this.publish(key, value);
  }
}

// Global singleton
export const stateBus = new StateBus();

// Micro-frontend A: Auth app
// apps/auth/src/AuthApp.tsx
import { stateBus } from '@shared/state-bus';

export function AuthApp() {
  const [user, setUser] = useState(null);

  const handleLogin = async (credentials) => {
    const userData = await login(credentials);
    setUser(userData);
    
    // Publish auth state to other micro-apps
    stateBus.setState('auth.user', userData);
    stateBus.publish('auth.login', { user: userData });
  };

  const handleLogout = () => {
    setUser(null);
    stateBus.setState('auth.user', null);
    stateBus.publish('auth.logout', {});
  };

  return <LoginForm onLogin={handleLogin} />;
}

// Micro-frontend B: Dashboard app
// apps/dashboard/src/DashboardApp.tsx
import { stateBus } from '@shared/state-bus';

export function DashboardApp() {
  const [user, setUser] = useState(stateBus.getState('auth.user'));

  useEffect(() => {
    // Subscribe to auth events from other micro-apps
    const unsubscribeLogin = stateBus.subscribe('auth.login', (data) => {
      console.log('Dashboard: User logged in', data.user);
      setUser(data.user);
    });

    const unsubscribeLogout = stateBus.subscribe('auth.logout', () => {
      console.log('Dashboard: User logged out');
      setUser(null);
    });

    return () => {
      unsubscribeLogin();
      unsubscribeLogout();
    };
  }, []);

  if (!user) return <div>Please log in</div>;

  return <div>Welcome, {user.name}</div>;
}

// Micro-frontend C: Shopping cart app
// apps/cart/src/CartApp.tsx
export function CartApp() {
  const [user, setUser] = useState(null);
  const [items, setItems] = useState([]);

  useEffect(() => {
    // Listen for auth changes
    const unsubscribe = stateBus.subscribe('auth.user', (userData) => {
      setUser(userData);
      
      if (userData) {
        // Load cart for logged-in user
        loadCart(userData.id).then(setItems);
      } else {
        // Clear cart on logout
        setItems([]);
      }
    });

    return unsubscribe;
  }, []);

  const handleAddItem = (item) => {
    setItems(prev => [...prev, item]);
    
    // Publish cart update
    stateBus.publish('cart.updated', { items: [...items, item] });
  };

  return <CartList items={items} onAddItem={handleAddItem} />;
}

Example: Custom Events for Framework-Agnostic Communication

// microFrontendEvents.ts - Browser custom events
export const MFEvents = {
  // Dispatch custom event
  dispatch: (eventName: string, detail: any) => {
    const event = new CustomEvent(`mf:${eventName}`, {
      detail,
      bubbles: true,
      composed: true
    });
    window.dispatchEvent(event);
  },

  // Listen to custom event
  listen: (eventName: string, handler: (detail: any) => void) => {
    const listener = (event: CustomEvent) => handler(event.detail);
    window.addEventListener(`mf:${eventName}`, listener as EventListener);
    
    return () => {
      window.removeEventListener(`mf:${eventName}`, listener as EventListener);
    };
  }
};

// React micro-frontend
function ReactMicroApp() {
  const [sharedData, setSharedData] = useState(null);

  useEffect(() => {
    const unlisten = MFEvents.listen('data-updated', (detail) => {
      console.log('React app received:', detail);
      setSharedData(detail);
    });

    return unlisten;
  }, []);

  const handleUpdate = () => {
    MFEvents.dispatch('data-updated', { from: 'react', data: 'Hello' });
  };

  return <button onClick={handleUpdate}>Update</button>;
}

// Vue micro-frontend (different framework)
export default {
  data() {
    return { sharedData: null };
  },
  mounted() {
    this.unlisten = MFEvents.listen('data-updated', (detail) => {
      console.log('Vue app received:', detail);
      this.sharedData = detail;
    });
  },
  beforeUnmount() {
    this.unlisten();
  },
  methods: {
    handleUpdate() {
      MFEvents.dispatch('data-updated', { from: 'vue', data: 'Bonjour' });
    }
  }
};

Example: Module Federation for Shared State

// webpack.config.js - Host app
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        auth: 'auth@http://localhost:3001/remoteEntry.js',
        dashboard: 'dashboard@http://localhost:3002/remoteEntry.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
        '@reduxjs/toolkit': { singleton: true },
        'react-redux': { singleton: true }
      }
    })
  ]
};

// Host app store - shared across micro-apps
// host/src/store/index.ts
import { configureStore } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: {
    shared: sharedReducer, // Shared global state
    // Micro-apps will inject their reducers dynamically
  }
});

// Expose store to micro-apps
window.__SHARED_STORE__ = store;

// Remote micro-app
// auth/src/bootstrap.tsx
import { Provider } from 'react-redux';

function AuthMicroApp() {
  // Use shared store from host
  const store = window.__SHARED_STORE__;

  // Inject auth reducer
  useEffect(() => {
    store.injectReducer('auth', authReducer);
  }, []);

  return (
    <Provider store={store}>
      <AuthApp />
    </Provider>
  );
}
Warning: Micro-frontend state sharing should be minimal. Prefer isolated state with event-based communication to maintain independence and avoid tight coupling between apps.

Section 20 Key Takeaways

  • Flux architecture - Unidirectional flow with actions, dispatcher, stores, and views for predictable state management
  • MVU pattern - Separate Model, View, and Update functions; pure update functions for testable state transitions
  • Unidirectional flow - Enforce one-way data flow with middleware, immutable state updates, and action dispatching
  • State composition - Organize by domain/feature, use combineReducers, maintain clear module boundaries
  • Scaling strategies - Normalize data, use code splitting, lazy reducers, memoized selectors, and virtual scrolling
  • Micro-frontends - Isolate state per app, use event bus for communication, share libraries via module federation