The official, opinionated, batteries-included toolset for efficient Redux development
—
Redux Toolkit provides advanced middleware for side effects, dynamic middleware injection, and development-time invariants.
Creates middleware for running side effects in response to actions or state changes with precise lifecycle control.
/**
* Creates middleware for running side effects in response to actions
* @param options - Configuration options for the listener middleware
* @returns ListenerMiddlewareInstance with middleware and control methods
*/
function createListenerMiddleware<
StateType = unknown,
DispatchType extends Dispatch = AppDispatch,
ExtraArgument = unknown
>(options?: CreateListenerMiddlewareOptions<ExtraArgument>): ListenerMiddlewareInstance<StateType, DispatchType, ExtraArgument>;
interface CreateListenerMiddlewareOptions<ExtraArgument> {
/** Additional context passed to listeners */
extra?: ExtraArgument;
/** Error handler for listener exceptions */
onError?: ListenerErrorHandler;
}
type ListenerErrorHandler = (error: unknown, errorInfo: ListenerErrorInfo) => void;
interface ListenerErrorInfo {
runtimeInfo: {
listenerApi: ListenerApi<any, any, any>;
extraArgument: any;
};
}
interface ListenerMiddlewareInstance<StateType, DispatchType, ExtraArgument> {
/** The actual middleware function for store configuration */
middleware: Middleware<{}, StateType, DispatchType>;
/** Add a listener for actions or state changes */
startListening<ActionCreator extends TypedActionCreator<any>>(
options: ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument>
): UnsubscribeListener;
/** Remove a specific listener */
stopListening<ActionCreator extends TypedActionCreator<any>>(
options: ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument>
): boolean;
/** Remove all listeners */
clearListeners(): void;
}
interface ListenerOptions<ActionCreator, StateType, DispatchType, ExtraArgument> {
/** Action creator or action type to listen for */
actionCreator?: ActionCreator;
type?: string;
matcher?: ActionMatcher<any>;
predicate?: ListenerPredicate<StateType, DispatchType>;
/** The effect function to run */
effect: ListenerEffect<ActionCreator, StateType, DispatchType, ExtraArgument>;
}
type ListenerEffect<ActionCreator, StateType, DispatchType, ExtraArgument> = (
action: ActionCreator extends TypedActionCreator<infer Action> ? Action : AnyAction,
listenerApi: ListenerApi<StateType, DispatchType, ExtraArgument>
) => void | Promise<void>;
interface ListenerApi<StateType, DispatchType, ExtraArgument> {
/** Get current state */
getState(): StateType;
/** Get original state when action was dispatched */
getOriginalState(): StateType;
/** Dispatch actions */
dispatch: DispatchType;
/** Extra argument from middleware options */
extra: ExtraArgument;
/** Unique request ID for this listener execution */
requestId: string;
/** Take future actions that match conditions */
take: TakePattern<StateType, DispatchType>;
/** Cancel the current listener */
cancelActiveListeners(): void;
/** AbortController signal for cancellation */
signal: AbortSignal;
/** Fork async tasks */
fork(executor: (forkApi: ForkApi<StateType, DispatchType, ExtraArgument>) => void | Promise<void>): TaskAbortError;
/** Delay execution */
delay(ms: number): Promise<void>;
/** Pause until condition is met */
condition(predicate: ListenerPredicate<StateType, DispatchType>, timeout?: number): Promise<boolean>;
/** Unsubscribe this listener */
unsubscribe(): void;
/** Subscribe to actions without removing listener */
subscribe(): void;
}Usage Examples:
import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit';
// Create listener middleware
const listenerMiddleware = createListenerMiddleware({
extra: { api, analytics }
});
// Simple action listener
listenerMiddleware.startListening({
actionCreator: userLoggedIn,
effect: async (action, listenerApi) => {
const { dispatch, extra } = listenerApi;
// Side effect: track login
extra.analytics.track('user_login', {
userId: action.payload.id,
timestamp: Date.now()
});
// Load user preferences
dispatch(loadUserPreferences(action.payload.id));
}
});
// Multiple action types
listenerMiddleware.startListening({
matcher: isAnyOf(userLoggedIn, userRegistered),
effect: (action, listenerApi) => {
// Handle both login and registration
listenerApi.extra.analytics.track('user_authenticated');
}
});
// State-based listener with predicate
listenerMiddleware.startListening({
predicate: (action, currentState, previousState) => {
return currentState.user.isAuthenticated && !previousState.user.isAuthenticated;
},
effect: async (action, listenerApi) => {
// User became authenticated
await listenerApi.extra.api.setupUserSession();
}
});
// Complex async workflow
listenerMiddleware.startListening({
actionCreator: startDataSync,
effect: async (action, listenerApi) => {
const { fork, delay, take, condition, signal } = listenerApi;
// Fork background sync process
const syncTask = fork(async (forkApi) => {
while (!signal.aborted) {
try {
await forkApi.extra.api.syncData();
await forkApi.delay(30000); // Sync every 30 seconds
} catch (error) {
forkApi.dispatch(syncErrorOccurred(error));
break;
}
}
});
// Wait for stop signal
await take(stopDataSync);
// Cancel the sync task
syncTask.cancel();
}
});
// Add to store
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().prepend(listenerMiddleware.middleware)
});Creates middleware that allows adding and removing other middleware at runtime.
/**
* Creates middleware that can have other middleware added at runtime
* @returns DynamicMiddlewareInstance with middleware and control methods
*/
function createDynamicMiddleware<
State = any,
DispatchType extends Dispatch<AnyAction> = Dispatch<AnyAction>
>(): DynamicMiddlewareInstance<State, DispatchType>;
interface DynamicMiddlewareInstance<State, DispatchType> {
/** The actual middleware function for store configuration */
middleware: Middleware<{}, State, DispatchType>;
/** Add middleware programmatically */
addMiddleware(...middlewares: Middleware<any, State, DispatchType>[]): void;
/** Action creator to add middleware via dispatch */
withMiddleware(...middlewares: Middleware<any, State, DispatchType>[]): PayloadAction<Middleware<any, State, DispatchType>[]>;
/** Unique instance identifier */
instanceId: string;
}Usage Examples:
import { createDynamicMiddleware } from '@reduxjs/toolkit';
// Create dynamic middleware
const dynamicMiddleware = createDynamicMiddleware();
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(dynamicMiddleware.middleware)
});
// Add middleware programmatically
const analyticsMiddleware: Middleware = (store) => (next) => (action) => {
// Track actions
analytics.track(action.type, action.payload);
return next(action);
};
dynamicMiddleware.addMiddleware(analyticsMiddleware);
// Add middleware via action
store.dispatch(
dynamicMiddleware.withMiddleware(
loggerMiddleware,
crashReportingMiddleware
)
);
// Conditional middleware loading
if (process.env.NODE_ENV === 'development') {
dynamicMiddleware.addMiddleware(devOnlyMiddleware);
}
// Feature-based middleware
const featureSlice = createSlice({
name: 'feature',
initialState: { enabled: false },
reducers: {
featureEnabled: (state, action) => {
state.enabled = true;
// Add feature-specific middleware when enabled
}
},
extraReducers: (builder) => {
builder.addCase(featureEnabled, (state, action) => {
// This would be handled in a listener or component
});
}
});Enhanced dynamic middleware with React hook factories for component-specific middleware.
/**
* React-enhanced version of dynamic middleware with hook factories
* Available in @reduxjs/toolkit/react
*/
interface ReactDynamicMiddlewareInstance<State, DispatchType> extends DynamicMiddlewareInstance<State, DispatchType> {
/** Creates hook factory for specific React context */
createDispatchWithMiddlewareHookFactory(context?: React.Context<any>): CreateDispatchWithMiddlewareHook<State, DispatchType>;
/** Default hook factory */
createDispatchWithMiddlewareHook: CreateDispatchWithMiddlewareHook<State, DispatchType>;
}
interface CreateDispatchWithMiddlewareHook<State, DispatchType> {
(middlewares: Middleware<any, State, DispatchType>[]): DispatchType;
}Usage Examples:
// Available from @reduxjs/toolkit/react
import { createDynamicMiddleware } from '@reduxjs/toolkit/react';
const dynamicMiddleware = createDynamicMiddleware<RootState, AppDispatch>();
// In components
const MyComponent = () => {
const dispatchWithAnalytics = dynamicMiddleware.createDispatchWithMiddlewareHook([
analyticsMiddleware,
loggingMiddleware
]);
const handleAction = () => {
// This dispatch will go through the additional middleware
dispatchWithAnalytics(someAction());
};
return <button onClick={handleAction}>Action</button>;
};
// Component-specific middleware factory
const useComponentMiddleware = dynamicMiddleware.createDispatchWithMiddlewareHookFactory();
const FeatureComponent = () => {
const dispatch = useComponentMiddleware([
featureSpecificMiddleware,
performanceTrackingMiddleware
]);
// Use dispatch with component-specific middleware
};Middleware for development-time checking and debugging.
/**
* Middleware that detects mutations to state objects
* @param options - Configuration options
* @returns Middleware function that throws on mutations
*/
function createImmutableStateInvariantMiddleware<S = any>(
options?: ImmutableStateInvariantMiddlewareOptions
): Middleware<{}, S>;
interface ImmutableStateInvariantMiddlewareOptions {
/** Function to check if value should be considered immutable */
isImmutable?: (value: any) => boolean;
/** State paths to ignore during checking */
ignoredPaths?: string[];
/** Execution time warning threshold in ms */
warnAfter?: number;
}
/**
* Middleware that detects non-serializable values in actions and state
* @param options - Configuration options
* @returns Middleware function that warns about non-serializable values
*/
function createSerializableStateInvariantMiddleware(
options?: SerializableStateInvariantMiddlewareOptions
): Middleware<{}, any>;
interface SerializableStateInvariantMiddlewareOptions {
/** Action types to ignore */
ignoredActions?: string[];
/** Action property paths to ignore */
ignoredActionPaths?: string[];
/** State paths to ignore */
ignoredPaths?: string[];
/** Function to check if value is serializable */
isSerializable?: (value: any) => boolean;
/** Function to get object entries for checking */
getEntries?: (value: any) => [string, any][];
/** Execution time warning threshold in ms */
warnAfter?: number;
}
/**
* Middleware that validates action creators are called correctly
* @param options - Configuration options
* @returns Middleware function that warns about misused action creators
*/
function createActionCreatorInvariantMiddleware(
options?: ActionCreatorInvariantMiddlewareOptions
): Middleware;
interface ActionCreatorInvariantMiddlewareOptions {
/** Function to identify action creators */
isActionCreator?: (value: any) => boolean;
}Usage Examples:
import {
createImmutableStateInvariantMiddleware,
createSerializableStateInvariantMiddleware,
createActionCreatorInvariantMiddleware
} from '@reduxjs/toolkit';
// Custom immutability checking
const immutableCheckMiddleware = createImmutableStateInvariantMiddleware({
ignoredPaths: ['router.location', 'form.values'],
warnAfter: 32, // Warn if checking takes more than 32ms
isImmutable: (value) => {
// Custom immutability logic
return typeof value !== 'object' || value === null || Object.isFrozen(value);
}
});
// Custom serializability checking
const serializableCheckMiddleware = createSerializableStateInvariantMiddleware({
ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'],
ignoredPaths: ['auth.token', 'ui.fileUpload'],
ignoredActionPaths: ['payload.file', 'meta.timestamp'],
warnAfter: 64,
isSerializable: (value) => {
// Allow specific non-serializable types
return typeof value !== 'function' && !(value instanceof File);
}
});
// Action creator checking
const actionCreatorCheckMiddleware = createActionCreatorInvariantMiddleware({
isActionCreator: (value) => {
return typeof value === 'function' &&
typeof value.type === 'string' &&
typeof value.match === 'function';
}
});
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(
immutableCheckMiddleware,
serializableCheckMiddleware,
actionCreatorCheckMiddleware
)
});Utility functions for checking serialization and finding non-serializable values.
/**
* Checks if a value is "plain" (JSON-serializable)
* @param val - Value to check
* @returns True if value is directly serializable
*/
function isPlain(val: any): boolean;
/**
* Finds non-serializable values in nested objects
* @param value - Value to examine
* @param path - Current path in object (for error reporting)
* @param isSerializable - Function to check if value is serializable
* @param getEntries - Function to get object entries
* @param ignoredPaths - Paths to ignore during checking
* @param cache - WeakSet cache to avoid circular references
* @returns Non-serializable value info or false if all values are serializable
*/
function findNonSerializableValue(
value: unknown,
path?: string,
isSerializable?: (value: unknown) => boolean,
getEntries?: (value: unknown) => [string, any][],
ignoredPaths?: string[],
cache?: WeakSet<object>
): { keyPath: string; value: unknown } | false;
/**
* Checks if value is immutable (for development middleware)
* @param value - Value to check
* @returns True if value should be treated as immutable
*/
function isImmutableDefault(value: any): boolean;Usage Examples:
import {
isPlain,
findNonSerializableValue,
isImmutableDefault
} from '@reduxjs/toolkit';
// Check if values are serializable
console.log(isPlain({ name: 'John', age: 30 })); // true
console.log(isPlain(new Date())); // false
console.log(isPlain(function() {})); // false
// Find non-serializable values
const state = {
user: { name: 'John', age: 30 },
callback: () => console.log('hello'), // Non-serializable
timestamp: new Date() // Non-serializable
};
const nonSerializable = findNonSerializableValue(state);
if (nonSerializable) {
console.log(`Non-serializable value at ${nonSerializable.keyPath}:`, nonSerializable.value);
// Output: "Non-serializable value at callback: [Function]"
}
// Custom serialization check
const customCheck = findNonSerializableValue(
state,
'',
(value) => isPlain(value) || value instanceof Date // Allow Dates
);
// Check immutability
console.log(isImmutableDefault({})); // false (plain objects are mutable)
console.log(isImmutableDefault([])); // false (arrays are mutable)
console.log(isImmutableDefault('string')); // true (strings are immutable)Enhancer and utilities for automatic action batching to optimize React renders.
/**
* Store enhancer for automatic action batching
* @param options - Batching configuration options
* @returns Store enhancer function
*/
function autoBatchEnhancer(options?: AutoBatchOptions): StoreEnhancer;
interface AutoBatchOptions {
/** Batching strategy */
type?: 'tick' | 'timer' | 'callback' | ((action: Action) => boolean);
}
/** Symbol to mark actions for auto-batching */
const SHOULD_AUTOBATCH: unique symbol;
/**
* Prepare function for auto-batched actions
* @param payload - Action payload
* @returns Prepared action with auto-batch symbol
*/
function prepareAutoBatched<T>(payload: T): {
payload: T;
meta: { [SHOULD_AUTOBATCH]: true };
};Usage Examples:
import { autoBatchEnhancer, prepareAutoBatched, SHOULD_AUTOBATCH } from '@reduxjs/toolkit';
// Configure store with auto-batching
const store = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(
autoBatchEnhancer({
type: 'tick' // Batch actions until next tick
})
)
});
// Mark actions for batching
const userSlice = createSlice({
name: 'user',
initialState: { users: [], loading: false },
reducers: {
usersLoaded: {
reducer: (state, action) => {
state.users = action.payload;
state.loading = false;
},
prepare: prepareAutoBatched
},
// Alternative: manual batching metadata
bulkUpdateUsers: {
reducer: (state, action) => {
action.payload.forEach(update => {
const user = state.users.find(u => u.id === update.id);
if (user) {
Object.assign(user, update.changes);
}
});
},
prepare: (updates) => ({
payload: updates,
meta: { [SHOULD_AUTOBATCH]: true }
})
}
}
});
// Custom batching logic
const customBatchingStore = configureStore({
reducer: rootReducer,
enhancers: (getDefaultEnhancers) =>
getDefaultEnhancers().concat(
autoBatchEnhancer({
type: (action) => {
// Batch all async thunk fulfilled actions
return action.type.endsWith('/fulfilled');
}
})
)
});// Complex workflow with multiple stages
listenerMiddleware.startListening({
actionCreator: startWorkflow,
effect: async (action, listenerApi) => {
const { fork, delay, take, condition, cancelActiveListeners } = listenerApi;
try {
// Stage 1: Initialize
listenerApi.dispatch(workflowStageChanged('initializing'));
await listenerApi.delay(1000);
// Stage 2: Process data
listenerApi.dispatch(workflowStageChanged('processing'));
const processTask = fork(async (forkApi) => {
for (let i = 0; i < 10; i++) {
await forkApi.delay(500);
forkApi.dispatch(workflowProgressUpdated(i * 10));
}
});
// Wait for completion or cancellation
const result = await Promise.race([
processTask.result,
take(cancelWorkflow).then(() => 'cancelled')
]);
if (result === 'cancelled') {
processTask.cancel();
listenerApi.dispatch(workflowCancelled());
} else {
listenerApi.dispatch(workflowCompleted());
}
} catch (error) {
listenerApi.dispatch(workflowFailed(error));
}
}
});// Compose multiple middleware concerns
const createAppMiddleware = () => {
const listenerMiddleware = createListenerMiddleware();
const dynamicMiddleware = createDynamicMiddleware();
// Add core listeners
listenerMiddleware.startListening({
matcher: isAnyOf(userLoggedIn, userLoggedOut),
effect: (action, listenerApi) => {
// Authentication side effects
}
});
return {
listener: listenerMiddleware.middleware,
dynamic: dynamicMiddleware.middleware,
addListener: listenerMiddleware.startListening.bind(listenerMiddleware),
addMiddleware: dynamicMiddleware.addMiddleware.bind(dynamicMiddleware)
};
};
const appMiddleware = createAppMiddleware();
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(appMiddleware.listener, appMiddleware.dynamic)
});// Feature flag-based middleware loading
const createFeatureMiddleware = (features: FeatureFlags) => {
const middleware: Middleware[] = [];
if (features.analytics) {
middleware.push(analyticsMiddleware);
}
if (features.logging) {
middleware.push(loggingMiddleware);
}
if (features.performance) {
middleware.push(performanceMiddleware);
}
return middleware;
};
// Environment-based middleware
const environmentMiddleware = () => {
const middleware: Middleware[] = [];
if (process.env.NODE_ENV === 'development') {
middleware.push(
createImmutableStateInvariantMiddleware(),
createSerializableStateInvariantMiddleware()
);
}
if (process.env.ENABLE_REDUX_LOGGER) {
middleware.push(logger);
}
return middleware;
};Install with Tessl CLI
npx tessl i tessl/npm-reduxjs--toolkit