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

utilities.mddocs/

Utilities

Redux Toolkit provides a comprehensive set of utility functions for selectors, action matching, state management, and development tools.

Capabilities

Selector Utilities

Redux Toolkit re-exports and enhances Reselect for creating memoized selectors with additional draft-safe functionality.

/**
 * Create a memoized selector from input selectors and result function
 * Re-exported from Reselect with Redux Toolkit enhancements
 */
function createSelector<InputSelectors extends readonly unknown[], Result>(
  ...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
): Selector<GetStateFromSelectors<InputSelectors>, Result>;

/**
 * Create custom selector creator with specific memoization function
 */
function createSelectorCreator<MemoizeFunction extends Function>(
  memoizeFunc: MemoizeFunction,
  ...memoizeOptions: any[]
): <InputSelectors extends readonly unknown[], Result>(
  ...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
) => Selector<GetStateFromSelectors<InputSelectors>, Result>;

/**
 * LRU (Least Recently Used) memoization function
 * Re-exported from Reselect
 */
function lruMemoize<Func extends Function>(func: Func, equalityCheckOrOptions?: any): Func;

/**
 * WeakMap-based memoization for object references
 * Re-exported from Reselect  
 */
function weakMapMemoize<Func extends Function>(func: Func): Func;

/**
 * Create draft-safe selector that works with Immer draft objects
 * RTK-specific enhancement
 */
function createDraftSafeSelector<InputSelectors extends readonly unknown[], Result>(
  ...args: [...inputSelectors: InputSelectors, resultFunc: (...args: SelectorResultArray<InputSelectors>) => Result]
): Selector<GetStateFromSelectors<InputSelectors>, Result>;

/**
 * Create custom draft-safe selector creator
 */
function createDraftSafeSelectorCreator<MemoizeFunction extends Function>(
  memoizeFunc: MemoizeFunction,
  ...memoizeOptions: any[]
): typeof createDraftSafeSelector;

Usage Examples:

import { 
  createSelector, 
  createDraftSafeSelector,
  lruMemoize,
  weakMapMemoize 
} from '@reduxjs/toolkit';

interface RootState {
  todos: { id: string; text: string; completed: boolean }[];
  filter: 'all' | 'active' | 'completed';
}

// Basic selector
const selectTodos = (state: RootState) => state.todos;
const selectFilter = (state: RootState) => state.filter;

// Memoized selector
const selectFilteredTodos = createSelector(
  [selectTodos, selectFilter],
  (todos, filter) => {
    switch (filter) {
      case 'active':
        return todos.filter(todo => !todo.completed);
      case 'completed':
        return todos.filter(todo => todo.completed);
      default:
        return todos;
    }
  }
);

// Selector with arguments
const selectTodoById = createSelector(
  [selectTodos, (state: RootState, id: string) => id],
  (todos, id) => todos.find(todo => todo.id === id)
);

// Draft-safe selector for use with Immer drafts
const selectTodoStats = createDraftSafeSelector(
  [selectTodos],
  (todos) => ({
    total: todos.length,
    completed: todos.filter(todo => todo.completed).length,
    active: todos.filter(todo => !todo.completed).length
  })
);

// Custom memoization
const selectExpensiveComputation = createSelector(
  [selectTodos],
  (todos) => {
    // Expensive computation
    return todos.map(todo => ({
      ...todo,
      hash: computeExpensiveHash(todo)
    }));
  },
  {
    // Use LRU memoization with cache size 10
    memoize: lruMemoize,
    memoizeOptions: { maxSize: 10 }
  }
);

// WeakMap memoization for object keys
const createObjectKeySelector = createSelectorCreator(weakMapMemoize);

const selectTodosByCategory = createObjectKeySelector(
  [selectTodos, (state: RootState, category: object) => category],
  (todos, category) => todos.filter(todo => todo.category === category)
);

// Usage in components
const TodoList = () => {
  const filteredTodos = useAppSelector(selectFilteredTodos);
  const stats = useAppSelector(selectTodoStats);
  
  return (
    <div>
      <div>Total: {stats.total}, Active: {stats.active}, Completed: {stats.completed}</div>
      {filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
    </div>
  );
};

const TodoDetail = ({ todoId }: { todoId: string }) => {
  const todo = useAppSelector(state => selectTodoById(state, todoId));
  
  return todo ? <div>{todo.text}</div> : <div>Todo not found</div>;
};

Action Matching Utilities

Powerful utilities for matching and combining action matchers in reducers and middleware.

/**
 * Creates matcher that requires all conditions to be true
 * @param matchers - Array of action matchers or action creators
 * @returns Combined action matcher
 */
function isAllOf<A extends AnyAction>(
  ...matchers: readonly ActionMatcherOrType<A>[]
): ActionMatcher<A>;

/**
 * Creates matcher that requires any condition to be true
 * @param matchers - Array of action matchers or action creators  
 * @returns Combined action matcher
 */
function isAnyOf<A extends AnyAction>(
  ...matchers: readonly ActionMatcherOrType<A>[]
): ActionMatcher<A>;

/**
 * Matches pending actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher for pending states
 */
function isPending(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<PendingAction<any>>;

/**
 * Matches fulfilled actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher for fulfilled states
 */
function isFulfilled(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<FulfilledAction<any, any>>;

/**
 * Matches rejected actions from async thunks
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher for rejected states
 */
function isRejected(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedAction<any, any>>;

/**
 * Matches any action from async thunks (pending, fulfilled, or rejected)
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher for any async thunk action
 */
function isAsyncThunkAction(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<AnyAsyncThunkAction>;

/**
 * Matches rejected actions that used rejectWithValue
 * @param asyncThunks - Async thunk action creators to match
 * @returns Action matcher for rejected with value actions
 */
function isRejectedWithValue(...asyncThunks: AsyncThunk<any, any, any>[]): ActionMatcher<RejectedWithValueAction<any, any>>;

type ActionMatcher<A extends AnyAction> = (action: AnyAction) => action is A;
type ActionMatcherOrType<A extends AnyAction> = ActionMatcher<A> | string | ActionCreator<A>;

Usage Examples:

import { 
  isAllOf, 
  isAnyOf, 
  isPending, 
  isFulfilled, 
  isRejected,
  isRejectedWithValue 
} from '@reduxjs/toolkit';

// Action creators and async thunks
const increment = createAction('counter/increment');
const decrement = createAction('counter/decrement');
const reset = createAction('counter/reset');

const fetchUser = createAsyncThunk('user/fetch', async (id: string) => {
  const response = await api.getUser(id);
  return response.data;
});

const updateUser = createAsyncThunk('user/update', async (data: UserData) => {
  const response = await api.updateUser(data);
  return response.data;
});

// Complex matchers in reducers
const appSlice = createSlice({
  name: 'app',
  initialState: {
    counter: 0,
    loading: false,
    error: null as string | null,
    lastAction: null as string | null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      // Match any counter action
      .addMatcher(
        isAnyOf(increment, decrement, reset),
        (state, action) => {
          state.lastAction = action.type;
          state.error = null;
        }
      )
      // Match all counter increment actions (example with multiple conditions)
      .addMatcher(
        isAllOf(
          (action): action is PayloadAction<number> => 
            typeof action.payload === 'number',
          isAnyOf(increment)
        ),
        (state, action) => {
          state.counter += action.payload;
        }
      )
      // Match any pending async action
      .addMatcher(
        isPending(fetchUser, updateUser),
        (state) => {
          state.loading = true;
          state.error = null;
        }
      )
      // Match any fulfilled async action
      .addMatcher(
        isFulfilled(fetchUser, updateUser),
        (state) => {
          state.loading = false;
        }
      )
      // Match rejected actions with custom error values
      .addMatcher(
        isRejectedWithValue(fetchUser, updateUser),
        (state, action) => {
          state.loading = false;
          state.error = action.payload as string;
        }
      )
      // Match other rejected actions
      .addMatcher(
        isRejected(fetchUser, updateUser),
        (state, action) => {
          state.loading = false;
          state.error = action.error.message || 'Unknown error';
        }
      );
  }
});

// Custom matchers
const isUserAction = (action: AnyAction): action is AnyAction => {
  return action.type.startsWith('user/');
};

const isHighPriorityAction = (action: AnyAction): action is AnyAction => {
  return action.meta?.priority === 'high';
};

// Combine custom matchers
const isHighPriorityUserAction = isAllOf(isUserAction, isHighPriorityAction);

const prioritySlice = createSlice({
  name: 'priority',
  initialState: { highPriorityUserActions: 0 },
  reducers: {},
  extraReducers: (builder) => {
    builder.addMatcher(
      isHighPriorityUserAction,
      (state) => {
        state.highPriorityUserActions += 1;
      }
    );
  }
});

// Middleware usage
const actionMatchingMiddleware: Middleware = (store) => (next) => (action) => {
  if (isAnyOf(increment, decrement)(action)) {
    console.log('Counter action dispatched:', action);
  }
  
  if (isPending(fetchUser, updateUser)(action)) {
    console.log('Async operation started:', action);
  }
  
  return next(action);
};

State Utilities

Utilities for advanced state management patterns and slice composition.

/**
 * Combines multiple slices into a single reducer with injection support
 * @param slices - Slice objects to combine
 * @returns Combined reducer with slice injection capabilities
 */
function combineSlices(...slices: Slice[]): CombinedSliceReducer;

interface CombinedSliceReducer extends Reducer {
  /** Inject additional slices at runtime */
  inject<S extends Slice>(slice: S, config?: { reducerPath?: string }): CombinedSliceReducer;
  /** Get the current slice configuration */
  selector: (state: any) => any;
}

/**
 * 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 metadata
 */
function prepareAutoBatched<T>(payload: T): {
  payload: T;
  meta: { [SHOULD_AUTOBATCH]: true };
};

/**
 * Store enhancer for automatic action batching
 * @param options - Batching configuration
 * @returns Store enhancer function
 */
function autoBatchEnhancer(options?: {
  type?: 'tick' | 'timer' | 'callback' | ((action: Action) => boolean);
}): StoreEnhancer;

Usage Examples:

import { combineSlices, prepareAutoBatched, autoBatchEnhancer } from '@reduxjs/toolkit';

// Individual slices
const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value += 1; },
    decrement: (state) => { state.value -= 1; }
  }
});

const todosSlice = createSlice({
  name: 'todos',
  initialState: [] as Todo[],
  reducers: {
    addTodo: (state, action) => {
      state.push(action.payload);
    }
  }
});

// Combine slices
const rootReducer = combineSlices(counterSlice, todosSlice);

// Add slice at runtime
const userSlice = createSlice({
  name: 'user',
  initialState: { profile: null },
  reducers: {
    setProfile: (state, action) => {
      state.profile = action.payload;
    }
  }
});

// Inject new slice
const enhancedReducer = rootReducer.inject(userSlice);

// Store with combined slices
const store = configureStore({
  reducer: enhancedReducer,
  enhancers: (getDefaultEnhancers) =>
    getDefaultEnhancers().concat(
      autoBatchEnhancer({ type: 'tick' })
    )
});

// Auto-batched actions
const batchedSlice = createSlice({
  name: 'batched',
  initialState: { items: [], count: 0 },
  reducers: {
    bulkAddItems: {
      reducer: (state, action) => {
        state.items.push(...action.payload);
        state.count = state.items.length;
      },
      prepare: prepareAutoBatched
    },
    batchedUpdate: {
      reducer: (state, action) => {
        Object.assign(state, action.payload);
      },
      prepare: (updates: any) => ({
        payload: updates,
        meta: { [SHOULD_AUTOBATCH]: true }
      })
    }
  }
});

// Dynamic slice injection
const createDynamicStore = () => {
  let currentReducer = combineSlices();
  
  const store = configureStore({
    reducer: currentReducer
  });
  
  return {
    ...store,
    injectSlice: (slice: Slice) => {
      currentReducer = currentReducer.inject(slice);
      store.replaceReducer(currentReducer);
    }
  };
};

// Usage
const dynamicStore = createDynamicStore();

// Add slices dynamically
dynamicStore.injectSlice(counterSlice);
dynamicStore.injectSlice(todosSlice);

Development Utilities

Utilities for development-time debugging and state inspection.

/**
 * Checks if value is serializable for Redux state
 * @param value - Value to check
 * @param path - Current path in object tree
 * @param isSerializable - Custom serializability checker
 * @returns First non-serializable value found or false if all serializable
 */
function findNonSerializableValue(
  value: any,
  path?: string,
  isSerializable?: (value: any) => boolean
): false | { keyPath: string; value: any };

/**
 * Checks if value is a plain object
 * @param value - Value to check
 * @returns True if value is plain object
 */
function isPlain(value: any): boolean;

/**
 * Default immutability check function
 * @param value - Value to check for immutability
 * @returns True if value is considered immutable
 */
function isImmutableDefault(value: any): boolean;

Usage Examples:

import { 
  findNonSerializableValue, 
  isPlain, 
  isImmutableDefault 
} from '@reduxjs/toolkit';

// Debug non-serializable values in state
const debugState = (state: any) => {
  const nonSerializable = findNonSerializableValue(state);
  
  if (nonSerializable) {
    console.warn(
      'Non-serializable value found at path:',
      nonSerializable.keyPath,
      'Value:',
      nonSerializable.value
    );
  }
};

// Custom serializability checker
const customSerializableCheck = (value: any): boolean => {
  // Allow Date objects
  if (value instanceof Date) return true;
  
  // Allow specific function types
  if (typeof value === 'function' && value.name === 'allowedFunction') {
    return true;
  }
  
  // Default check for other types
  return isPlain(value) || 
         typeof value === 'string' || 
         typeof value === 'number' || 
         typeof value === 'boolean' || 
         value === null;
};

// Debug middleware
const debugMiddleware: Middleware = (store) => (next) => (action) => {
  const prevState = store.getState();
  const result = next(action);
  const nextState = store.getState();
  
  // Check for non-serializable values
  const actionNonSerializable = findNonSerializableValue(action, 'action', customSerializableCheck);
  const stateNonSerializable = findNonSerializableValue(nextState, 'state', customSerializableCheck);
  
  if (actionNonSerializable) {
    console.warn('Non-serializable action:', actionNonSerializable);
  }
  
  if (stateNonSerializable) {
    console.warn('Non-serializable state:', stateNonSerializable);
  }
  
  return result;
};

// State validation utility
const validateState = (state: any, path = 'state'): string[] => {
  const errors: string[] = [];
  
  const checkValue = (value: any, currentPath: string) => {
    if (!isImmutableDefault(value) && typeof value === 'object' && value !== null) {
      errors.push(`Mutable object at ${currentPath}`);
    }
    
    if (!isPlain(value) && typeof value === 'object' && value !== null) {
      errors.push(`Non-plain object at ${currentPath}`);
    }
    
    if (typeof value === 'object' && value !== null) {
      Object.keys(value).forEach(key => {
        checkValue(value[key], `${currentPath}.${key}`);
      });
    }
  };
  
  checkValue(state, path);
  return errors;
};

// Usage in development
if (process.env.NODE_ENV === 'development') {
  // Validate store state after each action
  store.subscribe(() => {
    const state = store.getState();
    const errors = validateState(state);
    
    if (errors.length > 0) {
      console.group('State validation errors:');
      errors.forEach(error => console.warn(error));
      console.groupEnd();
    }
  });
}

Utility Functions

Additional utility functions for common Redux patterns.

/**
 * Generate unique ID string
 * @param size - Length of generated ID (default: 21)
 * @returns Random URL-safe string
 */
function nanoid(size?: number): string;

/**
 * Tuple helper for maintaining array types in TypeScript
 * @param items - Array items to maintain as tuple type
 * @returns Tuple with preserved types
 */
function Tuple<T extends readonly unknown[]>(...items: T): T;

/**
 * Current value of Immer draft
 * Re-exported from Immer
 */
function current<T>(draft: T): T;

/**
 * Original value before Immer draft modifications  
 * Re-exported from Immer
 */
function original<T>(draft: T): T | undefined;

/**
 * Create next immutable state (alias for Immer's produce)
 * Re-exported from Immer
 */
function createNextState<Base>(base: Base, recipe: (draft: Draft<Base>) => void): Base;

/**
 * Freeze object to prevent mutations
 * Re-exported from Immer
 */
function freeze<T>(obj: T): T;

/**
 * Check if value is Immer draft
 * Re-exported from Immer
 */
function isDraft(value: any): boolean;

/**
 * Format production error message with error code
 * Used internally for minified error messages
 * @param code - Error code number
 * @returns Formatted error message with link to documentation
 */
function formatProdErrorMessage(code: number): string;

Usage Examples:

import { 
  nanoid, 
  Tuple, 
  current, 
  original, 
  createNextState,
  freeze,
  isDraft,
  formatProdErrorMessage 
} from '@reduxjs/toolkit';

// Generate unique IDs
const createTodo = (text: string) => ({
  id: nanoid(), // Generates unique ID
  text,
  completed: false,
  createdAt: Date.now()
});

// Maintain tuple types
const actionTypes = Tuple('INCREMENT', 'DECREMENT', 'RESET');
type ActionType = typeof actionTypes[number]; // 'INCREMENT' | 'DECREMENT' | 'RESET'

// Immer utilities in reducers
const complexReducer = createReducer(initialState, (builder) => {
  builder.addCase(complexUpdate, (state, action) => {
    // Log current draft state
    console.log('Current state:', current(state));
    
    // Log original state before modifications
    console.log('Original state:', original(state));
    
    // Check if working with draft
    if (isDraft(state)) {
      console.log('Working with Immer draft');
    }
    
    // Make complex modifications
    state.items.forEach(item => {
      if (item.needsUpdate) {
        item.lastUpdated = Date.now();
      }
    });
  });
});

// Create immutable state manually
const updateStateManually = (currentState: State, updates: Partial<State>) => {
  return createNextState(currentState, (draft) => {
    Object.assign(draft, updates);
  });
};

// Freeze objects for immutability
const createImmutableConfig = (config: Config) => {
  return freeze({
    ...config,
    metadata: freeze(config.metadata)
  });
};

// Custom ID generator
const createCustomId = () => {
  const timestamp = Date.now().toString(36);
  const randomPart = nanoid(8);
  return `${timestamp}-${randomPart}`;
};

// Format production errors (typically used internally)
console.log(formatProdErrorMessage(1));
// Output: "Minified Redux Toolkit error #1; visit https://redux-toolkit.js.org/Errors?code=1 for the full message or use the non-minified dev environment for full errors."

// Type-safe tuple operations
const middleware = Tuple(
  thunkMiddleware,
  loggerMiddleware,
  crashReportingMiddleware
);

// Each middleware is properly typed
type MiddlewareArray = typeof middleware; // [ThunkMiddleware, LoggerMiddleware, CrashMiddleware]

// Utility for debugging Immer operations
const debugImmerReducer = <S>(
  initialState: S,
  name: string
) => createReducer(initialState, (builder) => {
  builder.addDefaultCase((state, action) => {
    if (isDraft(state)) {
      console.log(`${name} - Draft state:`, current(state));
      console.log(`${name} - Original state:`, original(state));
      console.log(`${name} - Action:`, action);
    }
  });
});

Advanced Utility Patterns

Selector Composition

// Compose selectors for complex state selection
const createEntitySelectors = <T>(selectEntities: (state: any) => Record<string, T>) => ({
  selectById: (id: string) => createSelector(
    [selectEntities],
    (entities) => entities[id]
  ),
  
  selectByIds: (ids: string[]) => createSelector(
    [selectEntities],
    (entities) => ids.map(id => entities[id]).filter(Boolean)
  ),
  
  selectAll: createSelector(
    [selectEntities],
    (entities) => Object.values(entities)
  ),
  
  selectCount: createSelector(
    [selectEntities],
    (entities) => Object.keys(entities).length
  )
});

// Usage
const userSelectors = createEntitySelectors((state: RootState) => state.users.entities);
const postSelectors = createEntitySelectors((state: RootState) => state.posts.entities);

Action Matcher Patterns

// Create reusable matcher patterns
const createAsyncMatchers = <T extends AsyncThunk<any, any, any>[]>(...thunks: T) => ({
  pending: isPending(...thunks),
  fulfilled: isFulfilled(...thunks),
  rejected: isRejected(...thunks),
  rejectedWithValue: isRejectedWithValue(...thunks),
  settled: isAnyOf(isFulfilled(...thunks), isRejected(...thunks)),
  any: isAsyncThunkAction(...thunks)
});

// Usage
const userMatchers = createAsyncMatchers(fetchUser, updateUser, deleteUser);
const dataMatchers = createAsyncMatchers(fetchPosts, fetchComments);

// Apply patterns in reducers
builder
  .addMatcher(userMatchers.pending, handleUserPending)
  .addMatcher(userMatchers.fulfilled, handleUserFulfilled)
  .addMatcher(userMatchers.rejected, handleUserRejected);

Development Tool Integration

// Enhanced development utilities
const createDevUtils = (store: EnhancedStore) => ({
  logState: () => console.log('Current state:', store.getState()),
  
  validateState: () => {
    const state = store.getState();
    const errors = validateState(state);
    if (errors.length > 0) {
      console.warn('State validation errors:', errors);
    }
    return errors.length === 0;
  },
  
  inspectAction: (action: AnyAction) => {
    const nonSerializable = findNonSerializableValue(action);
    return {
      isSerializable: !nonSerializable,
      nonSerializablePath: nonSerializable?.keyPath,
      nonSerializableValue: nonSerializable?.value
    };
  },
  
  timeAction: async (actionCreator: () => AnyAction) => {
    const startTime = performance.now();
    const action = actionCreator();
    await store.dispatch(action);
    const endTime = performance.now();
    
    console.log(`Action ${action.type} took ${endTime - startTime}ms`);
    return endTime - startTime;
  }
});

// Usage in development
if (process.env.NODE_ENV === 'development') {
  (window as any).devUtils = createDevUtils(store);
}

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