Not long ago, I decided to come back to Redux since 5 months ago when I threw it in the corner after the interview.
I read through the documentation and since I use Redux with React most of the time, I read through React-Redux as well. I also followed through an online tutorial to create a small project, but I found it really helpful if I use this post as a documentation to go through the steps again so I can make sure I’m digesting it properly (the tutorial is video-based), and share with anyone who is also confused with the basics and don’t want to rush to advanced.
Premise:
Start a create-react-app boilerplate and have below dependencies installed:
"@testing-library/jest-dom": "⁴.2.4","@testing-library/react": "⁹.3.2","@testing-library/user-event": "⁷.1.2","axios": "⁰.19.2","react": "¹⁶.13.1","react-dom": "¹⁶.13.1","react-redux": "⁷.2.0","react-scripts": "3.4.1","redux-logger": "³.0.6","redux-thunk": "².3.0"
Then, in src
directory, add components
and redux
folder.
Our project aims to help a dessert shop selling ice-creams and cakes, and (in the later stage) will have a list of staffs “fetched” from other place(to mimic the async calls).
Let’s tackle the cake first. Create files as below:
In cakeTypes.js:
export const BUY_ICECREAM = “BUY_ICECREAM”;
And in cakeAction.js:
import { BUY_CAKE } from “./cakeTypes”;export const buyCake = (number = 1) => {
return {
type: BUY_CAKE,
payload: number,
};
};
Remember that Actions are payloads of information that send data from your application to your store. It must be a plain JavaScript object and have a type
property.They are the only source of information for the store. You send them to the store using store.dispatch()
.
Note that the const buyCake
is actually a function- Action creators , that is functions that create actions.
In normal redux, you can just dispatch an action like below:
store.dispatch(buyCake())
But in react-redux, we are using connect()
, which we will discuss soon.
Following up to the action, we will move on to Reducer.
In cakeReducer.js, writes down:
import { BUY_CAKE } from “./cakeTypes”;const initialState = {
numberOfCakes: 10,
};const cakeReducer = (state = initialState, action) => {
switch (action.type) {
case BUY_CAKE:
return {
…state,
numberOfCakes: action.payload
? state.numberOfCakes — action.payload
: state.numberOfCakes — 1,
};
default:
return state;
}
};export default cakeReducer;
What is reducer? Reducers specify how the application’s state changes in response to actions sent to the store. It is basically doing the transformation like below:
(previousState, action) => nextState
Once rule of thumb in reducer is that you have to make it pure, meaning you won’t do like following:
- Mutate its arguments;
- Perform side effects like API calls and routing transitions;
- Call non-pure functions, e.g.
Date.now()
orMath.random()
.
As note that when pass params to reducer, we initialise our state, this is because we need to return the initial state when reducer is called the first time when there’s no action performed.
But we also want to sell icecreams! So we need another reducer. Create files as below:
And put code in each file like below, it’s very similar to what we just did:
//iceCreamType.js
export const BUY_ICECREAM = “BUY_ICECREAM”;//iceCreamAction.js
import { BUY_ICECREAM } from "./iceCreamTypes";export const buyIceCream = () => {
return {
type: BUY_ICECREAM,
};
};//iceCreamReducer.js
import { BUY_ICECREAM } from "./iceCreamTypes";const initialState = {
numberOfIceCreams: 20,
};const iceCreamReducer = (state = initialState, action) => {
switch (action.type) {
case BUY_ICECREAM:
return {
...state,
numberOfIceCreams: state.numberOfIceCreams - 1,
};
default:
return state;
}
};export default iceCreamReducer;
Now the question is how do we group all the individual reducers together. Luckily, Redux provides a utility called combineReducers()
:
So create a rootReducer
under redux
and write:
import { combineReducers } from “redux”;
import cakeReducer from “./cake/cakeReducer”;
import iceCreamReducer from “./iceCream/iceCreamReducer”;const rootReducer = combineReducers({
cake: cakeReducer,
iceCream: iceCreamReducer,
});export default rootReducer;
Note that each of reducers is managing its own part of the global state. The state
parameter is different for every reducer, and corresponds to the part of the state it manages.
What combineReducers()
does is generate a function that calls our reducers with the slices of state selected according to their keys, and combines their results into a single object again. So now if we want to call the state of number of cakes, instead of state.numberOfCakes
we need to call state.cake.numberOfCakes
(Call the cake reducer first by calling .cake
and access the .numberOfCakes
in the return statement of cake reducer).
The next step is the store! Create store.js
under redux
:
import { createStore, applyMiddleware } from “redux”;
import rootReducer from “./rootReducer”;const store = createStore(rootReducer);export default store;
What is a store?
The Store is the object that has the following responsibilities:
- Holds application state;
- Allows access to state via
getState()
; - Allows state to be updated via
dispatch(action)
; - Registers listeners via
subscribe(listener)
; - Handles unregistering of listeners via the function returned by
subscribe(listener)
.
So that’s all we do in the redux
folder part! What we need to do now is just to dispatch the actions, which we will do it in the UI components in React.
But just to recap and be clear about the data flow in redux:
Redux architecture revolves around a strict unidirectional data flow.
The data lifecycle in any Redux app follows these 4 steps:
- You call
store.dispatch(action)
.(which we haven’t done yet) - The Redux store calls the reducer function you gave it with the current state tree and the action(types, payloads, etc), the reducer return the next state tree
- The root reducer may combine the output of multiple reducers into a single state tree.
- The Redux store saves the complete state tree returned by the root reducer, and invoke every listener registered with subscription, listener may call
.getState()
to get current state and update the UI. (In React, this is whensetState()
happens)
Of course there is some mini steps in between for example if we want to plugin a middleware between store dispatching an action until the point when it reaches reducer.
In next session, we will talk about different ways to dispatch an action in React components.