Advanced State Patterns and Techniques

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.

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.

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.

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.

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.

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