What is a slice? #
In redux, a slice is a "slice" of your redux state object.
1store.getState();
2/*
3{
4 token: '', // this is a slice
5 users: {}, // this is a slice
6 todos: {}, // this is a slice
7}
8*/
Actions and reducers per slice #
Splitting up reducer logic is an import concept in redux where we compose multiple reducers into one big reducer using combineReducers. For every slice, there is a single corresponding reducer. When building store data inside redux, it is very common to build a set of actions and a reducer that responds to those actions.
What is createSlice
? #
createSlice
is a higher-order function that accepts the slice name (e.g.
token
, user
, todos
), a set of reducers, and returns a single reducer along
with the action creators for that reducer. The goal of createSlice
is to
reduce the boilerplate required to add data to redux the canonical way.
The createSlice we know of today
from redux-toolkit
was inspired by
autodux.
I also helped build the original implementation in redux-toolkit
and have been using it for every redux project since. It is a powerful helper
function that has gained a ton of popularity in the redux community.
However, it is common for engineers learning redux for the first time to be
completely overwhelmed by the terms and phrases used by the redux community.
This is exacerbated by the fact that every reducer is now wrapped by
createSlice
.
In this post, I want to demystify createSlice
by building our own stripped
down version of it for new engineers to use as a reference guide when learning
redux.
BYO createSlice
#
In order to build our own createSlice
we need to build a couple of other
helper functions first.
Note: all of these implementations are simplified versions of the official ones
to be used as a learning guide. If you dig into the redux-toolkit
source code,
you'll see that most of the code are typings and embellishments on top of the
code written in this article.
For our example usage we will be recreating redux's example todo list
1type ToDo {
2 id: string;
3 text: string;
4 completed: boolean;
5}
BYO createAction
#
createAction
is a simple helper function that accepts a string and returns an
action creator.
1function createAction<P = any>(type: string) {
2 const actionCreator = (payload?: P) => {
3 return {
4 type,
5 payload,
6 };
7 };
8
9 // this overrides the default stringification
10 // method so when we stringify the action creator
11 // we get the action type
12 actionCreator.toString = () => `${type}`;
13 return actionCreator;
14}
Example usage for createAction
#
1const addTodo = createAction<ToDo>("ADD_TODO");
2addTodo({
3 id: "1",
4 text: "build my own createAction",
5 completed: true,
6});
7/*
8{
9 type: 'ADD_TODO',
10 payload: {
11 id: '1',
12 text: 'build my own createAction',
13 completed: true
14 },
15}
16*/
BYO createReducer
#
createReducer
is a function that accepts an object where the keys are the
action type and the values are the reducer.
The redux-toolkit
version of createReducer
leverages
immer to handle state updates. I won't go
into the details of how immer
works but just know that it is a clever way for
the end-developer to appear to mutate their state object while under-the-hood
immer
actually handles updates to the state in an immutable, redux-friendly
manner.
For the purposes of our demonstration, we will not be using immer
.
1// for the purposes of this demonstration I'm removing
2// types because otherwise it would dramatically increase
3// the complexity of this code.
4function createReducer(initialState, reducers) {
5 /*
6 This is a reducer function that selects one of the
7 other reducer functions based on the action type (key).
8 When we call this reducer, we do a lookup on our
9 `reducers` object by the key which, in this case,
10 is the `action.type`. If there's a match we call that
11 reducer function with the `action.payload`.
12
13 If our `reducers` object was
14 { increment: (state, payload) => state += 1 }
15 and the reducer function received:
16 state = 0, action = { type: 'increment' }
17 we match the action type with the reducers key
18 'increment',call that reducer function, and the new
19 state value would be `1`.
20 */
21 const reducer = (state = initialState, action) => {
22 const caseReducer = reducers[action.type];
23 if (!caseReducer) {
24 return state;
25 }
26 // note that I am not passing the entire action
27 // object to each reducer, simply the payload
28 return caseReducer(state, action.payload);
29 };
30
31 return reducer;
32}
Example usage for createReducer
#
1import { createStore } from "redux";
2
3type State = ToDo[];
4const addTodo = createAction<ToDo>("ADD_TODO");
5const toggleTodo = createAction<string>("TOGGLE_TODO");
6const reducer = createReducer([], {
7 addTodo: (state: State, payload: ToDo) => {
8 return [...state, action.payload];
9 },
10 toggleTodo: (state, payload: string) => {
11 return state.map((todo) => {
12 // when we find the todo id that
13 // matches the payload we toggle the completed state
14 if (todo.id === payload) {
15 return { ...todo, completed: !todo.completed };
16 }
17 return todo;
18 });
19 },
20});
21
22const store = createStore(reducer, []);
23store.dispatch(
24 addTodo({
25 id: "1",
26 text: "byo createAction",
27 completed: true,
28 }),
29);
30store.dispatch(
31 addTodo({
32 id: "2",
33 text: "byo createReducer",
34 completed: false,
35 }),
36);
37store.dispatch(
38 addTodo({
39 id: "3",
40 text: "byo createSlice",
41 completed: false,
42 }),
43);
44/*
45 [
46 { id: '1', text: 'byo createAction', completed: true }
47 { id: '2', text: 'byo createReducer', completed: false }
48 { id: '3', text: 'byo createSlice', completed: false }
49 ]
50*/
51store.dispatch(toggleTodo("2"));
52/*
53 [
54 { id: '1', text: 'byo createAction', completed: true }
55 { id: '2', text: 'byo createReducer', completed: true }
56 { id: '3', text: 'byo createSlice', completed: false }
57 ]
58*/
createSlice
implementation #
Okay, now that we have our implementation for createAction
and createReducer
built, we can move onto building our createSlice
.
1// helper to build action types scoped to the
2// slice name to avoid naming conflicts
3const actionTypeBuilder = (slice) => (action) =>
4 slice ? `${slice}/${action}` : action;
5
6export default function createSlice({
7 name,
8 initialState,
9 reducers,
10 extraReducers = {},
11}) {
12 const actionKeys = Object.keys(reducers);
13 const createActionType = actionTypeBuilder(name);
14
15 /*
16 `createSlice` will create an action for each key:value
17 pair inside the main `reducers` property.
18 `extraReducers` does not create an action for the key:value
19 pair which allows outside actions to map to a
20 reducer inside our slice.
21 */
22 const reducerMap = actionKeys.reduce((map, action) => {
23 map[createActionType(action)] = reducers[action];
24 return map;
25 }, extraReducers);
26
27 // using our `createReducer` :tada:
28 const reducer = createReducer(initialState, reducerMap);
29
30 // this builds an object where the key is the
31 // actionType and the value is the corresponding
32 // actionCreator
33 const actionMap = actionKeys.reduce((map, action) => {
34 const type = createActionType(action);
35 // using our `createAction` :tada:
36 map[action] = createAction(type);
37 return map;
38 }, {});
39
40 return {
41 actions: actionMap,
42 reducer,
43 name,
44 };
45}
Example usage for createSlice
#
1import { createStore } from "redux";
2
3const { reducer, actions } = createSlice({
4 name: "todos",
5 initialState: [],
6 reducers: {
7 addTodo: (state: State, payload: ToDo) => {
8 return [...state, action.payload];
9 },
10 toggleTodo: (state, payload: string) => {
11 return state.map((todo) => {
12 if (todo.id === payload) {
13 return { ...todo, completed: !todo.completed };
14 }
15 return todo;
16 });
17 },
18 },
19});
20const { addTodo, toggleTodo } = actions;
21console.log(
22 addTodo({
23 id: "1",
24 text: "build my own createAction",
25 completed: true,
26 }),
27);
28/*
29{
30 type: 'todos/ADD_TODO',
31 payload: {
32 id: '1',
33 text: 'build my own createAction',
34 completed: true
35 },
36}
37*/
38
39// after this point everything works exactly
40// the same as our previous example
41const store = createStore(reducer, []);
42store.dispatch(
43 addTodo({
44 id: "1",
45 text: "byo createAction",
46 completed: true,
47 }),
48);
49
50store.dispatch(
51 addTodo({
52 id: "2",
53 text: "byo createReducer",
54 completed: false,
55 }),
56);
57store.dispatch(
58 addTodo({
59 id: "3",
60 text: "byo createSlice",
61 completed: false,
62 }),
63);
64/*
65 [
66 { id: '1', text: 'byo createAction', completed: true }
67 { id: '2', text: 'byo createReducer', completed: false }
68 { id: '3', text: 'byo createSlice', completed: false }
69 ]
70*/
71store.dispatch(toggleTodo("2"));
72/*
73 [
74 { id: '1', text: 'byo createAction', completed: true }
75 { id: '2', text: 'byo createReducer', completed: true }
76 { id: '3', text: 'byo createSlice', completed: false }
77 ]
78*/
79// all of our todos are done!
80store.dispatch(toggleTodo("3"));
Conclusion #
This article demonstrates how leveraging a few simple helper functions
significantly reduces the amount of boilerplate code required to add state and
reducer logic to your redux app. All three of these functions can be used
independently of each other. I also hope this article demystifies createSlice
,
which is now considered the canonical way to use redux.