The hardest part on writing this tutorial is precisely how to explain what redux is in plain language. The documentation describes it as
Redux is a predictable state container for JavaScript apps.
So, it's a tool that manages the state (or the data) of our entire application. It's ideal for complex Single Page Applications.
In a classical website where ajax isn't used, the data would come from the server into the page. When the user tries to add an item into the basket, a new request is made to the server, the browser would reload and the users would see the result of their action. Redux manages all the data and all the actions interact with it rather than the server. Hence the single page application doesn't reload.
When creating a SPA, especially anything as big as to require redux, it would be logical to use a JavaScript framework such as React, Angular and so forth. However, in order to understand how redux works, I decided to use pure JavaScript. Creating a functional demo is messy, but getting your head around redux, I hope it will be very clear.
By the end we will have gone through the redux code required to manage a basic shopping card demo. A live demo is here and the complete code is in github. The aim of the tutorial however, is to explore how we would use Redux, to manage the state of this application:
We have all the books that would come from a database on the left, and everything that the user want's to buy, on the right.
In basic terms, the books on the shop and on the basket components come from the Redux store. The Redux store is a JSON object which is made available throughout our app. The architecture of our object would be like this:
{
baseket: [],
shop: [{
id: 1,
title: 'Ways of seeing',
img: 'cover.png',
price: '23.73'
}]
}
Note that the basket would be empty and the shop would be full. If this was a database, the above would represent two tables, the basket and the shop.
Let's create the skeleton of the store
import { createStore, combineReducers } from 'redux';
const storeReducer = (state=[{title:'...'},{}], action) => {
if(state === undefined){
return state;
}
return state;
}
const basketReducer = (state=[], action) => {
if(state === undefined){
return state;
}
return state;
}
const allReducers = combineReducers({
basket: basketReducer,
shop: storeReducer
})
const store = createStore(allReducers)
The store is created through reducers, if we had only one set of data, say only the store, we would not need to use combineReducers
, the one reducer would be passed: createStore(storeReducer)
. So, combineReducers
simply gives us the ability to separate the data logically. As to why we need reducers at all will become apparent soon.
1. Interacting with the Redux store API
If we console the store right now, we'll see the methods it provides us so that we can interact with it.
store.dispatch(action)
store.subscribe(listener)
store.getState()
From this point, we will interact with the store through those three methods.
We'll want the books to be displayed, so we'll use store.getState().shop
. We'll want the store to be effected when the user clicks the book, we'll use store.dispatch()
. We'll want the basket to show the newly added items, will use store.subscribe()
to listen for the changes.
1.2 Getting items on the page
The first time the store is instantiated it will return the current state. We know that the shop
object contains the books. To display them on the page, we make use of the store.getState()
method.
store.getState().shop.map(({ id, title, price, img }) => {
insertShopDOM(shopElem, id, title, price, img)
return null;
})
store.getState()
clearly returns the whole store. We then select the shop object array and loop through it.
To not distract us from learning redux, I don't want to waist time on DOM manipulation, functions such as insertShopDOM
simply manipulate the DOM, nothing to do with redux. That's how items are picked from the store, what insertShopDOM
does with that information is up to you. Here's just one of many alternatives:
function insertShopDOM(shopElem, id, title, price, img) {
shopElem.innerHTML += `
<div data-id=${id} class="box item">
<img src=${img}>
<div class="meta">
<h2>${title}</h2>
<p>£<span>${price}</span></p>
</div>
</div>`
}
With that, we have displayed all our books on the page.
2. Reducers and Actions
Reducers shine and the usefulness of this architecture becomes clear when we want to interact with the store. In reality We are not interacting with the store. The store is read-only, and we just read it above. The actions are retrieved by the reducers and it's they that reply.
First, lets create an action. Basically, on some click, store.dispatch(someAction)
dispatch an action
[...shopItems].map(item => {
item.addEventListener('click', e =>{
...
store.dispatch({
type: 'ADD_TO_CARD',
payload: { id, title, price, img, qty: '1' }
})
})
})
The dispatch structure is important. We are passing an object with two properties. type
has to be named type
and conventionally the name should be all caps. payload
can be named anything, but conventionally is payload
. Above we are dispatching an action named ADD_TO_CARD
and which ever reducer handles it, it will get the payload
object.
2.1. The power of reducers
As we dispatch our actions, all the reducers can read its object. Both the storeReducer
and the basketReducer
can act upon the dispatched action. Since this action is to add data to the store, basketReducer
should respond. Let's write the code for that responce.
const basketReducer = (state=cartInitState, action) => {
...
if(action.type === 'ADD_TO_CARD'){
const data = action.payload;
const newState = [ ...state, data ];
return newState;
}
return state;
}
Again, out of all the actions we can dispatch, the basketReducer
will only respond to the ADD_TO_CARD
type. Then it creates a new version of the store state and passes it back.
2.2. Never mutate state
For every action that reducers respond to, they have to return a copy of the state, never an updated version of the original. Redux requires reducers to be immutable.
That's what we did above. The ES6 spread operator returns a new state array object, and we are adding the new data to that new state. If we were to use ES5, the code would be const newState = state.concat(data)
.
Though it's beyond the scope of this tutorial, when developing with Redux, the Redux browser extension would help you see the benefits of immutable state, by allowing you to "time travel" through actions
If you install the Redux extension for chrome or firefox and then view the demo whilst having the extension open, you'll see the action name appear everytime an action is dispatched, and if you click "skip" (as shown above, on the right) you'll see your actions undone, all because state is updated immutably.
3. Subscribing to the store
We dispatched an action, the basketReducer
reducer acted by returning a new state, now we need to take that new state and add it to our application.
The good thing about using redux is that we don't care what button was clicked for the basket to render some html. We just need to act upon the changes of the Redux state.
store.subscribe(() => {
cartElem.innerHTML = '';
store.getState().basket.map(({ id, title, price, img, qty }) => {
insertCartDOM(id, title, price, img, qty)
});
})
store.subscribe()
allows us to do something when the state changes. Basically, what ever happens to get Redux state to change will also cause the above to run. When ever the state changes, we are looping through the basket and displaying it's content.
The app so far looks like this
4. A final action to drive everything home
The user has added few books in the basket, now they decided to remove some. The process is the same as before, dispatch an action when user clicks the x
button.
item.addEventListener('click', e=>{
let id = item.dataset.id;
store.dispatch({ type: 'REMOVE_FROM_CARD', payload: { id } })
})
On click we are dispatching an action REMOVE_FROM_CARD
and passing the the id
.
On the basketReducer
reducer we are going to listen to that new action.
const basketReducer = (state=cartInitState, action) => {
if(state === undefined){
return state;
}
if(action.type ==="REMOVE_FROM_CARD"){
return [...state].filter(book => book.id !== action.payload.id )
}
return state;
}
The spread creates a copy of the state and by using filter
we make sure the returned state has all the books apart from that with the same id
that came from the action.
Conclusion
That's all there is to Redux. As we said, the API we have to work with is three methods and as you saw it will be the easiest part of developing a single page application.
It does need repeating that Redux should be used with a framework and the benefits become clear as the application grows.
To save you from having to scroll up, here's the demo and here's the code.
Top comments (1)
Thank you.