Deep dive into Redux core concepts with modern patterns and best practices.
Deep dive into Redux core concepts with modern patterns and best practices.
Redux follows a strict unidirectional data flow pattern that ensures predictable state management. Understanding this flow is crucial for building maintainable applications.
1// User clicks a button, triggering an action
2const handleAddTodo = (text) => {
3 dispatch(todoAdded(text)); // Action is dispatched
4};1// Reducer receives the action and current state
2function todosReducer(state = initialState, action) {
3 switch (action.type) {
4 case 'todos/todoAdded':
5 return {
6 ...state,
7 items: [...state.items, action.payload]
8 };
9 default:
10 return state;
11 }
12}1// Components subscribe to state changes
2const todos = useSelector(state => state.todos.items);
3// UI automatically re-renders when state changesAll application state lives in one store tree.
1// ✅ Centralized state
2const store = {
3 user: { id: 1, name: 'John' },
4 todos: { items: [], filter: 'all' },
5 ui: { sidebarOpen: false }
6};
7
8// ❌ Scattered state across components
9const UserComponent = () => {
10 const [user, setUser] = useState({}); // Duplicated user state
11};State can only be changed by dispatching actions.
1// ✅ Dispatching actions
2dispatch(userNameChanged('Jane'));
3
4// ❌ Direct state mutation
5state.user.name = 'Jane'; // This won't work and breaks ReduxReducers must be pure functions that produce new state.
1// ✅ Pure reducer
2function counterReducer(state = 0, action) {
3 switch (action.type) {
4 case 'INCREMENT':
5 return state + 1; // Returns new state
6 default:
7 return state;
8 }
9}
10
11// ❌ Impure reducer
12function badReducer(state = 0, action) {
13 switch (action.type) {
14 case 'INCREMENT':
15 state++; // Mutates existing state
16 Math.random(); // Side effect
17 return state;
18 }
19}1// Middleware intercepts actions before they reach reducers
2const loggingMiddleware = store => next => action => {
3 console.log('Dispatching:', action);
4 const result = next(action); // Pass to next middleware or reducer
5 console.log('New state:', store.getState());
6 return result;
7};
8
9// Flow: Action → Middleware → Reducer → New State → Subscribers1// Thunk middleware enables async actions
2const fetchUser = (userId) => async (dispatch, getState) => {
3 dispatch(userFetchStarted());
4
5 try {
6 const user = await api.getUser(userId);
7 dispatch(userFetchSucceeded(user));
8 } catch (error) {
9 dispatch(userFetchFailed(error.message));
10 }
11};
12
13// Flow: Async Action → Multiple Sync Actions → State Updates1// Selectors compute derived data from state
2const selectCompletedTodos = createSelector(
3 [state => state.todos.items],
4 (todos) => todos.filter(todo => todo.completed)
5);
6
7// Flow: State Change → Selector Recompute → Component UpdateActions are plain JavaScript objects that describe what happened in your application. They are the only way to trigger state changes in Redux.
1// Minimal action structure
2const action = {
3 type: 'todos/todoAdded', // Required: describes what happened
4 payload: 'Learn Redux' // Optional: data associated with the action
5};
6
7// FSA (Flux Standard Action) compliant
8const fsaAction = {
9 type: 'user/loginFailed',
10 payload: new Error('Invalid credentials'),
11 error: true, // Indicates this action represents an error
12 meta: { // Meta information about the action
13 timestamp: Date.now(),
14 source: 'loginForm'
15 }
16};1// ✅ Good action types - descriptive and hierarchical
2const ActionTypes = {
3 // Feature/Entity pattern
4 USER_LOGIN_REQUESTED: 'user/loginRequested',
5 USER_LOGIN_SUCCEEDED: 'user/loginSucceeded',
6 USER_LOGIN_FAILED: 'user/loginFailed',
7
8 // CRUD operations
9 TODOS_FETCH_STARTED: 'todos/fetchStarted',
10 TODOS_FETCH_SUCCEEDED: 'todos/fetchSucceeded',
11 TODOS_ADD: 'todos/add',
12 TODOS_UPDATE: 'todos/update',
13 TODOS_DELETE: 'todos/delete',
14
15 // UI state
16 UI_MODAL_OPENED: 'ui/modalOpened',
17 UI_SIDEBAR_TOGGLED: 'ui/sidebarToggled'
18};
19
20// ❌ Poor action types
21const BadActionTypes = {
22 CLICK: 'CLICK', // Too vague
23 DATA: 'DATA', // What kind of data?
24 UPDATE: 'UPDATE', // Update what?
25 userlogin: 'userlogin' // No separation/hierarchy
26};1import { createSlice } from '@reduxjs/toolkit';
2
3const todosSlice = createSlice({
4 name: 'todos',
5 initialState: { items: [] },
6 reducers: {
7 // RTK automatically creates action creators
8 todoAdded: (state, action) => {
9 state.items.push({
10 id: Date.now(),
11 text: action.payload,
12 completed: false
13 });
14 },
15
16 todoToggled: (state, action) => {
17 const todo = state.items.find(item => item.id === action.payload);
18 if (todo) {
19 todo.completed = !todo.completed;
20 }
21 }
22 }
23});
24
25// Auto-generated action creators
26export const { todoAdded, todoToggled } = todosSlice.actions;
27
28// Usage:
29dispatch(todoAdded('Learn Redux'));
30// Creates: { type: 'todos/todoAdded', payload: 'Learn Redux' }1const postsSlice = createSlice({
2 name: 'posts',
3 initialState: { posts: [] },
4 reducers: {
5 postAdded: {
6 reducer: (state, action) => {
7 state.posts.push(action.payload);
8 },
9 // Prepare callback customizes action creation
10 prepare: (title, content, author) => {
11 return {
12 payload: {
13 id: nanoid(),
14 title,
15 content,
16 author,
17 createdAt: new Date().toISOString(),
18 updatedAt: new Date().toISOString(),
19 likes: 0,
20 comments: []
21 },
22 meta: {
23 timestamp: Date.now(),
24 source: 'user'
25 }
26 };
27 }
28 },
29
30 postLiked: {
31 reducer: (state, action) => {
32 const post = state.posts.find(p => p.id === action.payload.postId);
33 if (post) {
34 post.likes += 1;
35 post.updatedAt = action.payload.timestamp;
36 }
37 },
38 prepare: (postId) => ({
39 payload: {
40 postId,
41 timestamp: new Date().toISOString()
42 }
43 })
44 }
45 }
46});
47
48// Usage with prepare:
49dispatch(postAdded('My Title', 'Post content', 'John Doe'));
50// Creates complex payload with generated ID and timestamps1const userSlice = createSlice({
2 name: 'user',
3 initialState: {
4 profile: null,
5 errors: {},
6 validationState: {}
7 },
8 reducers: {
9 profileUpdateStarted: (state, action) => {
10 state.validationState[action.payload.field] = 'validating';
11 delete state.errors[action.payload.field];
12 },
13
14 profileUpdateSucceeded: (state, action) => {
15 const { field, value } = action.payload;
16 if (state.profile) {
17 state.profile[field] = value;
18 }
19 state.validationState[field] = 'valid';
20 },
21
22 profileUpdateFailed: {
23 reducer: (state, action) => {
24 const { field, error } = action.payload;
25 state.errors[field] = error;
26 state.validationState[field] = 'invalid';
27 },
28 prepare: (field, error) => ({
29 payload: { field, error },
30 error: true, // FSA error flag
31 meta: {
32 field,
33 timestamp: Date.now()
34 }
35 })
36 }
37 }
38});1const appSlice = createSlice({
2 name: 'app',
3 initialState: {},
4 reducers: {
5 // Batch multiple operations
6 batchUpdate: (state, action) => {
7 // action.payload contains multiple updates
8 action.payload.forEach(update => {
9 switch (update.type) {
10 case 'updateUser':
11 Object.assign(state.user, update.data);
12 break;
13 case 'updateSettings':
14 Object.assign(state.settings, update.data);
15 break;
16 case 'clearNotifications':
17 state.notifications = [];
18 break;
19 }
20 });
21 }
22 }
23});
24
25// Usage:
26dispatch(batchUpdate([
27 { type: 'updateUser', data: { name: 'John' } },
28 { type: 'updateSettings', data: { theme: 'dark' } },
29 { type: 'clearNotifications' }
30]));1const conditionalActions = createSlice({
2 name: 'conditional',
3 initialState: { count: 0, max: 10 },
4 reducers: {
5 incrementIfPossible: (state, action) => {
6 // Conditional logic in reducer
7 if (state.count < state.max) {
8 state.count += action.payload || 1;
9 }
10 },
11
12 resetIfOver: (state, action) => {
13 if (state.count >= action.payload.threshold) {
14 state.count = 0;
15 }
16 }
17 }
18});
19
20// Alternative: Conditional dispatch in components
21const handleIncrement = () => {
22 const state = store.getState();
23 if (state.conditional.count < state.conditional.max) {
24 dispatch(incrementIfPossible());
25 }
26};Reducers are the heart of Redux - they specify how the application state changes in response to actions. With RTK and Immer, writing reducers becomes more intuitive while maintaining immutability.
1// ✅ Pure reducer - same input always produces same output
2function counterReducer(state = 0, action) {
3 switch (action.type) {
4 case 'INCREMENT':
5 return state + 1;
6 case 'DECREMENT':
7 return state - 1;
8 case 'RESET':
9 return 0;
10 default:
11 return state; // Always return current state for unknown actions
12 }
13}
14
15// ❌ Impure reducer - contains side effects
16function impureReducer(state = 0, action) {
17 switch (action.type) {
18 case 'INCREMENT':
19 console.log('Incrementing'); // Side effect: logging
20 Math.random(); // Non-deterministic
21 localStorage.setItem('count', state + 1); // Side effect: storage
22 return state + 1;
23 }
24}1import { createSlice } from '@reduxjs/toolkit';
2
3const shopSlice = createSlice({
4 name: 'shop',
5 initialState: {
6 products: [],
7 cart: {
8 items: [],
9 total: 0,
10 tax: 0,
11 shipping: 0
12 },
13 user: {
14 preferences: {
15 currency: 'USD',
16 notifications: {
17 orderUpdates: true,
18 promotions: false
19 }
20 }
21 }
22 },
23 reducers: {
24 // Immer allows "direct mutation" syntax
25 addToCart: (state, action) => {
26 const { productId, quantity = 1 } = action.payload;
27 const existingItem = state.cart.items.find(item => item.productId === productId);
28
29 if (existingItem) {
30 // Direct mutation - Immer handles immutability
31 existingItem.quantity += quantity;
32 } else {
33 // Direct push - Immer creates new array
34 state.cart.items.push({
35 productId,
36 quantity,
37 addedAt: Date.now()
38 });
39 }
40
41 // Recalculate total
42 state.cart.total = state.cart.items.reduce((sum, item) => {
43 const product = state.products.find(p => p.id === item.productId);
44 return sum + (product ? product.price * item.quantity : 0);
45 }, 0);
46 },
47
48 updateUserPreference: (state, action) => {
49 const { path, value } = action.payload;
50 // Deep nested updates made simple
51 if (path === 'currency') {
52 state.user.preferences.currency = value;
53 } else if (path.startsWith('notifications.')) {
54 const notificationType = path.split('.')[1];
55 state.user.preferences.notifications[notificationType] = value;
56 }
57 },
58
59 // Complex array operations
60 reorderCartItems: (state, action) => {
61 const { fromIndex, toIndex } = action.payload;
62 const [movedItem] = state.cart.items.splice(fromIndex, 1);
63 state.cart.items.splice(toIndex, 0, movedItem);
64 }
65 }
66});1// Normalized state structure for better performance
2const initialState = {
3 entities: {
4 users: {},
5 posts: {},
6 comments: {}
7 },
8 relations: {
9 postsByUser: {},
10 commentsByPost: {}
11 },
12 ui: {
13 selectedUserId: null,
14 selectedPostId: null
15 }
16};
17
18const blogSlice = createSlice({
19 name: 'blog',
20 initialState,
21 reducers: {
22 usersLoaded: (state, action) => {
23 action.payload.forEach(user => {
24 state.entities.users[user.id] = user;
25 });
26 },
27
28 postAdded: (state, action) => {
29 const post = action.payload;
30
31 // Add to entities
32 state.entities.posts[post.id] = post;
33
34 // Update relations
35 if (!state.relations.postsByUser[post.authorId]) {
36 state.relations.postsByUser[post.authorId] = [];
37 }
38 state.relations.postsByUser[post.authorId].push(post.id);
39 },
40
41 commentAdded: (state, action) => {
42 const comment = action.payload;
43
44 state.entities.comments[comment.id] = comment;
45
46 if (!state.relations.commentsByPost[comment.postId]) {
47 state.relations.commentsByPost[comment.postId] = [];
48 }
49 state.relations.commentsByPost[comment.postId].push(comment.id);
50
51 // Update comment count in post
52 if (state.entities.posts[comment.postId]) {
53 state.entities.posts[comment.postId].commentCount++;
54 }
55 }
56 }
57});1// Compose reducers for different parts of state
2const formSlice = createSlice({
3 name: 'form',
4 initialState: {
5 personal: {
6 firstName: '',
7 lastName: '',
8 email: '',
9 errors: {}
10 },
11 address: {
12 street: '',
13 city: '',
14 zipCode: '',
15 errors: {}
16 },
17 validation: {
18 isValid: false,
19 touchedFields: new Set(),
20 validationRules: {}
21 }
22 },
23 reducers: {
24 // Handle different form sections
25 personalInfoUpdated: (state, action) => {
26 const { field, value } = action.payload;
27 state.personal[field] = value;
28
29 // Clear previous error
30 delete state.personal.errors[field];
31
32 // Mark field as touched
33 state.validation.touchedFields.add(`personal.${field}`);
34
35 // Validate field
36 const error = validatePersonalField(field, value);
37 if (error) {
38 state.personal.errors[field] = error;
39 }
40
41 // Update overall validation status
42 state.validation.isValid =
43 Object.keys(state.personal.errors).length === 0 &&
44 Object.keys(state.address.errors).length === 0;
45 },
46
47 addressUpdated: (state, action) => {
48 const { field, value } = action.payload;
49 state.address[field] = value;
50
51 delete state.address.errors[field];
52 state.validation.touchedFields.add(`address.${field}`);
53
54 const error = validateAddressField(field, value);
55 if (error) {
56 state.address.errors[field] = error;
57 }
58
59 state.validation.isValid =
60 Object.keys(state.personal.errors).length === 0 &&
61 Object.keys(state.address.errors).length === 0;
62 },
63
64 formReset: (state) => {
65 // Reset to initial state
66 state.personal = { firstName: '', lastName: '', email: '', errors: {} };
67 state.address = { street: '', city: '', zipCode: '', errors: {} };
68 state.validation.isValid = false;
69 state.validation.touchedFields.clear();
70 }
71 }
72});
73
74// Helper validation functions (would be defined elsewhere)
75function validatePersonalField(field, value) {
76 switch (field) {
77 case 'email':
78 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : 'Invalid email';
79 case 'firstName':
80 case 'lastName':
81 return value.trim().length >= 2 ? null : 'Must be at least 2 characters';
82 default:
83 return null;
84 }
85}
86
87function validateAddressField(field, value) {
88 switch (field) {
89 case 'zipCode':
90 return /^\d{5}(-\d{4})?$/.test(value) ? null : 'Invalid ZIP code';
91 case 'street':
92 case 'city':
93 return value.trim().length > 0 ? null : 'This field is required';
94 default:
95 return null;
96 }
97}1const apiSlice = createSlice({
2 name: 'api',
3 initialState: {
4 data: {},
5 loading: {},
6 errors: {},
7 lastUpdated: {}
8 },
9 reducers: {
10 apiCallStarted: (state, action) => {
11 const { endpoint } = action.payload;
12 state.loading[endpoint] = true;
13 delete state.errors[endpoint];
14 },
15
16 apiCallSucceeded: (state, action) => {
17 const { endpoint, data } = action.payload;
18 state.data[endpoint] = data;
19 state.loading[endpoint] = false;
20 state.lastUpdated[endpoint] = Date.now();
21 },
22
23 apiCallFailed: (state, action) => {
24 const { endpoint, error } = action.payload;
25 state.errors[endpoint] = {
26 message: error.message,
27 code: error.code,
28 timestamp: Date.now(),
29 retry: error.retry || false
30 };
31 state.loading[endpoint] = false;
32 },
33
34 errorCleared: (state, action) => {
35 const { endpoint } = action.payload;
36 delete state.errors[endpoint];
37 }
38 }
39});1import { counterSlice } from './counterSlice';
2
3const { increment, decrement, reset } = counterSlice.actions;
4const counterReducer = counterSlice.reducer;
5
6describe('counterSlice', () => {
7 const initialState = { value: 0, history: [] };
8
9 test('should handle increment', () => {
10 const action = increment();
11 const newState = counterReducer(initialState, action);
12
13 expect(newState.value).toBe(1);
14 expect(newState.history).toContain(0);
15 });
16
17 test('should handle multiple increments', () => {
18 let state = initialState;
19 state = counterReducer(state, increment());
20 state = counterReducer(state, increment());
21
22 expect(state.value).toBe(2);
23 expect(state.history).toEqual([0, 1]);
24 });
25
26 test('should not mutate original state', () => {
27 const action = increment();
28 const newState = counterReducer(initialState, action);
29
30 expect(newState).not.toBe(initialState);
31 expect(initialState.value).toBe(0); // Original unchanged
32 });
33});The Redux store is the central hub that holds the application state, orchestrates actions through reducers, and notifies subscribers of changes. Understanding store architecture is key to scalable applications.
1// features/auth/authSlice.js
2const authSlice = createSlice({
3 name: 'auth',
4 initialState: {
5 user: null,
6 token: null,
7 permissions: [],
8 sessionTimeout: null
9 },
10 reducers: { /* auth reducers */ }
11});
12
13// features/dashboard/dashboardSlice.js
14const dashboardSlice = createSlice({
15 name: 'dashboard',
16 initialState: {
17 widgets: [],
18 layout: 'grid',
19 filters: {},
20 dateRange: { start: null, end: null }
21 },
22 reducers: { /* dashboard reducers */ }
23});
24
25// store/index.js - Combine feature slices
26export const store = configureStore({
27 reducer: {
28 auth: authSlice.reducer,
29 dashboard: dashboardSlice.reducer,
30 notifications: notificationsSlice.reducer,
31 settings: settingsSlice.reducer
32 },
33
34 // State shape:
35 // {
36 // auth: { user: null, token: null, ... },
37 // dashboard: { widgets: [], layout: 'grid', ... },
38 // notifications: { items: [], unreadCount: 0 },
39 // settings: { theme: 'light', language: 'en' }
40 // }
41});1import { createEntityAdapter } from '@reduxjs/toolkit';
2
3// Entity adapters for normalized data
4const usersAdapter = createEntityAdapter({
5 selectId: (user) => user.id,
6 sortComparer: (a, b) => a.name.localeCompare(b.name)
7});
8
9const postsAdapter = createEntityAdapter({
10 selectId: (post) => post.id,
11 sortComparer: (a, b) => b.createdAt.localeCompare(a.createdAt)
12});
13
14const usersSlice = createSlice({
15 name: 'users',
16 initialState: usersAdapter.getInitialState({
17 loading: false,
18 error: null
19 }),
20 reducers: {
21 usersLoading: (state) => {
22 state.loading = true;
23 },
24 usersReceived: (state, action) => {
25 state.loading = false;
26 usersAdapter.setAll(state, action.payload);
27 },
28 userAdded: usersAdapter.addOne,
29 userUpdated: usersAdapter.updateOne,
30 userRemoved: usersAdapter.removeOne
31 }
32});
33
34// Export entity selectors
35export const {
36 selectAll: selectAllUsers,
37 selectById: selectUserById,
38 selectIds: selectUserIds,
39 selectEntities: selectUserEntities,
40 selectTotal: selectUsersTotal
41} = usersAdapter.getSelectors((state) => state.users);
42
43// Advanced selectors with createSelector
44export const selectActiveUsers = createSelector(
45 [selectAllUsers],
46 (users) => users.filter(user => user.status === 'active')
47);
48
49export const selectUsersByRole = createSelector(
50 [selectAllUsers, (state, role) => role],
51 (users, role) => users.filter(user => user.role === role)
52);1// Custom middleware for different concerns
2const analyticsMiddleware = (store) => (next) => (action) => {
3 // Track user actions
4 if (action.type.startsWith('user/')) {
5 analytics.track(action.type, {
6 userId: store.getState().auth.user?.id,
7 timestamp: Date.now(),
8 sessionId: store.getState().auth.sessionId
9 });
10 }
11
12 return next(action);
13};
14
15const errorReportingMiddleware = (store) => (next) => (action) => {
16 try {
17 return next(action);
18 } catch (error) {
19 // Report errors to external service
20 errorReporter.captureException(error, {
21 action,
22 state: store.getState(),
23 timestamp: Date.now()
24 });
25
26 // Dispatch error action
27 store.dispatch({
28 type: 'app/errorOccurred',
29 payload: {
30 error: error.message,
31 action: action.type,
32 timestamp: Date.now()
33 }
34 });
35
36 throw error; // Re-throw for debugging
37 }
38};
39
40const performanceMiddleware = (store) => (next) => (action) => {
41 const start = performance.now();
42 const result = next(action);
43 const duration = performance.now() - start;
44
45 if (duration > 16) { // Longer than one frame
46 console.warn(`Slow action: ${action.type} (${duration.toFixed(2)}ms)`);
47 }
48
49 return result;
50};
51
52const store = configureStore({
53 reducer: rootReducer,
54 middleware: (getDefaultMiddleware) =>
55 getDefaultMiddleware({
56 serializableCheck: {
57 ignoredActions: ['persist/PERSIST']
58 }
59 })
60 .concat(analyticsMiddleware)
61 .concat(errorReportingMiddleware)
62 .concat(performanceMiddleware)
63});1import { persistStore, persistReducer } from 'redux-persist';
2import storage from 'redux-persist/lib/storage';
3
4// Selective persistence configuration
5const persistConfig = {
6 key: 'root',
7 storage,
8 whitelist: ['auth', 'settings'], // Only persist these slices
9 blacklist: ['api', 'ui'], // Never persist these
10
11 // Transform data before persistence
12 transforms: [
13 // Remove sensitive data before storage
14 {
15 in: (inboundState, key) => {
16 if (key === 'auth' && inboundState.token) {
17 return {
18 ...inboundState,
19 token: encrypt(inboundState.token) // Encrypt sensitive data
20 };
21 }
22 return inboundState;
23 },
24 out: (outboundState, key) => {
25 if (key === 'auth' && outboundState.token) {
26 return {
27 ...outboundState,
28 token: decrypt(outboundState.token) // Decrypt on load
29 };
30 }
31 return outboundState;
32 }
33 }
34 ]
35};
36
37const persistedReducer = persistReducer(persistConfig, rootReducer);
38
39export const store = configureStore({
40 reducer: persistedReducer,
41 middleware: (getDefaultMiddleware) =>
42 getDefaultMiddleware({
43 serializableCheck: {
44 ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
45 }
46 })
47});
48
49export const persistor = persistStore(store);
50
51// In your app root
52import { PersistGate } from 'redux-persist/integration/react';
53
54function App() {
55 return (
56 <Provider store={store}>
57 <PersistGate loading={<LoadingSpinner />} persistor={persistor}>
58 <MainApp />
59 </PersistGate>
60 </Provider>
61 );
62}1// Dynamic reducer injection for code splitting
2const store = configureStore({
3 reducer: {
4 app: appReducer, // Always present
5 // Other reducers added dynamically
6 }
7});
8
9// Store registry for tracking injected reducers
10store.injectedReducers = {};
11
12// Function to inject reducers dynamically
13store.injectReducer = (key, reducer) => {
14 if (store.injectedReducers[key]) {
15 return; // Already injected
16 }
17
18 store.injectedReducers[key] = reducer;
19
20 store.replaceReducer(
21 combineReducers({
22 app: appReducer,
23 ...store.injectedReducers
24 })
25 );
26};
27
28// Usage in lazy-loaded components
29const LazyDashboard = lazy(() =>
30 import('./Dashboard').then(module => {
31 // Inject dashboard reducer when component loads
32 store.injectReducer('dashboard', dashboardReducer);
33 return module;
34 })
35);1// ✅ Optimized state shape
2const optimizedState = {
3 // Normalized entities
4 entities: {
5 users: { 1: { id: 1, name: 'John' } },
6 posts: { 1: { id: 1, title: 'Post 1', authorId: 1 } }
7 },
8
9 // UI state separate from data
10 ui: {
11 users: {
12 selectedId: 1,
13 filterText: '',
14 sortBy: 'name',
15 isLoading: false
16 },
17 posts: {
18 currentPage: 1,
19 itemsPerPage: 10,
20 viewMode: 'grid'
21 }
22 },
23
24 // Computed indices for fast lookups
25 indices: {
26 postsByAuthor: { 1: [1, 2, 3] },
27 usersByRole: { admin: [1], user: [2, 3] }
28 }
29};
30
31// ❌ Poor state shape
32const poorState = {
33 users: [
34 {
35 id: 1,
36 name: 'John',
37 posts: [ // Denormalized - hard to update
38 { id: 1, title: 'Post 1' },
39 { id: 2, title: 'Post 2' }
40 ],
41 uiState: { selected: true } // UI mixed with data
42 }
43 ]
44};1import { createSelector } from '@reduxjs/toolkit';
2
3// Memoized selectors prevent unnecessary recalculations
4const selectExpensiveComputation = createSelector(
5 [
6 state => state.data.items,
7 state => state.filters.category,
8 state => state.filters.priceRange,
9 state => state.sorting.field,
10 state => state.sorting.direction
11 ],
12 (items, category, priceRange, sortField, sortDirection) => {
13 console.log('Recalculating expensive computation'); // Only logs when inputs change
14
15 return items
16 .filter(item =>
17 (!category || item.category === category) &&
18 item.price >= priceRange.min &&
19 item.price <= priceRange.max
20 )
21 .sort((a, b) => {
22 const aVal = a[sortField];
23 const bVal = b[sortField];
24 const multiplier = sortDirection === 'desc' ? -1 : 1;
25 return aVal < bVal ? -1 * multiplier : aVal > bVal ? 1 * multiplier : 0;
26 });
27 }
28);
29
30// Parametric selectors for dynamic arguments
31const makeSelectUserPosts = () => createSelector(
32 [
33 state => state.posts.entities,
34 (state, userId) => userId
35 ],
36 (posts, userId) => Object.values(posts).filter(post => post.authorId === userId)
37);
38
39// Usage with React
40const UserPosts = ({ userId }) => {
41 const selectUserPosts = useMemo(makeSelectUserPosts, []);
42 const posts = useSelector(state => selectUserPosts(state, userId));
43
44 return (
45 <div>
46 {posts.map(post => <PostCard key={post.id} post={post} />)}
47 </div>
48 );
49};1// Store testing utilities
2export function createTestStore(initialState = {}) {
3 const store = configureStore({
4 reducer: rootReducer,
5 preloadedState: initialState,
6 middleware: (getDefaultMiddleware) =>
7 getDefaultMiddleware({
8 // Disable middleware that interferes with testing
9 serializableCheck: false,
10 immutableCheck: false
11 })
12 });
13
14 return store;
15}
16
17// Integration test example
18describe('Store Integration', () => {
19 let store;
20
21 beforeEach(() => {
22 store = createTestStore({
23 auth: { user: null, token: null },
24 todos: { items: [] }
25 });
26 });
27
28 test('should handle user login flow', () => {
29 // Dispatch login action
30 store.dispatch(loginStarted());
31 expect(store.getState().auth.loading).toBe(true);
32
33 // Simulate successful login
34 const user = { id: 1, name: 'John' };
35 const token = 'abc123';
36 store.dispatch(loginSucceeded({ user, token }));
37
38 const state = store.getState();
39 expect(state.auth.user).toEqual(user);
40 expect(state.auth.token).toBe(token);
41 expect(state.auth.loading).toBe(false);
42 });
43
44 test('should maintain state consistency', () => {
45 // Add multiple todos
46 store.dispatch(todoAdded('Task 1'));
47 store.dispatch(todoAdded('Task 2'));
48 store.dispatch(todoToggled(1)); // Toggle first todo
49
50 const state = store.getState();
51 expect(state.todos.items).toHaveLength(2);
52 expect(state.todos.items[0].completed).toBe(true);
53 expect(state.todos.items[1].completed).toBe(false);
54 });
55});Your progress is saved in this browser, on this device only.