JavaScript
Managing State in React with Redux Toolkit: Advanced Patterns
Redux Toolkit (RTK) has revolutionized state management in React applications by reducing boilerplate and providing sensible defaults. While basic usage is straightforward, mastering advanced patterns can elevate your state management to production-grade quality. Here’s a deep dive into professional techniques used by top React developers.
1. Dynamic Reducer Injection
Problem:
Loading all reducers upfront hurts performance in large apps
Solution:
// store.js
import { configureStore } from '@reduxjs/toolkit'
import { createReducerManager } from './reducerManager'
const staticReducers = {
users: usersReducer,
auth: authReducer
}
export function setupStore(initialState) {
const reducerManager = createReducerManager(staticReducers)
const store = configureStore({
reducer: reducerManager.reduce,
preloadedState: initialState
})
store.reducerManager = reducerManager
return store
}
// reducerManager.js
export function createReducerManager(initialReducers) {
const reducers = { ...initialReducers }
let combinedReducer = combineReducers(reducers)
let keysToRemove = []
return {
reduce: (state, action) => {
if (keysToRemove.length > 0) {
state = { ...state }
keysToRemove.forEach(key => delete state[key])
keysToRemove = []
}
return combinedReducer(state, action)
},
add: (key, reducer) => {
reducers[key] = reducer
combinedReducer = combineReducers(reducers)
},
remove: key => {
keysToRemove.push(key)
delete reducers[key]
}
}
}Usage in Components:
useEffect(() => {
store.reducerManager.add('dynamicFeature', dynamicReducer)
return () => {
store.reducerManager.remove('dynamicFeature')
}
}, [])2. Normalized State with Entity Adapter
Problem:
Nested/denormalized state causes performance issues
Solution:
import { createEntityAdapter } from '@reduxjs/toolkit'
const usersAdapter = createEntityAdapter({
selectId: user => user.id,
sortComparer: (a, b) => a.name.localeCompare(b.name)
})
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {
userAdded: usersAdapter.addOne,
userUpdated: usersAdapter.updateOne,
userRemoved: usersAdapter.removeOne,
usersReceived(state, action) {
usersAdapter.setAll(state, action.payload)
}
}
})
// Selectors
export const {
selectAll: selectAllUsers,
selectById: selectUserById,
selectIds: selectUserIds
} = usersAdapter.getSelectors(state => state.users)Benefits:
- Automatic memoization
- Optimized updates
- Built-in CRUD operations
3. Advanced Middleware Patterns
Action Sequencing
const sequenceMiddleware = storeAPI => next => action => {
if (action.meta?.sequenceId) {
const state = storeAPI.getState()
const lastSequence = state.lastSequenceId || 0
if (action.meta.sequenceId <= lastSequence) {
return // Ignore out-of-order actions
}
}
return next(action)
}Optimistic Updates with Rollback
import { createAsyncThunk, nanoid } from '@reduxjs/toolkit'
const updateUser = createAsyncThunk(
'users/update',
async (userData, { dispatch, getState }) => {
const transactionId = nanoid()
dispatch({
type: 'users/optimisticUpdate',
payload: { ...userData, pending: true },
meta: { transactionId }
})
try {
const response = await api.updateUser(userData)
return { ...response, transactionId }
} catch (error) {
return { error, transactionId }
}
},
{
condition: (_, { getState }) => {
const { isUpdating } = getState().users
return !isUpdating // Skip if already updating
}
}
)
// In extraReducers:
.addCase(updateUser.fulfilled, (state, action) => {
const { transactionId, ...user } = action.payload
usersAdapter.updateOne(state, {
id: user.id,
changes: { ...user, pending: false }
})
})
.addCase(updateUser.rejected, (state, action) => {
const { transactionId } = action.meta.arg
// Rollback logic
})4. Selector Memoization Patterns
Reselect with RTK
import { createSelector } from '@reduxjs/toolkit'
const selectUsers = state => state.users.entities
const selectActiveFilter = state => state.users.activeFilter
export const selectFilteredUsers = createSelector(
[selectUsers, selectActiveFilter],
(users, filter) => {
return users.filter(user =>
filter === 'all' || user.status === filter
)
}
)Dynamic Selector Factory
export const makeSelectUserById = () =>
createSelector(
[selectUsers, (_, id) => id],
(users, id) => users[id]
)
// Usage:
const selectUser = makeSelectUserById()
const user = useSelector(state => selectUser(state, userId))5. Server-State Synchronization with RTK Query
Advanced Cache Management
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: builder => ({
getUsers: builder.query({
query: () => 'users',
providesTags: ['User']
}),
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `users/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
'Post' // Also invalidate all posts
],
onQueryStarted: async (arg, { dispatch, queryFulfilled }) => {
// Optimistic update
const patchResult = dispatch(
api.util.updateQueryData('getUser', arg.id, draft => {
Object.assign(draft, arg)
})
)
try {
await queryFulfilled
} catch {
patchResult.undo() // Rollback
}
}
})
})
})6. State Persistence Strategies
Rehydration with Redux-Persist
import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = {
key: 'root',
storage,
whitelist: ['auth'],
transforms: [encryptTransform] // For sensitive data
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
reducer: persistedReducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
}
})
})
export const persistor = persistStore(store)Differential Persistence
const advancedPersistConfig = {
key: 'userSettings',
storage,
serialize: data => {
const { temporary, ...persistent } = data
return JSON.stringify(persistent)
},
deserialize: str => {
const persistent = JSON.parse(str)
return { ...persistent, temporary: {} }
}
}7. Testing Strategies
Middleware Testing
test('auth middleware processes token', () => {
const store = mockStore({})
const next = jest.fn()
const action = { type: 'LOGIN', payload: { token: 'abc123' } }
authMiddleware(store)(next)(action)
expect(localStorage.setItem).toHaveBeenCalledWith('token', 'abc123')
expect(next).toHaveBeenCalledWith(action)
})Reducer Composition Testing
describe('nested reducers', () => {
let store
beforeEach(() => {
store = setupStore()
store.reducerManager.add('dynamicFeature', dynamicReducer)
})
test('handles cross-slice actions', () => {
store.dispatch({ type: 'TRIGGER_UPDATE' })
expect(store.getState().dynamicFeature).toEqual(/* expected */)
})
})Key Takeaways
- Dynamic injection enables code splitting for reducers
- Entity adapter solves normalization challenges
- Middleware patterns handle complex side effects
- Advanced selectors optimize performance
- RTK Query simplifies server state management
- Persistence strategies balance UX and security
- Testing techniques ensure reliability
These patterns represent professional-grade Redux architecture used in large-scale applications. Implement them progressively as your application’s complexity grows.

