The official, opinionated, batteries-included toolset for efficient Redux development
—
Redux Toolkit provides utilities for creating type-safe action creators and reducers with automatic action type generation and Immer-powered immutable updates.
Creates a type-safe action creator function with optional payload preparation.
/**
* Creates a type-safe action creator with optional payload preparation
* @param type - The action type string
* @param prepareAction - Optional function to prepare the payload
* @returns Action creator function
*/
function createAction<P = void, T extends string = string>(
type: T
): PayloadActionCreator<P, T>;
function createAction<PA extends PrepareAction<any>, T extends string = string>(
type: T,
prepareAction: PA
): PayloadActionCreator<ReturnType<PA>['payload'], T, PA>;
interface PayloadActionCreator<P, T extends string, PA extends PrepareAction<any> = PrepareAction<any>> {
(payload: P): PayloadAction<P, T>;
type: T;
toString(): T;
match(action: AnyAction): action is PayloadAction<P, T>;
}
interface PayloadAction<P = void, T extends string = string, M = never, E = never> {
payload: P;
type: T;
meta?: M;
error?: E;
}
interface PrepareAction<P> {
(payload: P): { payload: P };
(payload: P, meta: any): { payload: P; meta: any };
(payload: P, meta: any, error: any): { payload: P; meta: any; error: any };
}Usage Examples:
import { createAction } from '@reduxjs/toolkit';
// Simple action with payload
const increment = createAction<number>('counter/increment');
console.log(increment(5));
// { type: 'counter/increment', payload: 5 }
// Action without payload
const reset = createAction('counter/reset');
console.log(reset());
// { type: 'counter/reset' }
// Action with prepare function
const addTodo = createAction('todos/add', (text: string) => ({
payload: {
text,
id: nanoid(),
createdAt: Date.now()
}
}));
console.log(addTodo('Learn Redux Toolkit'));
// {
// type: 'todos/add',
// payload: {
// text: 'Learn Redux Toolkit',
// id: 'generated-id',
// createdAt: 1640995200000
// }
// }
// Action with meta and error handling
const fetchUser = createAction('user/fetch', (id: string) => ({
payload: id,
meta: { requestId: nanoid() }
}));Redux Toolkit provides various action creator types for different use cases.
/**
* Action creator that requires a payload
*/
interface ActionCreatorWithPayload<P, T extends string = string> {
(payload: P): PayloadAction<P, T>;
type: T;
match(action: AnyAction): action is PayloadAction<P, T>;
}
/**
* Action creator with optional payload
*/
interface ActionCreatorWithOptionalPayload<P, T extends string = string> {
(payload?: P): PayloadAction<P | undefined, T>;
type: T;
match(action: AnyAction): action is PayloadAction<P | undefined, T>;
}
/**
* Action creator without payload
*/
interface ActionCreatorWithoutPayload<T extends string = string> {
(): PayloadAction<undefined, T>;
type: T;
match(action: AnyAction): action is PayloadAction<undefined, T>;
}
/**
* Action creator with prepared payload
*/
interface ActionCreatorWithPreparedPayload<Args extends unknown[], P, T extends string = string, E = never, M = never> {
(...args: Args): PayloadAction<P, T, M, E>;
type: T;
match(action: AnyAction): action is PayloadAction<P, T, M, E>;
}Creates a reducer function using Immer for immutable updates with a builder callback pattern.
/**
* Creates a reducer using Immer for immutable updates
* @param initialState - Initial state value or factory function
* @param builderCallback - Function that receives builder for adding cases
* @param actionMatchers - Additional action matchers (deprecated, use builder)
* @param defaultCaseReducer - Default case reducer (deprecated, use builder)
* @returns Reducer with getInitialState method
*/
function createReducer<S>(
initialState: S | (() => S),
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
): ReducerWithInitialState<S>;
interface ReducerWithInitialState<S> extends Reducer<S> {
getInitialState(): S;
}
interface ActionReducerMapBuilder<State> {
/** Add a case reducer for a specific action type */
addCase<ActionCreator extends TypedActionCreator<string>>(
actionCreator: ActionCreator,
reducer: CaseReducer<State, ReturnType<ActionCreator>>
): ActionReducerMapBuilder<State>;
/** Add a case reducer for a specific action type string */
addCase<Type extends string, A extends Action<Type>>(
type: Type,
reducer: CaseReducer<State, A>
): ActionReducerMapBuilder<State>;
/** Add a matcher for actions */
addMatcher<A extends AnyAction>(
matcher: ActionMatcher<A>,
reducer: CaseReducer<State, A>
): ActionReducerMapBuilder<State>;
/** Add a default case reducer */
addDefaultCase(reducer: CaseReducer<State, AnyAction>): ActionReducerMapBuilder<State>;
}
type CaseReducer<S = any, A extends Action = AnyAction> = (state: Draft<S>, action: A) => S | void | Draft<S>;Usage Examples:
import { createReducer, createAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const increment = createAction<number>('increment');
const decrement = createAction<number>('decrement');
const reset = createAction('reset');
const counterReducer = createReducer(
{ value: 0 } as CounterState,
(builder) => {
builder
.addCase(increment, (state, action) => {
// Immer allows "mutating" the draft state
state.value += action.payload;
})
.addCase(decrement, (state, action) => {
state.value -= action.payload;
})
.addCase(reset, (state) => {
state.value = 0;
})
.addMatcher(
(action): action is PayloadAction<number> =>
typeof action.payload === 'number',
(state, action) => {
// Handle all actions with number payloads
}
)
.addDefaultCase((state, action) => {
// Handle any other actions
console.log('Unhandled action:', action.type);
});
}
);
// Alternative: return new state instead of mutating
const todoReducer = createReducer([], (builder) => {
builder.addCase(addTodo, (state, action) => {
// Can return new state instead of mutating
return [...state, action.payload];
});
});Automatically generates action creators and a reducer from a single configuration object.
/**
* Automatically generates action creators and reducer from configuration
* @param options - Slice configuration options
* @returns Slice object with actions, reducer, and utilities
*/
function createSlice<
State,
CaseReducers extends SliceCaseReducers<State>,
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = {}
>(options: CreateSliceOptions<State, CaseReducers, Name, ReducerPath, Selectors>): Slice<State, CaseReducers, Name, ReducerPath, Selectors>;
interface CreateSliceOptions<State, CR extends SliceCaseReducers<State>, Name extends string, ReducerPath extends string, Selectors extends SliceSelectors<State>> {
/** Slice name used to generate action types */
name: Name;
/** Path in the store where this slice's state will be located */
reducerPath?: ReducerPath;
/** Initial state value or factory function */
initialState: State | (() => State);
/** Case reducer functions or creator functions */
reducers: ValidateSliceCaseReducers<State, CR>;
/** Builder callback for additional action types */
extraReducers?: (builder: ActionReducerMapBuilder<NoInfer<State>>) => void;
/** Selector functions */
selectors?: Selectors;
}
interface Slice<State = any, CaseReducers = {}, Name extends string = string, ReducerPath extends string = Name, Selectors = {}> {
/** The slice name */
name: Name;
/** The reducer path in the store */
reducerPath: ReducerPath;
/** The combined reducer function */
reducer: Reducer<State>;
/** Generated action creators */
actions: CaseReducerActions<CaseReducers, Name>;
/** Individual case reducer functions */
caseReducers: CaseReducers;
/** Returns the initial state */
getInitialState(): State;
/** Creates selectors bound to the slice state */
getSelectors(): Selectors & SliceSelectors<State>;
/** Creates selectors with custom state selector */
getSelectors<RootState>(selectState: (state: RootState) => State): Selectors & SliceSelectors<State>;
/** Selectors with default slice state selector */
selectors: Selectors & SliceSelectors<State>;
/** Selector to get the slice state */
selectSlice(state: { [K in ReducerPath]: State }): State;
/** Inject into a combined reducer */
injectInto(injectable: WithSlice<Slice<State, CaseReducers, Name, ReducerPath, Selectors>>, config?: { reducerPath?: any }): any;
}Usage Examples:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
status: 'idle' | 'loading' | 'succeeded' | 'failed';
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0, status: 'idle' } as CounterState,
reducers: {
// Standard reducer with payload
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
// Reducer without payload
increment: (state) => {
state.value += 1;
},
// Reducer with prepare callback
incrementByAmountWithId: {
reducer: (state, action: PayloadAction<{ amount: number; id: string }>) => {
state.value += action.payload.amount;
},
prepare: (amount: number) => ({
payload: { amount, id: nanoid() }
})
},
// Reset to initial state
reset: () => ({ value: 0, status: 'idle' } as CounterState)
},
// Handle external actions
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUserById.fulfilled, (state) => {
state.status = 'succeeded';
})
.addCase(fetchUserById.rejected, (state) => {
state.status = 'failed';
});
},
// Selectors
selectors: {
selectValue: (state) => state.value,
selectStatus: (state) => state.status,
selectIsLoading: (state) => state.status === 'loading'
}
});
// Export actions (automatically generated)
export const { increment, incrementByAmount, incrementByAmountWithId, reset } = counterSlice.actions;
// Export selectors
export const { selectValue, selectStatus, selectIsLoading } = counterSlice.selectors;
// Export reducer
export default counterSlice.reducer;
// Usage with typed selectors
const selectCounterValue = counterSlice.selectors.selectValue;
const value = selectCounterValue(rootState); // Automatically typed
// Create selectors with custom state selector
const counterSelectors = counterSlice.getSelectors((state: RootState) => state.counter);
const currentValue = counterSelectors.selectValue(rootState);/**
* Map of case reducer functions for a slice
*/
type SliceCaseReducers<State> = {
[K: string]:
| CaseReducer<State, PayloadAction<any>>
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>;
};
/**
* Case reducer with prepare method
*/
interface CaseReducerWithPrepare<State, Action extends PayloadAction> {
reducer: CaseReducer<State, Action>;
prepare: PrepareAction<Action['payload']>;
}
/**
* Generated action creators from slice case reducers
*/
type CaseReducerActions<CaseReducers, SliceName extends string> = {
[Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any }
? ActionCreatorForCaseReducerWithPrepare<CaseReducers[Type], SliceName>
: ActionCreatorForCaseReducer<CaseReducers[Type], SliceName>;
};
/**
* Validation helper for slice case reducers
*/
type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
[T in keyof ACR]: ACR[T] extends {
reducer(s: S, action?: infer A): any;
}
? {
prepare(...a: never[]): Omit<A, 'type'>;
}
: {};
};Redux Toolkit provides utilities for working with actions and action creators.
/**
* Type guard to check if a value is an RTK action creator
* @param action - Value to check
* @returns True if the value is an action creator
*/
function isActionCreator(action: any): action is (...args: any[]) => AnyAction;
/**
* Checks if an action follows the Flux Standard Action format
* @param action - Action to check
* @returns True if action is FSA compliant
*/
function isFSA(action: unknown): action is {
type: string;
payload?: any;
error?: boolean;
meta?: any;
};
/** Alias for isFSA */
function isFluxStandardAction(action: unknown): action is {
type: string;
payload?: any;
error?: boolean;
meta?: any;
};Usage Examples:
import { isActionCreator, isFSA, createAction } from '@reduxjs/toolkit';
const increment = createAction('increment');
const plainAction = { type: 'PLAIN_ACTION' };
console.log(isActionCreator(increment)); // true
console.log(isActionCreator(plainAction)); // false
console.log(isFSA(increment(5))); // true
console.log(isFSA({ type: 'TEST', invalid: true })); // false// Using builder callback for complex reducer logic
const complexSlice = createSlice({
name: 'complex',
initialState: { items: [], status: 'idle' },
reducers: {
itemAdded: (state, action) => {
state.items.push(action.payload);
}
},
extraReducers: (builder) => {
// Handle async thunk actions
builder
.addCase(fetchItems.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchItems.fulfilled, (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
})
// Handle multiple action types with matcher
.addMatcher(
(action) => action.type.endsWith('/pending'),
(state) => {
state.status = 'loading';
}
)
// Default handler
.addDefaultCase((state, action) => {
console.log('Unhandled action:', action);
});
}
});Advanced slice creation with custom reducer creators like async thunks.
/**
* Creates a custom createSlice function with additional reducer creators
* @param config - Configuration with custom creators
* @returns Custom createSlice function
*/
function buildCreateSlice(config?: {
creators?: {
asyncThunk?: typeof asyncThunkCreator;
};
}): typeof createSlice;
/**
* Async thunk creator symbol for buildCreateSlice
* Used to define async reducers directly in slice definitions
*/
const asyncThunkCreator: {
[Symbol.for('rtk-slice-createasyncthunk')]: typeof createAsyncThunk;
};
/**
* Enum defining different types of reducers
*/
enum ReducerType {
reducer = 'reducer',
reducerWithPrepare = 'reducerWithPrepare',
asyncThunk = 'asyncThunk',
}Usage Examples:
import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit';
// Create custom slice builder with async thunk support
const createSliceWithAsyncThunks = buildCreateSlice({
creators: { asyncThunk: asyncThunkCreator }
});
// Use it to create slices with async reducers
const userSlice = createSliceWithAsyncThunks({
name: 'user',
initialState: { data: null, loading: false },
reducers: (create) => ({
// Regular reducer
clearUser: create.reducer((state) => {
state.data = null;
}),
// Async thunk reducer defined inline
fetchUser: create.asyncThunk(
async (userId: string) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
{
pending: (state) => {
state.loading = true;
},
fulfilled: (state, action) => {
state.loading = false;
state.data = action.payload;
},
rejected: (state) => {
state.loading = false;
}
}
)
})
});// Dynamically inject slices into combined reducers
import { combineSlices } from '@reduxjs/toolkit';
const rootReducer = combineSlices(counterSlice, todosSlice);
// Later, inject a new slice
const withAuthSlice = authSlice.injectInto(rootReducer);const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded: {
reducer: (state, action) => {
state.push(action.payload);
},
prepare: (text: string) => {
return {
payload: {
id: nanoid(),
text,
completed: false,
createdAt: Date.now()
},
meta: {
timestamp: Date.now()
}
};
}
}
}
});Install with Tessl CLI
npx tessl i tessl/npm-reduxjs--toolkit