Skip to main content

createSlice

A function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.

This API is the standard approach for writing Redux logic.

Internally, it uses createAction and createReducer, so you may also use Immer to write "mutating" immutable updates:

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
value: number
}

const initialState = { value: 0 } as CounterState

const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
},
},
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

Parameters

createSlice accepts a single configuration object parameter, with the following options:

function createSlice({
// A name, used in action types
name: string,
// The initial state for the reducer
initialState: any,
// An object of "case reducers". Key names will be used to generate actions.
reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
// A "builder callback" function used to add more reducers, or
// an additional object of "case reducers", where the keys should be other
// action types
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})

initialState

The initial state value for this slice of state.

This may also be a "lazy initializer" function, which should return an initial state value when called. This will be used whenever the reducer is called with undefined as its state value, and is primarily useful for cases like reading initial state from localStorage.

name

A string name for this slice of state. Generated action type constants will use this as a prefix.

reducers

An object containing Redux "case reducer" functions (functions intended to handle a specific action type, equivalent to a single case statement in a switch).

The keys in the object will be used to generate string action type constants, and these will show up in the Redux DevTools Extension when they are dispatched. Also, if any other part of the application happens to dispatch an action with the exact same type string, the corresponding reducer will be run. Therefore, you should give the functions descriptive names.

This object will be passed to createReducer, so the reducers may safely "mutate" the state they are given.

import { createSlice } from '@reduxjs/toolkit'

const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
})
// Will handle the action type `'counter/increment'`

Customizing Generated Action Creators

If you need to customize the creation of the payload value of an action creator by means of a prepare callback, the value of the appropriate field of the reducers argument object should be an object instead of a function. This object must contain two properties: reducer and prepare. The value of the reducer field should be the case reducer function while the value of the prepare field should be the prepare callback function:

import { createSlice, nanoid } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

interface Item {
id: string
text: string
}

const todosSlice = createSlice({
name: 'todos',
initialState: [] as Item[],
reducers: {
addTodo: {
reducer: (state, action: PayloadAction<Item>) => {
state.push(action.payload)
},
prepare: (text: string) => {
const id = nanoid()
return { payload: { id, text } }
},
},
},
})

extraReducers

Conceptually, each slice reducer "owns" its slice of state. There's also a natural correspondance between the update logic defined inside reducers, and the action types that are generated based on those.

However, there are many times that a Redux slice may also need to update its own state in response to action types that were defined elsewhere in the application (such as clearing many different kinds of data when a "user logged out" action is dispatched). This can include action types defined by another createSlice call, actions generated by a createAsyncThunk, RTK Query endpoint matchers, or any other action. In addition, one of the key concepts of Redux is that many slice reducers can independently respond to the same action type.

extraReducers allows createSlice to respond and update its own state in response to other action types besides the types it has generated.

As with the reducers field, each case reducer in extraReducers is wrapped in Immer and may use "mutating" syntax to safely update the state inside.

However, unlike the reducers field, each individual case reducer inside of extraReducers will not generate a new action type or action creator.

If two fields from reducers and extraReducers happen to end up with the same action type string, the function from reducers will be used to handle that action type.

The extraReducers "builder callback" notation

The recommended way of using extraReducers is to use a callback that receives a ActionReducerMapBuilder instance.

This builder notation is also the only way to add matcher reducers and default case reducers to your slice.

import { createAction, createSlice, Action, AnyAction } from '@reduxjs/toolkit'
const incrementBy = createAction<number>('incrementBy')
const decrement = createAction('decrement')

interface RejectedAction extends Action {
error: Error
}

function isRejectedAction(action: AnyAction): action is RejectedAction {
return action.type.endsWith('rejected')
}

createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(incrementBy, (state, action) => {
// action is inferred correctly here if using TS
})
// You can chain calls, or have separate `builder.addCase()` lines each time
.addCase(decrement, (state, action) => {})
// You can match a range of action types
.addMatcher(
isRejectedAction,
// `action` will be inferred as a RejectedAction due to isRejectedAction being defined as a type guard
(state, action) => {}
)
// and provide a default case if no other handlers matched
.addDefaultCase((state, action) => {})
},
})

We recommend using this API as it has better TypeScript support (and thus, IDE autocomplete even for JavaScript users), as it will correctly infer the action type in the reducer based on the provided action creator. It's particularly useful for working with actions produced by createAction and createAsyncThunk.

See the "Builder Callback Notation" section of the createReducer reference for details on how to use builder.addCase, builder.addMatcher, and builder.addDefault

The extraReducers "map object" notation

caution

The "map object" notation is deprecated and will be removed in RTK 2.0. Please migrate to the "builder callback" notation, which offers much better TypeScript support and more flexibility. (There is a "builder callback" codemod available to help with this migration.)

If you do not use the builder callback and are using TypeScript, you will need to use actionCreator.type or actionCreator.toString() to force the TS compiler to accept the computed property. Please see Usage With TypeScript for further details.

Like reducers, extraReducers can be an object containing Redux case reducer functions. However, the keys should be other Redux string action type constants, and createSlice will not auto-generate action types or action creators for reducers included in this parameter.

Action creators that were generated using createAction may be used directly as the keys here, using computed property syntax.

const incrementBy = createAction('incrementBy')

createSlice({
name: 'counter',
initialState: 0,
reducers: {},
extraReducers: {
[incrementBy]: (state, action) => {
return state + action.payload
},
'some/other/action': (state, action) => {},
},
})

Return Value

createSlice will return an object that looks like:

{
name : string,
reducer : ReducerFunction,
actions : Record<string, ActionCreator>,
caseReducers: Record<string, CaseReducer>.
getInitialState: () => State
}

Each function defined in the reducers argument will have a corresponding action creator generated using createAction and included in the result's actions field using the same function name.

The generated reducer function is suitable for passing to the Redux combineReducers function as a "slice reducer".

You may want to consider destructuring the action creators and exporting them individually, for ease of searching for references in a larger codebase.

The functions passed to the reducers parameter can be accessed through the caseReducers return field. This can be particularly useful for testing or direct access to reducers created inline.

Result's function getInitialState provides access to the initial state value given to the slice. If a lazy state initializer was provided, it will be called and a fresh value returned.

Note: the result object is conceptually similar to a "Redux duck" code structure. The actual code structure you use is up to you, but there are a couple caveats to keep in mind:

  • Actions are not exclusively limited to a single slice. Any part of the reducer logic can (and should!) respond to any dispatched action.
  • At the same time, circular references can cause import problems. If slices A and B are defined in separate files, and each file tries to import the other so it can listen to other actions, unexpected behavior may occur.

Examples

import { createSlice, createAction, configureStore } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { combineReducers } from 'redux'

const incrementBy = createAction<number>('incrementBy')
const decrementBy = createAction<number>('decrementBy')

const counter = createSlice({
name: 'counter',
initialState: 0 as number,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1,
multiply: {
reducer: (state, action: PayloadAction<number>) => state * action.payload,
prepare: (value?: number) => ({ payload: value || 2 }), // fallback if the payload is a falsy value
},
},
// "builder callback API", recommended for TypeScript users
extraReducers: (builder) => {
builder.addCase(incrementBy, (state, action) => {
return state + action.payload
})
builder.addCase(decrementBy, (state, action) => {
return state - action.payload
})
},
})

const user = createSlice({
name: 'user',
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
},
},
// "map object API"
extraReducers: {
// @ts-expect-error in TypeScript, this would need to be [counter.actions.increment.type]
[counter.actions.increment]: (
state,
action /* action will be inferred as "any", as the map notation does not contain type information */
) => {
state.age += 1
},
},
})

const store = configureStore({
reducer: {
counter: counter.reducer,
user: user.reducer,
},
})

store.dispatch(counter.actions.increment())
// -> { counter: 1, user: {name : '', age: 21} }
store.dispatch(counter.actions.increment())
// -> { counter: 2, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply(3))
// -> { counter: 6, user: {name: '', age: 22} }
store.dispatch(counter.actions.multiply())
// -> { counter: 12, user: {name: '', age: 22} }
console.log(`${counter.actions.decrement}`)
// -> "counter/decrement"
store.dispatch(user.actions.setUserName('eric'))
// -> { counter: 12, user: { name: 'eric', age: 22} }