CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-reduxjs--toolkit

The official, opinionated, batteries-included toolset for efficient Redux development

Pending
Overview
Eval results
Files

middleware.mddocs/

Middleware

Redux Toolkit provides advanced middleware for side effects, dynamic middleware injection, and development-time invariants.

Capabilities

Listener Middleware

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)
});

Dynamic 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
    });
  }
});

React Dynamic Middleware

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
};

Development 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
      )
});

Serialization Utilities

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)

Auto-batching

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');
        }
      })
    )
});

Advanced Patterns

Listener Middleware Workflows

// 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));
    }
  }
});

Middleware Composition

// 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)
});

Conditional Middleware Loading

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

docs

actions-reducers.md

async-thunks.md

core-store.md

entity-adapters.md

index.md

middleware.md

react-integration.md

rtk-query-react.md

rtk-query.md

utilities.md

tile.json