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
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
anddispatch
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