Dynamic Slice Injection in Redux Toolkit for Micro-Frontend Architectures

Traditional Redux apps define all slices at app initialization. But when you're building dynamic applications like micro-frontends or plugin-based systems, you can't know all reducers ahead of time. Solution? **Dynamically inject slices at runtime** — and Redux Toolkit makes this surprisingly achievable. Why Inject Slices Dynamically? Use cases include: Loading feature modules only when needed (code splitting) Building micro-frontend systems where each team manages its own Redux slice Adding plugins or extensions post-deployment without redeploying the main app Step 1: Create a Reducer Manager A reducer manager dynamically adds and removes slices: // src/store/reducerManager.js export function createReducerManager(initialReducers) { const reducers = { ...initialReducers }; let combinedReducer = combineReducers(reducers); return { getReducerMap: () => reducers, reduce: (state, action) => combinedReducer(state, action), add: (key, reducer) => { if (!key || reducers[key]) return; reducers[key] = reducer; combinedReducer = combineReducers(reducers); }, remove: (key) => { if (!key || !reducers[key]) return; delete reducers[key]; combinedReducer = combineReducers(reducers); }, }; } Step 2: Set Up Store with Reducer Manager Wire it up when configuring the store: // src/store/store.js import { configureStore } from '@reduxjs/toolkit'; import { createReducerManager } from './reducerManager'; import baseReducer from './baseReducer'; export function configureAppStore() { const reducerManager = createReducerManager({ base: baseReducer }); const store = configureStore({ reducer: reducerManager.reduce, }); store.reducerManager = reducerManager; return store; } Step 3: Inject New Slices Dynamically At runtime, when you load a feature module: // src/features/chat/chatSlice.js import { createSlice } from '@reduxjs/toolkit'; const chatSlice = createSlice({ name: 'chat', initialState: { messages: [] }, reducers: { sendMessage(state, action) { state.messages.push(action.payload); }, }, }); export default chatSlice.reducer; export const { sendMessage } = chatSlice.actions; // Somewhere when loading the Chat feature import chatReducer from './features/chat/chatSlice'; import { store } from './store/store'; // Assume already configured store.reducerManager.add('chat', chatReducer); How It Works reducerManager.add() updates the store’s reducer on the fly. New slices become immediately available in useSelector and dispatch calls. You can remove slices too, e.g., when a micro-frontend unmounts. Pros and Cons ✅ Pros Massive scalability for large apps Load reducers only when needed = smaller initial bundles Enables micro-frontend and plugin-based architectures ⚠️ Cons Increased complexity in debugging and DevTools tracking Risk of stale slices if not properly removed Harder to fully type with TypeScript without extra utilities

Apr 27, 2025 - 07:31
 0
Dynamic Slice Injection in Redux Toolkit for Micro-Frontend Architectures

Traditional Redux apps define all slices at app initialization. But when you're building dynamic applications like micro-frontends or plugin-based systems, you can't know all reducers ahead of time. Solution? **Dynamically inject slices at runtime** — and Redux Toolkit makes this surprisingly achievable.

Why Inject Slices Dynamically?

Use cases include:

  • Loading feature modules only when needed (code splitting)
  • Building micro-frontend systems where each team manages its own Redux slice
  • Adding plugins or extensions post-deployment without redeploying the main app

Step 1: Create a Reducer Manager

A reducer manager dynamically adds and removes slices:

// src/store/reducerManager.js
export function createReducerManager(initialReducers) {
  const reducers = { ...initialReducers };
  let combinedReducer = combineReducers(reducers);

  return {
    getReducerMap: () => reducers,
    reduce: (state, action) => combinedReducer(state, action),
    add: (key, reducer) => {
      if (!key || reducers[key]) return;
      reducers[key] = reducer;
      combinedReducer = combineReducers(reducers);
    },
    remove: (key) => {
      if (!key || !reducers[key]) return;
      delete reducers[key];
      combinedReducer = combineReducers(reducers);
    },
  };
}

Step 2: Set Up Store with Reducer Manager

Wire it up when configuring the store:

// src/store/store.js
import { configureStore } from '@reduxjs/toolkit';
import { createReducerManager } from './reducerManager';
import baseReducer from './baseReducer';

export function configureAppStore() {
  const reducerManager = createReducerManager({ base: baseReducer });

  const store = configureStore({
    reducer: reducerManager.reduce,
  });

  store.reducerManager = reducerManager;
  return store;
}

Step 3: Inject New Slices Dynamically

At runtime, when you load a feature module:

// src/features/chat/chatSlice.js
import { createSlice } from '@reduxjs/toolkit';

const chatSlice = createSlice({
  name: 'chat',
  initialState: { messages: [] },
  reducers: {
    sendMessage(state, action) {
      state.messages.push(action.payload);
    },
  },
});

export default chatSlice.reducer;
export const { sendMessage } = chatSlice.actions;
// Somewhere when loading the Chat feature
import chatReducer from './features/chat/chatSlice';
import { store } from './store/store'; // Assume already configured

store.reducerManager.add('chat', chatReducer);

How It Works

  • reducerManager.add() updates the store’s reducer on the fly.
  • New slices become immediately available in useSelector and dispatch calls.
  • You can remove slices too, e.g., when a micro-frontend unmounts.

Pros and Cons

✅ Pros

  • Massive scalability for large apps
  • Load reducers only when needed = smaller initial bundles
  • Enables micro-frontend and plugin-based architectures

⚠️ Cons

  • Increased complexity in debugging and DevTools tracking
  • Risk of stale slices if not properly removed
  • Harder to fully type with TypeScript without extra utilities