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