Master industry best practices for testing, performance optimization, debugging, and maintainable state management architectures
Master industry best practices for testing, performance optimization, debugging, and maintainable state management architectures
Welcome to this comprehensive guide on state management best practices! This lesson covers industry-standard approaches to building production-ready, maintainable, and performant state management systems.
By the end of this lesson, you'll be able to:
Testing is crucial for maintaining reliable state management. Here's how to test Redux slices effectively:
1import { configureStore } from '@reduxjs/toolkit';
2import todosSlice, { addTodo, toggleTodo, removeTodo } from './todosSlice';
3
4describe('todosSlice', () => {
5 let store: ReturnType<typeof configureStore>;
6
7 beforeEach(() => {
8 store = configureStore({
9 reducer: { todos: todosSlice }
10 });
11 });
12
13 test('should add a new todo', () => {
14 const todoText = 'Test todo';
15 store.dispatch(addTodo(todoText));
16
17 const state = store.getState().todos;
18 expect(state.todos).toHaveLength(1);
19 expect(state.todos[0].text).toBe(todoText);
20 expect(state.todos[0].completed).toBe(false);
21 });
22
23 test('should toggle todo completion', () => {
24 store.dispatch(addTodo('Test todo'));
25 const todoId = store.getState().todos.todos[0].id;
26
27 store.dispatch(toggleTodo(todoId));
28
29 const todo = store.getState().todos.todos[0];
30 expect(todo.completed).toBe(true);
31 });
32});1import { fetchTodos } from './todosSlice';
2import { mockApiResponse } from '../__mocks__/api';
3
4describe('async thunks', () => {
5 test('should fetch todos successfully', async () => {
6 const mockTodos = [{ id: '1', text: 'Test', completed: false }];
7 mockApiResponse(mockTodos);
8
9 const result = await store.dispatch(fetchTodos());
10
11 expect(result.type).toBe('todos/fetchTodos/fulfilled');
12 expect(result.payload).toEqual(mockTodos);
13 expect(store.getState().todos.loading).toBe(false);
14 expect(store.getState().todos.todos).toEqual(mockTodos);
15 });
16
17 test('should handle fetch errors', async () => {
18 const errorMessage = 'Failed to fetch';
19 mockApiResponse(null, errorMessage);
20
21 const result = await store.dispatch(fetchTodos());
22
23 expect(result.type).toBe('todos/fetchTodos/rejected');
24 expect(store.getState().todos.error).toBe(errorMessage);
25 expect(store.getState().todos.loading).toBe(false);
26 });
27});1import { render, screen, fireEvent } from '@testing-library/react';
2import { Provider } from 'react-redux';
3import { configureStore } from '@reduxjs/toolkit';
4import { TodoList } from './TodoList';
5import todosSlice from './todosSlice';
6
7const renderWithState = (initialState = {}) => {
8 const store = configureStore({
9 reducer: { todos: todosSlice },
10 preloadedState: initialState
11 });
12
13 return {
14 ...render(
15 <Provider store={store}>
16 <TodoList />
17 </Provider>
18 ),
19 store
20 };
21};
22
23test('should render todos and handle interactions', () => {
24 const initialState = {
25 todos: {
26 todos: [
27 { id: '1', text: 'Test todo', completed: false }
28 ],
29 loading: false,
30 error: null
31 }
32 };
33
34 const { store } = renderWithState(initialState);
35
36 expect(screen.getByText('Test todo')).toBeInTheDocument();
37
38 fireEvent.click(screen.getByRole('checkbox'));
39
40 expect(store.getState().todos.todos[0].completed).toBe(true);
41});Use memoized selectors to prevent unnecessary recalculations:
1import { createSelector } from '@reduxjs/toolkit';
2
3// Basic selectors
4const selectTodos = (state: RootState) => state.todos.todos;
5const selectFilter = (state: RootState) => state.todos.filter;
6
7// Memoized computed selector
8export const selectVisibleTodos = createSelector(
9 [selectTodos, selectFilter],
10 (todos, filter) => {
11 switch (filter) {
12 case 'active':
13 return todos.filter(todo => !todo.completed);
14 case 'completed':
15 return todos.filter(todo => todo.completed);
16 default:
17 return todos;
18 }
19 }
20);
21
22// Parameterized selectors
23export const selectTodoById = createSelector(
24 [selectTodos, (state: RootState, todoId: string) => todoId],
25 (todos, todoId) => todos.find(todo => todo.id === todoId)
26);1import React, { memo } from 'react';
2import { useSelector } from 'react-redux';
3import { selectTodoById } from './selectors';
4
5interface TodoItemProps {
6 todoId: string;
7}
8
9// Memoize component to prevent unnecessary re-renders
10export const TodoItem = memo<TodoItemProps>(({ todoId }) => {
11 // Select only the specific todo by ID
12 const todo = useSelector(state => selectTodoById(state, todoId));
13
14 if (!todo) return null;
15
16 return (
17 <div>
18 <span>{todo.text}</span>
19 <input
20 type="checkbox"
21 checked={todo.completed}
22 onChange={() => dispatch(toggleTodo(todoId))}
23 />
24 </div>
25 );
26});
27
28// Use display name for debugging
29TodoItem.displayName = 'TodoItem';1// ❌ Over-normalized (can cause performance issues)
2interface OverNormalizedState {
3 users: { [id: string]: User };
4 todos: { [id: string]: Todo };
5 projects: { [id: string]: Project };
6 userTodos: { [userId: string]: string[] };
7 projectTodos: { [projectId: string]: string[] };
8}
9
10// ✅ Balanced normalization
11interface BalancedState {
12 todos: {
13 byId: { [id: string]: Todo };
14 allIds: string[];
15 byUser: { [userId: string]: string[] };
16 };
17 users: {
18 byId: { [id: string]: User };
19 allIds: string[];
20 };
21}1import { configureStore } from '@reduxjs/toolkit';
2
3export const store = configureStore({
4 reducer: rootReducer,
5 devTools: process.env.NODE_ENV !== 'production' && {
6 trace: true,
7 traceLimit: 25,
8 actionSanitizer: (action) => ({
9 ...action,
10 // Sanitize sensitive data
11 payload: action.type.includes('auth')
12 ? '[HIDDEN]'
13 : action.payload
14 }),
15 stateSanitizer: (state) => ({
16 ...state,
17 auth: state.auth ? '[HIDDEN]' : state.auth
18 })
19 }
20});1import { createListenerMiddleware } from '@reduxjs/toolkit';
2
3// Logger middleware for development
4const loggerMiddleware = createListenerMiddleware();
5
6loggerMiddleware.startListening({
7 predicate: () => process.env.NODE_ENV === 'development',
8 effect: (action, listenerApi) => {
9 console.group(`🔄 Action: ${action.type}`);
10 console.log('Payload:', action.payload);
11 console.log('Previous State:', listenerApi.getOriginalState());
12 console.log('Current State:', listenerApi.getState());
13 console.groupEnd();
14 }
15});1// Error tracking slice
2const errorSlice = createSlice({
3 name: 'errors',
4 initialState: {
5 errors: [] as ErrorEntry[],
6 retryableActions: [] as any[]
7 },
8 reducers: {
9 logError: (state, action) => {
10 state.errors.push({
11 id: Date.now().toString(),
12 message: action.payload.message,
13 stack: action.payload.stack,
14 timestamp: new Date().toISOString(),
15 action: action.payload.action
16 });
17 },
18 clearErrors: (state) => {
19 state.errors = [];
20 },
21 addRetryableAction: (state, action) => {
22 state.retryableActions.push(action.payload);
23 }
24 }
25});
26
27// Error boundary integration
28export class StateErrorBoundary extends React.Component {
29 componentDidCatch(error: Error, errorInfo: ErrorInfo) {
30 store.dispatch(logError({
31 message: error.message,
32 stack: error.stack,
33 componentStack: errorInfo.componentStack
34 }));
35 }
36}1src/
2 features/
3 todos/
4 components/
5 TodoList.tsx
6 TodoItem.tsx
7 TodoForm.tsx
8 hooks/
9 useTodos.ts
10 useTodoFilters.ts
11 services/
12 todosApi.ts
13 store/
14 todosSlice.ts
15 todosSelectors.ts
16 todosThunks.ts
17 types/
18 index.ts
19 index.ts
20 users/
21 // similar structure
22 shared/
23 store/
24 rootReducer.ts
25 store.ts
26 types/
27 index.ts1// todos.duck.ts - All todo-related logic in one file
2import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit';
3
4// Types
5export interface Todo {
6 id: string;
7 text: string;
8 completed: boolean;
9}
10
11interface TodosState {
12 todos: Todo[];
13 loading: boolean;
14 error: string | null;
15}
16
17// Async thunks
18export const fetchTodos = createAsyncThunk(
19 'todos/fetchTodos',
20 async () => {
21 const response = await todosApi.fetchTodos();
22 return response.data;
23 }
24);
25
26// Slice
27const todosSlice = createSlice({
28 name: 'todos',
29 initialState: {
30 todos: [],
31 loading: false,
32 error: null
33 } as TodosState,
34 reducers: {
35 addTodo: (state, action) => {
36 state.todos.push({
37 id: Date.now().toString(),
38 text: action.payload,
39 completed: false
40 });
41 },
42 toggleTodo: (state, action) => {
43 const todo = state.todos.find(t => t.id === action.payload);
44 if (todo) {
45 todo.completed = !todo.completed;
46 }
47 }
48 },
49 extraReducers: (builder) => {
50 builder
51 .addCase(fetchTodos.pending, (state) => {
52 state.loading = true;
53 })
54 .addCase(fetchTodos.fulfilled, (state, action) => {
55 state.loading = false;
56 state.todos = action.payload;
57 })
58 .addCase(fetchTodos.rejected, (state, action) => {
59 state.loading = false;
60 state.error = action.error.message || 'Failed to fetch todos';
61 });
62 }
63});
64
65// Selectors
66export const selectTodos = (state: RootState) => state.todos.todos;
67export const selectTodosLoading = (state: RootState) => state.todos.loading;
68export const selectCompletedTodos = createSelector(
69 [selectTodos],
70 (todos) => todos.filter(todo => todo.completed)
71);
72
73// Export actions and reducer
74export const { addTodo, toggleTodo } = todosSlice.actions;
75export default todosSlice.reducer;1/**
2 * State Management Guidelines
3 *
4 * 1. State Structure
5 * - Keep state flat and normalized
6 * - Use entities pattern for collections
7 * - Separate domain data from UI state
8 *
9 * 2. Action Naming
10 * - Use verb/noun pattern: addTodo, removeTodo
11 * - Include async lifecycle: fetchTodos.pending, fetchTodos.fulfilled
12 * - Be descriptive: updateUserProfile not updateUser
13 *
14 * 3. Selector Conventions
15 * - Start with 'select': selectTodos, selectUserById
16 * - Use memoization for computed values
17 * - Keep selectors pure and simple
18 *
19 * 4. Error Handling
20 * - Always handle async errors
21 * - Provide user-friendly error messages
22 * - Implement retry mechanisms for critical operations
23 */
24
25// Example: Standardized async thunk pattern
26export const createAsyncOperation = <T, P = void>(
27 actionType: string,
28 operation: (params: P) => Promise<T>
29) =>
30 createAsyncThunk(actionType, async (params: P, { rejectWithValue }) => {
31 try {
32 return await operation(params);
33 } catch (error) {
34 return rejectWithValue({
35 message: error instanceof Error ? error.message : 'Unknown error',
36 code: error instanceof ApiError ? error.code : 'UNKNOWN_ERROR',
37 timestamp: new Date().toISOString()
38 });
39 }
40 });1// Comprehensive type definitions
2export interface RootState {
3 todos: TodosState;
4 users: UsersState;
5 ui: UIState;
6}
7
8export type AppDispatch = typeof store.dispatch;
9export type AppThunk<ReturnType = void> = ThunkAction<
10 ReturnType,
11 RootState,
12 unknown,
13 Action<string>
14>;
15
16// Typed hooks
17export const useAppDispatch = () => useDispatch<AppDispatch>();
18export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
19
20// Typed async thunk
21export const fetchTodosTyped = createAsyncThunk<
22 Todo[], // Return type
23 { userId: string }, // Params type
24 {
25 dispatch: AppDispatch;
26 state: RootState;
27 rejectValue: ErrorPayload;
28 }
29>('todos/fetchTodos', async (params, { getState, rejectWithValue }) => {
30 const currentUser = getState().users.currentUser;
31 if (!currentUser) {
32 return rejectWithValue({ message: 'User not authenticated' });
33 }
34
35 const response = await todosApi.fetchTodos(params.userId);
36 return response.data;
37});1// Code splitting for large state modules
2const LazyTodosSlice = lazy(() => import('./features/todos/todosSlice'));
3
4// Conditional middleware loading
5const middleware = [
6 ...getDefaultMiddleware({
7 serializableCheck: {
8 ignoredActions: ['persist/PERSIST']
9 }
10 })
11];
12
13if (process.env.NODE_ENV === 'development') {
14 middleware.push(loggerMiddleware);
15}1// Cleanup patterns
2const todosSlice = createSlice({
3 name: 'todos',
4 initialState,
5 reducers: {
6 // Clear data when navigating away
7 clearTodos: (state) => {
8 state.todos = [];
9 state.cache = {};
10 },
11 // Limit cache size
12 updateCache: (state, action) => {
13 const cacheKeys = Object.keys(state.cache);
14 if (cacheKeys.length > 100) {
15 // Remove oldest entries
16 const oldestKeys = cacheKeys.slice(0, 50);
17 oldestKeys.forEach(key => delete state.cache[key]);
18 }
19 state.cache[action.payload.key] = action.payload.data;
20 }
21 }
22});Try implementing these best practices in the interactive editor:
In the next lesson, we'll explore Production State Architecture where you'll learn to design enterprise-grade state management systems for large-scale applications.
Ready to build production-ready state management? Let's dive into the interactive examples!
Your progress is saved in this browser, on this device only.