Managing application state with Redux
Bài đăng này đã không được cập nhật trong 3 năm
1. Introduction
Today, I bring you Redux, a library that's popularly paired with React and is used for managing application state. Let's go over core Redux principles/patterns and work on a small example application together.
2. Problem
We might have heard about React components a lot of time. Conceptually, components are like functions which accept inputs (props) and return React elements in charge of describing what should be rendered on the screen. Nothing more. In React, data flows through components, which is specifically called "unidirectional data flow" — data flows in one direction from parent to child. Going on with the flow of such characteristic, it is kind of vague how two non parent-child components will communicate in React.
Of course, you can pass a callback function to the props of child component, then each time the function is called, you save the data into the state of the parent component and pass it as the props of the other component. Like this:
Such feat is not impossible, however, not recommended. Even if React does have features to support this approach, implementing direct component-to-component communication this way is considered poor practice by many developers due to being error prone and possibilities of leading to spaghetti code — an old term for code that is hard to follow.
This is the critical moment when Redux comes in handy.
3. Redux
3.1. Concept
Redux is a tool for managing both data-state and UI-state in JavaScript applications. It's ideal for Single Page Applications (SPAs) where managing state might becomes complex over time.
In a Redux application, all of your application states is stored in one place, called a store
. Component A, which is in charge of initializing changes, dispatch
an action which leads to state changes in the store. Meanwhile, component B, which needs to be aware of any state changes can subscribe
to the store.
The store can be considered a "middleman" for all state changes in the application. With Redux involved, components don't communicate directly with each other. Rather than that, all state changes must go through the single source of truth, the store.
Where do all components get their state? The store
.
And where should they send their state changes? Also the store
.
So in other word, instead of worrying about a ton of components that need the state change, the component initiating the change is only concerned with the "mission" of dispatching the change to the store. After that, all components subscribing to the store can have their state changes. This is how Redux makes data flow easier to reason about.
3.2. Redux & Flux
The general concept of using store(s) to coordinate application state is a pattern known as the Flux design pattern, which compliments unidirectional data flow architectures like React. While Redux is a tool, Flux is just a pattern, it's not something you can download. It has been proved that Redux is Flux or is Flux-like, depending on the strictness in which one defines the rules of Flux but ultimately, it does not really matter as long as you carve into your memory Redux's 3 guiding principals:
- Single Source of Truth:
Redux uses only one store for all its application state. Since all state resides in one place, Redux calls this the single source of truth. This one-store approach of Redux is one of the primary differences between it and Flux's multiple store approach.
- State is Read-Only
The only way to mutate the state is to emit an action, an object describing what happened.
- Changes are made with Pure Functions:
The functions that handle actions dispatched and are capable of actually changing the state are called
reducers
. Reducers are "pure" functions - functions that do nothing but return a value based on their parameters while having no side effects into any other part of the application.
4. Implementation
4.1. Create Redux store
To start, create a store with Redux.createStore() and pass all reducers in as arguments. Let's look at a small example with only one reducer:
//reducer
const heroes = (state = [], action) => {
switch (action.type) {
case 'ADD_HERO':
const { hero } = action.payload
return state.concat([hero])
default:
return state
}
}
// Create a store by passing in the reducer
const store = Redux.createStore(heroes)
// Dispatch our first action to express an intent to change the state
store.dispatch({
type: 'ADD_HERO',
payload: { name: 'Oliver Queen', alias: 'Green Arrow' }
})
So what happened above?
First, The store is created with one reducer named heroes
. Reducer heroes
establishes that the inital state of the application is an empty array (state = []
). After that, an action ADD_HERO
is dispatched. The reducers retrieve the parameters (payload: { ... }
) and returns a new state, which updates the store.
There are several things that should be noted here:
- The reducer is actually called twice: Once when the store is created and then again after the action
ADD_HERO
is dispatched. As soon as the store is created, Redux immediately calls the reducers and uses their return values as initial state. After that, reducers are called each time an action is dispatched. Since a returned state from a reducer will become a new state in the store, Redux always exects reducers to return state. - Reducers return a new state instead of mutating the previous state: Mutating state is considered poor practice. Instead of using mutation methods like
push()
orsplice
, non-mutation methods - methods that returns a new output value without mutating directly the input value such asconcat
,filter
,slice
are recommended.
4.2. Implement multiple reducers
Inevitably, most application will need more complex state for the entire application. It is merely the matter of time. Sice Redux uses just one store for the whole application, it is necessary to use nested objects in order to organize state into different sections, for examples, like below:
{
heroReducer: { ... },
villainReducer: { ... }
}
It's still "one store = one object" for the entire application, but it has nested objects for heroState and villainState that can contain all kinds of data. This might seem overly simplistic, but it's actually not that far from resembling a real Redux store.
In order to create a store with nested objects, we'll need to define each section with a reducer:
import { createStore, combineReducers } from 'redux'
// The Hero Reducer
const heroReducer = (state = {
heroes: []
}, action) {
switch (action.type) {
case 'ADD_HERO':
const { hero } = action.payload
return state.concat([hero])
default:
return state
}
}
// The villain Reducer
const villainReducer = function(state = {
villains: []
}, action) {
switch (action.type) {
case 'GET_VILLAIN_LIST':
const { villains } = action.payload
return villains
default:
return state
}
}
// Combine Reducers
const reducers = combineReducers({
heroReducer, villainReducer
})
const store = createStore(reducers)
The use of combineReducers() allows us to describe our store in terms of different logical sections and assign reducers to each section. Now, when each reducer returns initial state, that state will go into it's respective heroState or villainState section of the store. But which Reducer is Called After a Dispatch?
Answer: All of them. Comparing reducers to funnels is even more apparent when we consider that each time an action is dispatched, all reducers will be called and will have an opportunity to update their respective state.
6. Connecting with React-redux
Just to be clear, react, redux, and react-redux are three separate modules on npm. The react-redux module allows us to "connect" React components to Redux in a more convenient way.
Here's what it looks like:
import React from 'react'
import { connect } from 'react-redux'
import store from '../path/to/store'
import { getVillainList } from '../../actions/villain_actions
import VillainList from '../components/VillainList/VillainList,react'
class VillainListContainer extends React.Component {
constructor(props) {
super()
}
componentDidMount() {
const { getVillainList } = this.props
getVillainList()
}
render() {
return (
<VillainList villains={this.props.villains} />
)
}
}
const mapStateToProps = store => {
return store.villainState.villains
}
const mapDispatchToProps = dispatch => ({
dispatch(getVillainList())
})
export default connect(mapStateToProps)(mapDispatchToProps)(VillainListContainer)
We've imported the connect function from react-redux.
mapStateToProps()
will receive an argument which is the entire Redux store. The main idea ofmapStateToProps()
is to isolate which parts of the overall state this component needs as its props. For this reason, we no longer have to set initial state of component. Also notice that we refer tothis.props.villains
instead ofthis.state.villains
since the villains array is now a prop and not local component state.mapDispatchToProps()
will receive an argument which is adispatch
. We know that events can be passed as props of components. It turns out react-redux helps with that too in cases where an event simply needs to dispatch an action.
All rights reserved