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

  1. Dynamic injection enables code splitting for reducers
  2. Entity adapter solves normalization challenges
  3. Middleware patterns handle complex side effects
  4. Advanced selectors optimize performance
  5. RTK Query simplifies server state management
  6. Persistence strategies balance UX and security
  7. 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.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button