Master complex state management with useReducer, including advanced patterns, middleware concepts, and scalable reducer architectures for enterprise applications.
useReducer is React's most powerful state management hook, providing a predictable way to manage complex state logic that would be difficult to handle with useState. While useState works well for simple state values, useReducer shines when you have complex state transitions, multiple sub-values, or when the next state depends on the previous one.
Think of useReducer as bringing Redux-like patterns directly into React components, but with better integration and simpler setup. It's particularly valuable for form handling, data fetching with complex loading states, undo/redo functionality, and any scenario where state updates follow predictable patterns.
In this comprehensive lesson, you'll learn advanced useReducer patterns that are essential for building production-ready React applications with sophisticated state management requirements.
useReducer provides a more structured approach to state management compared to useState, especially when dealing with complex state logic.
Basic useReducer Pattern:
1const [state, dispatch] = useReducer(reducer, initialState);Advanced Reducer with Action Types:
1// Action types for type safety and maintainability
2const ACTIONS = {
3 FETCH_START: 'FETCH_START',
4 FETCH_SUCCESS: 'FETCH_SUCCESS',
5 FETCH_ERROR: 'FETCH_ERROR',
6 UPDATE_FILTER: 'UPDATE_FILTER',
7 RESET_STATE: 'RESET_STATE'
8};
9
10// Complex state structure
11const initialState = {
12 data: [],
13 loading: false,
14 error: null,
15 filters: {
16 search: '',
17 category: 'all',
18 sortBy: 'name'
19 },
20 pagination: {
21 page: 1,
22 limit: 10,
23 total: 0
24 }
25};
26
27// Advanced reducer with nested state updates
28function dataReducer(state, action) {
29 switch (action.type) {
30 case ACTIONS.FETCH_START:
31 return {
32 ...state,
33 loading: true,
34 error: null
35 };
36
37 case ACTIONS.FETCH_SUCCESS:
38 return {
39 ...state,
40 loading: false,
41 data: action.payload.data,
42 pagination: {
43 ...state.pagination,
44 total: action.payload.total
45 }
46 };
47
48 case ACTIONS.UPDATE_FILTER:
49 return {
50 ...state,
51 filters: {
52 ...state.filters,
53 [action.payload.key]: action.payload.value
54 },
55 pagination: {
56 ...state.pagination,
57 page: 1 // Reset to first page when filtering
58 }
59 };
60
61 default:
62 return state;
63 }
64}Reducer Composition Pattern:
1// Separate reducers for different state slices
2function filtersReducer(state, action) {
3 switch (action.type) {
4 case 'UPDATE_SEARCH':
5 return { ...state, search: action.payload };
6 case 'UPDATE_CATEGORY':
7 return { ...state, category: action.payload };
8 case 'RESET_FILTERS':
9 return { search: '', category: 'all', sortBy: 'name' };
10 default:
11 return state;
12 }
13}
14
15function paginationReducer(state, action) {
16 switch (action.type) {
17 case 'NEXT_PAGE':
18 return { ...state, page: state.page + 1 };
19 case 'PREV_PAGE':
20 return { ...state, page: Math.max(1, state.page - 1) };
21 case 'SET_PAGE':
22 return { ...state, page: action.payload };
23 default:
24 return state;
25 }
26}
27
28// Combined reducer
29function combinedReducer(state, action) {
30 return {
31 ...state,
32 filters: filtersReducer(state.filters, action),
33 pagination: paginationReducer(state.pagination, action)
34 };
35}Benefits of Advanced useReducer:
TypeScript makes useReducer incredibly powerful by providing compile-time safety for actions and state updates.
Type-Safe Action Definitions:
1// Define all possible action types
2type Action =
3 | { type: 'FETCH_START' }
4 | { type: 'FETCH_SUCCESS'; payload: { data: User[]; total: number } }
5 | { type: 'FETCH_ERROR'; payload: string }
6 | { type: 'UPDATE_FILTER'; payload: { key: keyof Filters; value: string } }
7 | { type: 'SET_PAGE'; payload: number }
8 | { type: 'RESET_STATE' };
9
10// State interface
11interface State {
12 data: User[];
13 loading: boolean;
14 error: string | null;
15 filters: Filters;
16 pagination: Pagination;
17}
18
19interface User {
20 id: string;
21 name: string;
22 email: string;
23 role: 'admin' | 'user' | 'moderator';
24}
25
26interface Filters {
27 search: string;
28 category: string;
29 sortBy: 'name' | 'email' | 'created';
30}
31
32interface Pagination {
33 page: number;
34 limit: number;
35 total: number;
36}Type-Safe Reducer Implementation:
1function userReducer(state: State, action: Action): State {
2 switch (action.type) {
3 case 'FETCH_START':
4 return {
5 ...state,
6 loading: true,
7 error: null
8 };
9
10 case 'FETCH_SUCCESS':
11 return {
12 ...state,
13 loading: false,
14 data: action.payload.data, // TypeScript knows this exists
15 pagination: {
16 ...state.pagination,
17 total: action.payload.total
18 }
19 };
20
21 case 'FETCH_ERROR':
22 return {
23 ...state,
24 loading: false,
25 error: action.payload // TypeScript enforces string type
26 };
27
28 case 'UPDATE_FILTER':
29 return {
30 ...state,
31 filters: {
32 ...state.filters,
33 [action.payload.key]: action.payload.value
34 }
35 };
36
37 default:
38 // TypeScript ensures all cases are handled
39 const _exhaustiveCheck: never = action;
40 return state;
41 }
42}Action Creator Functions:
1// Type-safe action creators
2const actionCreators = {
3 fetchStart: (): Action => ({ type: 'FETCH_START' }),
4
5 fetchSuccess: (data: User[], total: number): Action => ({
6 type: 'FETCH_SUCCESS',
7 payload: { data, total }
8 }),
9
10 fetchError: (error: string): Action => ({
11 type: 'FETCH_ERROR',
12 payload: error
13 }),
14
15 updateFilter: <K extends keyof Filters>(key: K, value: Filters[K]): Action => ({
16 type: 'UPDATE_FILTER',
17 payload: { key, value }
18 }),
19
20 setPage: (page: number): Action => ({
21 type: 'SET_PAGE',
22 payload: page
23 })
24};
25
26// Usage with full type safety
27dispatch(actionCreators.fetchSuccess(users, 100));
28dispatch(actionCreators.updateFilter('search', 'john'));Custom Hook with TypeScript:
1function useUserManagement() {
2 const [state, dispatch] = useReducer(userReducer, initialState);
3
4 const actions = useMemo(() => ({
5 fetchUsers: async (filters: Filters) => {
6 dispatch(actionCreators.fetchStart());
7 try {
8 const response = await fetchUsers(filters);
9 dispatch(actionCreators.fetchSuccess(response.data, response.total));
10 } catch (error) {
11 dispatch(actionCreators.fetchError(error.message));
12 }
13 },
14
15 updateFilter: <K extends keyof Filters>(key: K, value: Filters[K]) => {
16 dispatch(actionCreators.updateFilter(key, value));
17 },
18
19 setPage: (page: number) => {
20 dispatch(actionCreators.setPage(page));
21 }
22 }), []);
23
24 return { state, actions };
25}This TypeScript integration provides compile-time safety, better IDE support, and prevents common runtime errors in complex state management scenarios.
Advanced useReducer patterns can implement middleware-like functionality and state normalization for complex data structures.
Middleware Pattern with useReducer:
1// Middleware function that wraps the reducer
2function withLogging(reducer) {
3 return (state, action) => {
4 console.group(`Action: ${action.type}`);
5 console.log('Previous State:', state);
6 console.log('Action:', action);
7
8 const nextState = reducer(state, action);
9
10 console.log('Next State:', nextState);
11 console.groupEnd();
12
13 return nextState;
14 };
15}
16
17// Middleware for persistence
18function withPersistence(reducer, storageKey) {
19 return (state, action) => {
20 const nextState = reducer(state, action);
21
22 // Persist to localStorage after each action
23 try {
24 localStorage.setItem(storageKey, JSON.stringify(nextState));
25 } catch (error) {
26 console.error('Failed to persist state:', error);
27 }
28
29 return nextState;
30 };
31}
32
33// Middleware for analytics
34function withAnalytics(reducer) {
35 return (state, action) => {
36 // Track user actions
37 if (action.type !== 'INTERNAL_UPDATE') {
38 analytics.track('user_action', {
39 action: action.type,
40 timestamp: Date.now(),
41 payload: action.payload
42 });
43 }
44
45 return reducer(state, action);
46 };
47}
48
49// Compose multiple middleware
50function composeMiddleware(...middlewares) {
51 return (reducer) => {
52 return middlewares.reduce((acc, middleware) => middleware(acc), reducer);
53 };
54}
55
56// Usage
57const enhancedReducer = composeMiddleware(
58 withLogging,
59 withPersistence('app-state'),
60 withAnalytics
61)(baseReducer);State Normalization Pattern:
1// Normalized state structure
2const initialState = {
3 entities: {
4 users: {},
5 posts: {},
6 comments: {}
7 },
8 collections: {
9 usersList: [],
10 postsById: {},
11 featuredPosts: []
12 },
13 ui: {
14 loading: false,
15 selectedUser: null,
16 currentPage: 1
17 }
18};
19
20// Normalization utilities
21function normalizeUsers(users) {
22 const entities = {};
23 const ids = [];
24
25 users.forEach(user => {
26 entities[user.id] = user;
27 ids.push(user.id);
28 });
29
30 return { entities, ids };
31}
32
33// Reducer with normalization
34function normalizedReducer(state, action) {
35 switch (action.type) {
36 case 'LOAD_USERS_SUCCESS': {
37 const { entities, ids } = normalizeUsers(action.payload);
38 return {
39 ...state,
40 entities: {
41 ...state.entities,
42 users: { ...state.entities.users, ...entities }
43 },
44 collections: {
45 ...state.collections,
46 usersList: ids
47 }
48 };
49 }
50
51 case 'UPDATE_USER': {
52 const { id, updates } = action.payload;
53 return {
54 ...state,
55 entities: {
56 ...state.entities,
57 users: {
58 ...state.entities.users,
59 [id]: {
60 ...state.entities.users[id],
61 ...updates
62 }
63 }
64 }
65 };
66 }
67
68 case 'DELETE_USER': {
69 const { id } = action.payload;
70 const { [id]: deleted, ...remainingUsers } = state.entities.users;
71
72 return {
73 ...state,
74 entities: {
75 ...state.entities,
76 users: remainingUsers
77 },
78 collections: {
79 ...state.collections,
80 usersList: state.collections.usersList.filter(userId => userId !== id)
81 }
82 };
83 }
84
85 default:
86 return state;
87 }
88}
89
90// Selectors for denormalized data access
91const selectors = {
92 getAllUsers: (state) =>
93 state.collections.usersList.map(id => state.entities.users[id]),
94
95 getUserById: (state, id) =>
96 state.entities.users[id],
97
98 getActiveUsers: (state) =>
99 state.collections.usersList
100 .map(id => state.entities.users[id])
101 .filter(user => user.isActive)
102};Undo/Redo Pattern:
1function withUndoRedo(reducer) {
2 const initialUndoState = {
3 past: [],
4 present: undefined,
5 future: []
6 };
7
8 return (state = initialUndoState, action) => {
9 const { past, present, future } = state;
10
11 switch (action.type) {
12 case 'UNDO':
13 if (past.length === 0) return state;
14
15 return {
16 past: past.slice(0, past.length - 1),
17 present: past[past.length - 1],
18 future: [present, ...future]
19 };
20
21 case 'REDO':
22 if (future.length === 0) return state;
23
24 return {
25 past: [...past, present],
26 present: future[0],
27 future: future.slice(1)
28 };
29
30 default:
31 if (present === undefined) {
32 return {
33 past: [],
34 present: reducer(undefined, action),
35 future: []
36 };
37 }
38
39 const newPresent = reducer(present, action);
40
41 if (newPresent === present) {
42 return state;
43 }
44
45 return {
46 past: [...past, present],
47 present: newPresent,
48 future: []
49 };
50 }
51 };
52}These patterns enable sophisticated state management that rivals dedicated state management libraries while staying within React's ecosystem.
A comprehensive task management system demonstrating advanced useReducer patterns including state normalization, middleware, TypeScript integration, and complex state transitions.
1import React, { useReducer, useContext, createContext, useMemo, useCallback, useEffect } from 'react';
2
3// =====================================================================
4// TypeScript Types and Interfaces
5// =====================================================================
6
7interface Task {
8 id: string;
9 title: string;
10 description: string;
11 status: 'todo' | 'in-progress' | 'completed';
12 priority: 'low' | 'medium' | 'high';
13 category: string;
14 assignedTo: string;
15 dueDate: string;
16 createdAt: string;
17 updatedAt: string;
18 tags: string[];
19 estimatedHours: number;
20 actualHours: number;
21}
22
23interface User {
24 id: string;
25 name: string;
26 email: string;
27 role: 'admin' | 'user' | 'manager';
28 avatar: string;
29}
30
31interface State {
32 entities: {
33 tasks: Record<string, Task>;
34 users: Record<string, User>;
35 };
36 collections: {
37 tasksByStatus: Record<string, string[]>;
38 tasksByUser: Record<string, string[]>;
39 tasksByCategory: Record<string, string[]>;
40 };
41 ui: {
42 loading: boolean;
43 error: string | null;
44 selectedTask: string | null;
45 filters: {
46 status: string;
47 assignee: string;
48 category: string;
49 priority: string;
50 search: string;
51 };
52 sortBy: 'dueDate' | 'priority' | 'createdAt' | 'title';
53 sortOrder: 'asc' | 'desc';
54 };
55 history: {
56 past: State[];
57 canUndo: boolean;
58 canRedo: boolean;
59 };
60}
61
62// Action Types
63type Action =
64 | { type: 'LOAD_TASKS_START' }
65 | { type: 'LOAD_TASKS_SUCCESS'; payload: { tasks: Task[]; users: User[] } }
66 | { type: 'LOAD_TASKS_ERROR'; payload: string }
67 | { type: 'ADD_TASK'; payload: Task }
68 | { type: 'UPDATE_TASK'; payload: { id: string; updates: Partial<Task> } }
69 | { type: 'DELETE_TASK'; payload: string }
70 | { type: 'MOVE_TASK'; payload: { id: string; newStatus: Task['status'] } }
71 | { type: 'ASSIGN_TASK'; payload: { id: string; userId: string } }
72 | { type: 'UPDATE_FILTER'; payload: { key: keyof State['ui']['filters']; value: string } }
73 | { type: 'SET_SORT'; payload: { sortBy: State['ui']['sortBy']; sortOrder: State['ui']['sortOrder'] } }
74 | { type: 'SELECT_TASK'; payload: string | null }
75 | { type: 'BULK_UPDATE_TASKS'; payload: { taskIds: string[]; updates: Partial<Task> } }
76 | { type: 'UNDO' }
77 | { type: 'REDO' }
78 | { type: 'CLEAR_HISTORY' };
79
80// =====================================================================
81// Initial State
82// =====================================================================
83
84const initialState: State = {
85 entities: {
86 tasks: {},
87 users: {}
88 },
89 collections: {
90 tasksByStatus: {
91 'todo': [],
92 'in-progress': [],
93 'completed': []
94 },
95 tasksByUser: {},
96 tasksByCategory: {}
97 },
98 ui: {
99 loading: false,
100 error: null,
101 selectedTask: null,
102 filters: {
103 status: 'all',
104 assignee: 'all',
105 category: 'all',
106 priority: 'all',
107 search: ''
108 },
109 sortBy: 'dueDate',
110 sortOrder: 'asc'
111 },
112 history: {
113 past: [],
114 canUndo: false,
115 canRedo: false
116 }
117};
118
119// =====================================================================
120// Normalization Utilities
121// =====================================================================
122
123function normalizeTasks(tasks: Task[]): {
124 entities: Record<string, Task>;
125 byStatus: Record<string, string[]>;
126 byUser: Record<string, string[]>;
127 byCategory: Record<string, string[]>;
128} {
129 const entities: Record<string, Task> = {};
130 const byStatus: Record<string, string[]> = {
131 'todo': [],
132 'in-progress': [],
133 'completed': []
134 };
135 const byUser: Record<string, string[]> = {};
136 const byCategory: Record<string, string[]> = {};
137
138 tasks.forEach(task => {
139 entities[task.id] = task;
140
141 // Group by status
142 byStatus[task.status].push(task.id);
143
144 // Group by user
145 if (!byUser[task.assignedTo]) {
146 byUser[task.assignedTo] = [];
147 }
148 byUser[task.assignedTo].push(task.id);
149
150 // Group by category
151 if (!byCategory[task.category]) {
152 byCategory[task.category] = [];
153 }
154 byCategory[task.category].push(task.id);
155 });
156
157 return { entities, byStatus, byUser, byCategory };
158}
159
160function normalizeUsers(users: User[]): Record<string, User> {
161 return users.reduce((acc, user) => {
162 acc[user.id] = user;
163 return acc;
164 }, {} as Record<string, User>);
165}
166
167// =====================================================================
168// Middleware Functions
169// =====================================================================
170
171function withLogging(reducer: (state: State, action: Action) => State) {
172 return (state: State, action: Action): State => {
173 if (process.env.NODE_ENV === 'development') {
174 console.group(`🎯 Action: ${action.type}`);
175 console.log('📋 Previous State:', state);
176 console.log('⚡ Action:', action);
177
178 const startTime = performance.now();
179 const nextState = reducer(state, action);
180 const endTime = performance.now();
181
182 console.log('📊 Next State:', nextState);
183 console.log(`⏱️ Execution Time: ${(endTime - startTime).toFixed(2)}ms`);
184 console.groupEnd();
185
186 return nextState;
187 }
188
189 return reducer(state, action);
190 };
191}
192
193function withHistory(reducer: (state: State, action: Action) => State) {
194 return (state: State, action: Action): State => {
195 if (action.type === 'UNDO') {
196 if (state.history.past.length === 0) return state;
197
198 const previous = state.history.past[state.history.past.length - 1];
199 return {
200 ...previous,
201 history: {
202 past: state.history.past.slice(0, -1),
203 canUndo: state.history.past.length > 1,
204 canRedo: true
205 }
206 };
207 }
208
209 if (action.type === 'REDO') {
210 // Redo implementation would require storing future states
211 return state;
212 }
213
214 if (action.type === 'CLEAR_HISTORY') {
215 return {
216 ...state,
217 history: {
218 past: [],
219 canUndo: false,
220 canRedo: false
221 }
222 };
223 }
224
225 // For state-changing actions, save current state to history
226 const shouldSaveToHistory = [
227 'ADD_TASK',
228 'UPDATE_TASK',
229 'DELETE_TASK',
230 'MOVE_TASK',
231 'BULK_UPDATE_TASKS'
232 ].includes(action.type);
233
234 const nextState = reducer(state, action);
235
236 if (shouldSaveToHistory && nextState !== state) {
237 return {
238 ...nextState,
239 history: {
240 past: [...state.history.past, state].slice(-10), // Keep last 10 states
241 canUndo: true,
242 canRedo: false
243 }
244 };
245 }
246
247 return nextState;
248 };
249}
250
251// =====================================================================
252// Main Reducer
253// =====================================================================
254
255function taskReducer(state: State, action: Action): State {
256 switch (action.type) {
257 case 'LOAD_TASKS_START':
258 return {
259 ...state,
260 ui: {
261 ...state.ui,
262 loading: true,
263 error: null
264 }
265 };
266
267 case 'LOAD_TASKS_SUCCESS': {
268 const { tasks, users } = action.payload;
269 const { entities, byStatus, byUser, byCategory } = normalizeTasks(tasks);
270 const userEntities = normalizeUsers(users);
271
272 return {
273 ...state,
274 entities: {
275 tasks: entities,
276 users: userEntities
277 },
278 collections: {
279 tasksByStatus: byStatus,
280 tasksByUser: byUser,
281 tasksByCategory: byCategory
282 },
283 ui: {
284 ...state.ui,
285 loading: false,
286 error: null
287 }
288 };
289 }
290
291 case 'LOAD_TASKS_ERROR':
292 return {
293 ...state,
294 ui: {
295 ...state.ui,
296 loading: false,
297 error: action.payload
298 }
299 };
300
301 case 'ADD_TASK': {
302 const task = action.payload;
303 const updatedTasks = { ...state.entities.tasks, [task.id]: task };
304 const { byStatus, byUser, byCategory } = normalizeTasks(Object.values(updatedTasks));
305
306 return {
307 ...state,
308 entities: {
309 ...state.entities,
310 tasks: updatedTasks
311 },
312 collections: {
313 tasksByStatus: byStatus,
314 tasksByUser: byUser,
315 tasksByCategory: byCategory
316 }
317 };
318 }
319
320 case 'UPDATE_TASK': {
321 const { id, updates } = action.payload;
322 const currentTask = state.entities.tasks[id];
323 if (!currentTask) return state;
324
325 const updatedTask = {
326 ...currentTask,
327 ...updates,
328 updatedAt: new Date().toISOString()
329 };
330
331 const updatedTasks = {
332 ...state.entities.tasks,
333 [id]: updatedTask
334 };
335
336 const { byStatus, byUser, byCategory } = normalizeTasks(Object.values(updatedTasks));
337
338 return {
339 ...state,
340 entities: {
341 ...state.entities,
342 tasks: updatedTasks
343 },
344 collections: {
345 tasksByStatus: byStatus,
346 tasksByUser: byUser,
347 tasksByCategory: byCategory
348 }
349 };
350 }
351
352 case 'DELETE_TASK': {
353 const taskId = action.payload;
354 const { [taskId]: deletedTask, ...remainingTasks } = state.entities.tasks;
355
356 if (!deletedTask) return state;
357
358 const { byStatus, byUser, byCategory } = normalizeTasks(Object.values(remainingTasks));
359
360 return {
361 ...state,
362 entities: {
363 ...state.entities,
364 tasks: remainingTasks
365 },
366 collections: {
367 tasksByStatus: byStatus,
368 tasksByUser: byUser,
369 tasksByCategory: byCategory
370 },
371 ui: {
372 ...state.ui,
373 selectedTask: state.ui.selectedTask === taskId ? null : state.ui.selectedTask
374 }
375 };
376 }
377
378 case 'MOVE_TASK': {
379 const { id, newStatus } = action.payload;
380 return taskReducer(state, {
381 type: 'UPDATE_TASK',
382 payload: { id, updates: { status: newStatus } }
383 });
384 }
385
386 case 'ASSIGN_TASK': {
387 const { id, userId } = action.payload;
388 return taskReducer(state, {
389 type: 'UPDATE_TASK',
390 payload: { id, updates: { assignedTo: userId } }
391 });
392 }
393
394 case 'UPDATE_FILTER': {
395 const { key, value } = action.payload;
396 return {
397 ...state,
398 ui: {
399 ...state.ui,
400 filters: {
401 ...state.ui.filters,
402 [key]: value
403 }
404 }
405 };
406 }
407
408 case 'SET_SORT': {
409 const { sortBy, sortOrder } = action.payload;
410 return {
411 ...state,
412 ui: {
413 ...state.ui,
414 sortBy,
415 sortOrder
416 }
417 };
418 }
419
420 case 'SELECT_TASK':
421 return {
422 ...state,
423 ui: {
424 ...state.ui,
425 selectedTask: action.payload
426 }
427 };
428
429 case 'BULK_UPDATE_TASKS': {
430 const { taskIds, updates } = action.payload;
431 const updatedTasks = { ...state.entities.tasks };
432
433 taskIds.forEach(id => {
434 if (updatedTasks[id]) {
435 updatedTasks[id] = {
436 ...updatedTasks[id],
437 ...updates,
438 updatedAt: new Date().toISOString()
439 };
440 }
441 });
442
443 const { byStatus, byUser, byCategory } = normalizeTasks(Object.values(updatedTasks));
444
445 return {
446 ...state,
447 entities: {
448 ...state.entities,
449 tasks: updatedTasks
450 },
451 collections: {
452 tasksByStatus: byStatus,
453 tasksByUser: byUser,
454 tasksByCategory: byCategory
455 }
456 };
457 }
458
459 default:
460 return state;
461 }
462}
463
464// Enhanced reducer with middleware
465const enhancedReducer = withLogging(withHistory(taskReducer));
466
467// =====================================================================
468// Context and Provider
469// =====================================================================
470
471interface TaskContextType {
472 state: State;
473 dispatch: React.Dispatch<Action>;
474 actions: {
475 loadTasks: () => Promise<void>;
476 addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => void;
477 updateTask: (id: string, updates: Partial<Task>) => void;
478 deleteTask: (id: string) => void;
479 moveTask: (id: string, newStatus: Task['status']) => void;
480 assignTask: (id: string, userId: string) => void;
481 updateFilter: (key: keyof State['ui']['filters'], value: string) => void;
482 setSort: (sortBy: State['ui']['sortBy'], sortOrder: State['ui']['sortOrder']) => void;
483 selectTask: (id: string | null) => void;
484 bulkUpdateTasks: (taskIds: string[], updates: Partial<Task>) => void;
485 undo: () => void;
486 redo: () => void;
487 };
488 selectors: {
489 getFilteredTasks: () => Task[];
490 getTaskById: (id: string) => Task | undefined;
491 getTasksByStatus: (status: Task['status']) => Task[];
492 getTasksByUser: (userId: string) => Task[];
493 getUserById: (id: string) => User | undefined;
494 getTaskStats: () => {
495 total: number;
496 byStatus: Record<string, number>;
497 byPriority: Record<string, number>;
498 };
499 };
500}
501
502const TaskContext = createContext<TaskContextType | null>(null);
503
504// =====================================================================
505// Custom Hook
506// =====================================================================
507
508export function useTaskManagement(): TaskContextType {
509 const context = useContext(TaskContext);
510 if (!context) {
511 throw new Error('useTaskManagement must be used within TaskProvider');
512 }
513 return context;
514}
515
516// =====================================================================
517// Provider Component
518// =====================================================================
519
520export function TaskProvider({ children }: { children: React.ReactNode }) {
521 const [state, dispatch] = useReducer(enhancedReducer, initialState);
522
523 // Action creators
524 const actions = useMemo(() => ({
525 loadTasks: async () => {
526 dispatch({ type: 'LOAD_TASKS_START' });
527 try {
528 // Simulate API call
529 await new Promise(resolve => setTimeout(resolve, 1000));
530
531 const mockTasks: Task[] = [
532 {
533 id: '1',
534 title: 'Implement user authentication',
535 description: 'Add JWT-based authentication system',
536 status: 'in-progress',
537 priority: 'high',
538 category: 'Development',
539 assignedTo: 'user-1',
540 dueDate: '2024-02-01',
541 createdAt: '2024-01-15T10:00:00Z',
542 updatedAt: '2024-01-15T10:00:00Z',
543 tags: ['auth', 'security', 'backend'],
544 estimatedHours: 16,
545 actualHours: 8
546 },
547 {
548 id: '2',
549 title: 'Design landing page',
550 description: 'Create responsive landing page design',
551 status: 'todo',
552 priority: 'medium',
553 category: 'Design',
554 assignedTo: 'user-2',
555 dueDate: '2024-01-30',
556 createdAt: '2024-01-14T09:00:00Z',
557 updatedAt: '2024-01-14T09:00:00Z',
558 tags: ['design', 'ui', 'frontend'],
559 estimatedHours: 12,
560 actualHours: 0
561 },
562 {
563 id: '3',
564 title: 'Write unit tests',
565 description: 'Add comprehensive test coverage',
566 status: 'completed',
567 priority: 'high',
568 category: 'Testing',
569 assignedTo: 'user-1',
570 dueDate: '2024-01-25',
571 createdAt: '2024-01-10T14:00:00Z',
572 updatedAt: '2024-01-20T16:00:00Z',
573 tags: ['testing', 'quality', 'automation'],
574 estimatedHours: 8,
575 actualHours: 10
576 }
577 ];
578
579 const mockUsers: User[] = [
580 {
581 id: 'user-1',
582 name: 'John Doe',
583 email: '[email protected]',
584 role: 'admin',
585 avatar: '👨💻'
586 },
587 {
588 id: 'user-2',
589 name: 'Jane Smith',
590 email: '[email protected]',
591 role: 'user',
592 avatar: '👩🎨'
593 }
594 ];
595
596 dispatch({
597 type: 'LOAD_TASKS_SUCCESS',
598 payload: { tasks: mockTasks, users: mockUsers }
599 });
600 } catch (error) {
601 dispatch({
602 type: 'LOAD_TASKS_ERROR',
603 payload: error instanceof Error ? error.message : 'Unknown error'
604 });
605 }
606 },
607
608 addTask: (taskData: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>) => {
609 const task: Task = {
610 ...taskData,
611 id: `task-${Date.now()}`,
612 createdAt: new Date().toISOString(),
613 updatedAt: new Date().toISOString()
614 };
615 dispatch({ type: 'ADD_TASK', payload: task });
616 },
617
618 updateTask: (id: string, updates: Partial<Task>) => {
619 dispatch({ type: 'UPDATE_TASK', payload: { id, updates } });
620 },
621
622 deleteTask: (id: string) => {
623 dispatch({ type: 'DELETE_TASK', payload: id });
624 },
625
626 moveTask: (id: string, newStatus: Task['status']) => {
627 dispatch({ type: 'MOVE_TASK', payload: { id, newStatus } });
628 },
629
630 assignTask: (id: string, userId: string) => {
631 dispatch({ type: 'ASSIGN_TASK', payload: { id, userId } });
632 },
633
634 updateFilter: (key: keyof State['ui']['filters'], value: string) => {
635 dispatch({ type: 'UPDATE_FILTER', payload: { key, value } });
636 },
637
638 setSort: (sortBy: State['ui']['sortBy'], sortOrder: State['ui']['sortOrder']) => {
639 dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
640 },
641
642 selectTask: (id: string | null) => {
643 dispatch({ type: 'SELECT_TASK', payload: id });
644 },
645
646 bulkUpdateTasks: (taskIds: string[], updates: Partial<Task>) => {
647 dispatch({ type: 'BULK_UPDATE_TASKS', payload: { taskIds, updates } });
648 },
649
650 undo: () => {
651 dispatch({ type: 'UNDO' });
652 },
653
654 redo: () => {
655 dispatch({ type: 'REDO' });
656 }
657 }), []);
658
659 // Selectors
660 const selectors = useMemo(() => ({
661 getFilteredTasks: () => {
662 const tasks = Object.values(state.entities.tasks);
663 const { filters } = state.ui;
664
665 return tasks.filter(task => {
666 if (filters.status !== 'all' && task.status !== filters.status) return false;
667 if (filters.assignee !== 'all' && task.assignedTo !== filters.assignee) return false;
668 if (filters.category !== 'all' && task.category !== filters.category) return false;
669 if (filters.priority !== 'all' && task.priority !== filters.priority) return false;
670 if (filters.search && !task.title.toLowerCase().includes(filters.search.toLowerCase())) return false;
671
672 return true;
673 }).sort((a, b) => {
674 const { sortBy, sortOrder } = state.ui;
675 const aValue = a[sortBy];
676 const bValue = b[sortBy];
677
678 if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
679 if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
680 return 0;
681 });
682 },
683
684 getTaskById: (id: string) => state.entities.tasks[id],
685
686 getTasksByStatus: (status: Task['status']) => {
687 return state.collections.tasksByStatus[status]?.map(id => state.entities.tasks[id]) || [];
688 },
689
690 getTasksByUser: (userId: string) => {
691 return state.collections.tasksByUser[userId]?.map(id => state.entities.tasks[id]) || [];
692 },
693
694 getUserById: (id: string) => state.entities.users[id],
695
696 getTaskStats: () => {
697 const tasks = Object.values(state.entities.tasks);
698 const byStatus = tasks.reduce((acc, task) => {
699 acc[task.status] = (acc[task.status] || 0) + 1;
700 return acc;
701 }, {} as Record<string, number>);
702
703 const byPriority = tasks.reduce((acc, task) => {
704 acc[task.priority] = (acc[task.priority] || 0) + 1;
705 return acc;
706 }, {} as Record<string, number>);
707
708 return {
709 total: tasks.length,
710 byStatus,
711 byPriority
712 };
713 }
714 }), [state]);
715
716 // Load initial data
717 useEffect(() => {
718 actions.loadTasks();
719 }, []);
720
721 const value: TaskContextType = {
722 state,
723 dispatch,
724 actions,
725 selectors
726 };
727
728 return (
729 <TaskContext.Provider value={value}>
730 {children}
731 </TaskContext.Provider>
732 );
733}
734
735// =====================================================================
736// Demo Components
737// =====================================================================
738
739function TaskList() {
740 const { state, actions, selectors } = useTaskManagement();
741 const filteredTasks = selectors.getFilteredTasks();
742
743 if (state.ui.loading) {
744 return (
745 <div className="p-6 text-center">
746 <div className="animate-spin w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"></div>
747 <p>Loading tasks...</p>
748 </div>
749 );
750 }
751
752 if (state.ui.error) {
753 return (
754 <div className="p-6 text-center text-red-600">
755 <p>Error: {state.ui.error}</p>
756 <button
757 onClick={actions.loadTasks}
758 className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
759 >
760 Retry
761 </button>
762 </div>
763 );
764 }
765
766 return (
767 <div className="p-6">
768 <div className="flex items-center justify-between mb-6">
769 <h2 className="text-2xl font-bold">Tasks</h2>
770 <div className="flex gap-2">
771 <button
772 onClick={actions.undo}
773 disabled={!state.history.canUndo}
774 className="px-3 py-2 bg-gray-500 text-white rounded disabled:opacity-50"
775 >
776 ↶ Undo
777 </button>
778 <button
779 onClick={() => actions.addTask({
780 title: 'New Task',
781 description: 'Task description',
782 status: 'todo',
783 priority: 'medium',
784 category: 'General',
785 assignedTo: 'user-1',
786 dueDate: new Date().toISOString().split('T')[0],
787 tags: [],
788 estimatedHours: 4,
789 actualHours: 0
790 })}
791 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
792 >
793 + Add Task
794 </button>
795 </div>
796 </div>
797
798 {/* Filters */}
799 <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
800 <select
801 value={state.ui.filters.status}
802 onChange={(e) => actions.updateFilter('status', e.target.value)}
803 className="p-2 border rounded"
804 >
805 <option value="all">All Status</option>
806 <option value="todo">Todo</option>
807 <option value="in-progress">In Progress</option>
808 <option value="completed">Completed</option>
809 </select>
810
811 <select
812 value={state.ui.filters.priority}
813 onChange={(e) => actions.updateFilter('priority', e.target.value)}
814 className="p-2 border rounded"
815 >
816 <option value="all">All Priority</option>
817 <option value="low">Low</option>
818 <option value="medium">Medium</option>
819 <option value="high">High</option>
820 </select>
821
822 <input
823 type="text"
824 placeholder="Search tasks..."
825 value={state.ui.filters.search}
826 onChange={(e) => actions.updateFilter('search', e.target.value)}
827 className="p-2 border rounded"
828 />
829
830 <select
831 value={`${state.ui.sortBy}-${state.ui.sortOrder}`}
832 onChange={(e) => {
833 const [sortBy, sortOrder] = e.target.value.split('-') as [State['ui']['sortBy'], State['ui']['sortOrder']];
834 actions.setSort(sortBy, sortOrder);
835 }}
836 className="p-2 border rounded"
837 >
838 <option value="dueDate-asc">Due Date (Asc)</option>
839 <option value="dueDate-desc">Due Date (Desc)</option>
840 <option value="priority-asc">Priority (Asc)</option>
841 <option value="priority-desc">Priority (Desc)</option>
842 <option value="title-asc">Title (A-Z)</option>
843 <option value="title-desc">Title (Z-A)</option>
844 </select>
845 </div>
846
847 {/* Task List */}
848 <div className="space-y-4">
849 {filteredTasks.length === 0 ? (
850 <p className="text-gray-500 text-center py-8">No tasks found matching your criteria.</p>
851 ) : (
852 filteredTasks.map(task => {
853 const user = selectors.getUserById(task.assignedTo);
854 const isSelected = state.ui.selectedTask === task.id;
855
856 return (
857 <div
858 key={task.id}
859 onClick={() => actions.selectTask(isSelected ? null : task.id)}
860 className={`p-4 border rounded-lg cursor-pointer transition-all ${
861 isSelected ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
862 }`}
863 >
864 <div className="flex items-start justify-between">
865 <div className="flex-1">
866 <div className="flex items-center gap-3 mb-2">
867 <h3 className="font-semibold">{task.title}</h3>
868 <span className={`px-2 py-1 text-xs rounded ${
869 task.status === 'completed' ? 'bg-green-100 text-green-800' :
870 task.status === 'in-progress' ? 'bg-yellow-100 text-yellow-800' :
871 'bg-gray-100 text-gray-800'
872 }`}>
873 {task.status}
874 </span>
875 <span className={`px-2 py-1 text-xs rounded ${
876 task.priority === 'high' ? 'bg-red-100 text-red-800' :
877 task.priority === 'medium' ? 'bg-orange-100 text-orange-800' :
878 'bg-blue-100 text-blue-800'
879 }`}>
880 {task.priority}
881 </span>
882 </div>
883 <p className="text-gray-600 text-sm mb-2">{task.description}</p>
884 <div className="flex items-center gap-4 text-xs text-gray-500">
885 <span>👤 {user?.name || 'Unassigned'}</span>
886 <span>📅 {task.dueDate}</span>
887 <span>📂 {task.category}</span>
888 <span>⏱️ {task.estimatedHours}h estimated</span>
889 </div>
890 </div>
891
892 <div className="flex gap-2">
893 <button
894 onClick={(e) => {
895 e.stopPropagation();
896 const newStatus = task.status === 'completed' ? 'todo' :
897 task.status === 'in-progress' ? 'completed' : 'in-progress';
898 actions.moveTask(task.id, newStatus);
899 }}
900 className="px-3 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600"
901 >
902 {task.status === 'completed' ? 'Reopen' :
903 task.status === 'in-progress' ? 'Complete' : 'Start'}
904 </button>
905 <button
906 onClick={(e) => {
907 e.stopPropagation();
908 actions.deleteTask(task.id);
909 }}
910 className="px-3 py-1 text-xs bg-red-500 text-white rounded hover:bg-red-600"
911 >
912 Delete
913 </button>
914 </div>
915 </div>
916
917 {task.tags.length > 0 && (
918 <div className="flex gap-1 mt-3">
919 {task.tags.map(tag => (
920 <span key={tag} className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded">
921 #{tag}
922 </span>
923 ))}
924 </div>
925 )}
926 </div>
927 );
928 })
929 )}
930 </div>
931 </div>
932 );
933}
934
935function TaskStats() {
936 const { selectors } = useTaskManagement();
937 const stats = selectors.getTaskStats();
938
939 return (
940 <div className="p-6 bg-gray-50 rounded-lg">
941 <h3 className="text-lg font-semibold mb-4">📊 Task Statistics</h3>
942 <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
943 <div className="text-center">
944 <div className="text-2xl font-bold text-blue-600">{stats.total}</div>
945 <div className="text-sm text-gray-600">Total Tasks</div>
946 </div>
947 <div className="text-center">
948 <div className="text-2xl font-bold text-green-600">{stats.byStatus.completed || 0}</div>
949 <div className="text-sm text-gray-600">Completed</div>
950 </div>
951 <div className="text-center">
952 <div className="text-2xl font-bold text-yellow-600">{stats.byStatus['in-progress'] || 0}</div>
953 <div className="text-sm text-gray-600">In Progress</div>
954 </div>
955 <div className="text-center">
956 <div className="text-2xl font-bold text-gray-600">{stats.byStatus.todo || 0}</div>
957 <div className="text-sm text-gray-600">Todo</div>
958 </div>
959 </div>
960
961 <div className="mt-4 pt-4 border-t">
962 <h4 className="text-sm font-semibold mb-2">Priority Distribution</h4>
963 <div className="flex gap-4 text-sm">
964 <span className="text-red-600">🔴 High: {stats.byPriority.high || 0}</span>
965 <span className="text-orange-600">🟡 Medium: {stats.byPriority.medium || 0}</span>
966 <span className="text-blue-600">🔵 Low: {stats.byPriority.low || 0}</span>
967 </div>
968 </div>
969 </div>
970 );
971}
972
973// =====================================================================
974// Main Demo App
975// =====================================================================
976
977export default function TaskManagementApp() {
978 return (
979 <TaskProvider>
980 <div className="min-h-screen bg-gray-100">
981 <div className="container mx-auto py-8">
982 <div className="bg-white rounded-lg shadow-lg overflow-hidden">
983 <div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-6">
984 <h1 className="text-3xl font-bold mb-2">🚀 Advanced useReducer Task Management</h1>
985 <p className="text-blue-100">
986 Complete demonstration of advanced useReducer patterns with TypeScript, middleware,
987 state normalization, and complex state management
988 </p>
989 </div>
990
991 <TaskStats />
992 <TaskList />
993
994 <div className="p-6 bg-gray-50 border-t">
995 <h3 className="text-lg font-semibold mb-3">🎯 Advanced Features Demonstrated</h3>
996 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
997 <div className="bg-white p-3 rounded border">
998 <strong>✅ TypeScript Integration</strong>
999 <p>Fully type-safe actions, state, and selectors</p>
1000 </div>
1001 <div className="bg-white p-3 rounded border">
1002 <strong>✅ State Normalization</strong>
1003 <p>Efficient data structure with entity-collection pattern</p>
1004 </div>
1005 <div className="bg-white p-3 rounded border">
1006 <strong>✅ Middleware Patterns</strong>
1007 <p>Logging, history tracking, and composable enhancements</p>
1008 </div>
1009 <div className="bg-white p-3 rounded border">
1010 <strong>✅ Advanced Selectors</strong>
1011 <p>Memoized, computed values with filtering and sorting</p>
1012 </div>
1013 <div className="bg-white p-3 rounded border">
1014 <strong>✅ Undo/Redo System</strong>
1015 <p>Time-travel debugging with state history</p>
1016 </div>
1017 <div className="bg-white p-3 rounded border">
1018 <strong>✅ Context Integration</strong>
1019 <p>Provider pattern with custom hooks</p>
1020 </div>
1021 </div>
1022 </div>
1023 </div>
1024 </div>
1025 </div>
1026 </TaskProvider>
1027 );
1028}This advanced useReducer demonstration showcases enterprise-level patterns and best practices:
🏗️ Architecture Highlights:
1. TypeScript Integration:
2. State Normalization:
3. Middleware Patterns:
4. Advanced Reducer Patterns:
5. Context Integration:
6. Production Features:
7. Developer Experience:
8. Real-World Scenarios:
This implementation demonstrates how useReducer can handle enterprise-level complexity while maintaining code quality, type safety, and developer productivity. The patterns shown here scale from simple components to large applications with sophisticated state management requirements.
Excellent work mastering advanced useReducer patterns! You now have the tools to handle complex state management in production applications. In our next lesson, we'll explore Custom Hook Development, where you'll learn to create reusable hook libraries, implement advanced patterns like compound hooks, and build your own hook ecosystem for maximum code reuse and maintainability.
Your progress is saved in this browser, on this device only.