Here we talk about a powerful library called redux toolkit
Open up a terminal and type:
npm i @reduxjs/toolkit
Redux toolkit provides a bunch of tools to simplify redux code.
One of them is for creating the store. Typically, we extract createStore from redux. Then put your reducers into createStore as a higher order function. We export this store to be used in our index.
1 2 3 4 5 6 7 8 |
import { createStore } from 'redux'; import reducer from './questions'; export default function MyStore() { const store = createStore( reducer, ); return store; |
If we use createStore way, we have do a lot of stuff manually.
But with redux tookit, we have a new way:
configureStore
We import our reducer(s) from our feature and insert via an object with property reducer:
1 2 3 4 5 6 7 8 |
import { configureStore } from '@reduxjs/toolkit'; import reducer from './questions'; export default function () { return configureStore({ reducer, }); } |
A pro on using configureStore is that allows us to dispatch asynchronous actions.
Actions with less boilerplate
Typically we create actions by defining constants, then define objects with properties type and payload. The type property uses the defined constants.
However, there is a problem. Every action returns an action like so:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export const questionAdded = description => ({ type: QUESTION_ADDED, payload: { description } }); export const questionAnswer = (id, answer) => ({ type: QUESTION_ANSWER, payload: { id, answer } }); |
We manually create these by hand.
Redux toolkit has a function called createAction to do this for us.
1 2 3 4 5 6 7 8 |
const questionAdded = createAction("questionAdded"); questionAdded({ description: 'what is the meaning of life' }); const questionAnswer = createAction("questionAnswer"); questionAnswer({ id: 1, answer: '42' }); // get action's type console.log(questionAdded.type); console.log(questionAdded.toString()); |
Hence, in your action creator, we re-factor like so:
1 2 3 |
export const questionAdded = createAction("questionAdded"); export const questionAnswer = createAction("questionAnswer"); export const questionRemoved = createAction("questionRemoved"); |
Now, in your reducer you can replace the type with our createAction.type:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
export default function reducer(state = [], action) { switch(action.type) { case questionAdded.type: return [ ...state, { id: ++lastId, description: action.payload.description, answer: null } ]; case questionRemoved.type: return state.filter(question => question.id !== action.payload.id); case questionAnswer.type: return state.map(question => { if (question.id !== action.payload.id) { return question; } else { return { ...question, answer: action.payload.answer } } }); default: return state; } } |
Notice this:
1 2 3 4 5 6 7 8 9 |
case questionAdded.type: return [ ...state, { id: ++lastId, description: action.payload.description, answer: null } ]; |
When we use createAction to create our action object, the parameter we give to createAction is the payload object. Make sure we have properties ‘description’.
make changes in your index.js:
1 2 3 4 5 6 |
import configureStore from './store/configureStore'; import { questionAdded } from './store/questions'; const store = configureStore(); store.dispatch(questionAdded({ description: 'what color is the sky'})); store.dispatch(questionAnswer({ id: 1, answer: 'go left' })); |
Improving the Reducers
A lot developers don’t like switch statements. Nor do they like returning immutable object patterns.
This is where we can use toolkit. We can create reducer without switch statements. Toolkit uses immer. Automatically translated to immutable update pattern.
Similar to createAction, we use library function createReducer.
When we call this parameter, the first parameter is the initial state
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { createAction, createReducer } from '@reduxjs/toolkit'; createReducer( [], { // key: value // action => function (event => eventHandler) questionAdded: (state, action) => { state.push({ id: ++lastId, description: action.payload.description, answer: action.payload.answer }); } }); |
Under the hood, redux toolkit uses immer, so the manipulation of your state gets translated to immutable updated pattern.
Reminder: immer uses function product with initial state. It takes an argument draftState
draftState is actually a proxy. So it will record all changes applied to a copy of the initial state. Thus, preventing the manipulation of the original object.
1 2 3 4 |
produce(initialState, draftState => { // here we write mutating code draftState.x = 1; }) |
Thus, our function:
1 2 3 4 5 6 7 |
(state, action) => { state.push({ id: ++lastId, description: action.payload.description, answer: action.payload.answer }); } |
is actually the draftState anonymous function that does the mutating.
By using the functionality from redux toolkit, you can streamline your code nicely:
question.js
1 2 3 4 5 6 7 8 9 10 11 12 |
import { createAction, createReducer } from '@reduxjs/toolkit'; export const questionAdded = createAction("questionAdded"); export default createReducer([], { [questionAdded.type]: (questions, action) => { questions.push({ id: ++lastId, description: action.payload.description, answer: action.payload.answer }); } }); |