The official, opinionated, batteries-included toolset for efficient Redux development
—
Redux Toolkit provides a comprehensive set of utility functions for selectors, action matching, state management, and development tools.
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>;
};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);
};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);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();
}
});
}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);
}
});
});// 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);// 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);// 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