The purpose of this series is to provide a reader with an insight into the data flow in Redux applications and how React components fit into it. To illustrate the Redux architecture we will build a simple application - a list of employees filterable by position.
Each part of the series is going to have a separate branch in the repository.
We'll start with a simple static data collection, progressively adding features and tools such as unit tests, async API calls, debugging tools routing and middleware.
ECMAScript 6 and WebPack module bundler are not going to be discussed in details, you can find many references in the web.
To start off with the project I've used Dan Abramov's (the creator of Redux) boilerplate: https://github.com/gaearon/react-hot-boilerplate
Before proceeding with the theory, let's run the code locally: clone the repository, check out to the 01-introduction
branch and run:
npm install
npm start
Then point your browser to http://localhost:3000
In short, Redux is a library which uses a handful of methods to manage the application state. It can (but doesn't have to) be used with React. For this purpose we'll use a library react-redux
- the official bindings for Redux.
The Redux building blocks are:
- Reducers
- Store
- Actions
I'm going to briefly describe the above elements, using our application as the example. The datailed description can be found at Redux creator's page: http://redux.js.org/.
The below diagram shows the organisation of Redux element and the general data flow:
The bootstrap file is index.js
. Here the store object is created with reducers
function as a parameter and passed down to React components.
Normally, for a React component, you would see something like ReactDOM.render(<App />, document.getElementById('root'))
, which renders the App
component and mounts it in the DOM element. However, Redux comes with a root Provider
component that wraps the App
component and allows passing the store
object to its children components.
The store
object holds the application state and can be updated with dispatch(action)
.
In order to bind the state to components Redux uses connect
wrapper. connect
takes two function arguments:
- mapStateToProps(state)
- mapDispatchToProps(dispatch) - not mandatory
The state gets transferred into a component's props with the mapStateToProps
method, while the mapDispatchToProps
passes functions to a component's props. These functions can be called in components with events like onClick
dispatching action creators (functions that returns actions with type and payload) which in turn modify the application state.
Let's take the example of our PositionFilter
component.
- Mapping state to props:
const mapStateToProps = (state) => {
return {
positions: positionList,
currentFilter: state.positionFilter
}
}
The keys of the returned objects correspond to the prop names while the values of the object are the properties of the application state tree.
positions
is an array with position names and currentFilter
holds the currently selected position. They are used in the component's render
function:
render() {
let spanStyle = (pos) => {
let s = {
marginRight: 10,
cursor: 'pointer'
}
if (pos === this.props.currentFilter) {
s['backgroundColor'] = 'grey'
}
// @TODO ugly - refactor
return s
}
return (
<div>
{this.props.positions.map(position =>
<span
key={position}
style={spanStyle(position)}
onClick={this.props.filterPositions.bind(null, position)}
>
{position}
</span>
)}
</div>
)
}
- Mapping dispatch to props
mapDispatchToProps
method takesdispatch
as a parameter which is used to dispatch a proper action creator.
const mapDispatchToProps = (dispatch) => {
return {
filterPositions: (name) => dispatch(setPositionFilter(name))
}
}
Here, we are passing to the component's props a function filterPositions
which takes a position name as a parameter.
In the component, the function is bound to onClick
event: onClick={this.props.filterPositions.bind(null, position)}
.
When a user clicks on the position filter the filterPositions
method dispatches setPositionFilter
action creator (actions/index.js). See the diagram above for a general flow.
What the positionFilter
component returns is a 'raw' React component bound to the application state via mapStateToProps
and mapDispatchToProps
method using connect
wrapper: export default connect(mapStateToProps, mapDispatchToProps)(PositionFilter)
.
As stated in the official documentation:
The reducer is a pure function that takes the previous state and an action, and returns the next state.
The signature of a reducer is (previousState, action) => newState
.
Our simple positionFilter
reducer provides only one function:
export default (state=null, action) => {
switch(action.type) {
case 'SET_POSITION_FILTER':
if (!state || state !== action.name) {
return action.name
} else {
return null
}
default:
return state
}
}
It sets the initial state as null
. When the action SET_POSITION_FILTER
is dispatched, by clicking the position filter in the PositionFilter
component, the positionFilter
reducer is called. It takes the previous state (first arguent) and compares it to the current selected (action.name
) to determine if the filter should be switch on or off.
Our other reducer, employees.js
returns the initial state which is the employees collection (we will add cases in the upcoming parts)
export default (state=employeeList, action) => {
switch(action.type) {
default:
return state
}
}
NOTE: positions should have their own reducer, with the initial state and not being manually passed to mapStateToProps
. We will correct this in the future.
The combineReducers
function (reducers/index.js
file) combines the two reducers into a single state object in form of separate properties:
export default combineReducers({
employees,
positionFilter
})
(Note the ES6 object literal shorthand)
The above means that the employees state
will be managed (updated) by the employees reducer
and the positionFilter state
by the positionFilter reducer
.
If you look into the EmployeeList
component, you'll notice that the mapStateToProps
function has access to both employees
and positionFilter
pieces of the global state object:
const mapStateToProps = (state) => {
return {
employees: getEmployeeList(state.employees, state.positionFilter)
}
}
NOTE: for the sake of clarity, we are only using components. The Redux approach makes a difference between containers
and components
. More about it in the future parts of the series.
To see how the employee list gets updated check the blue elements flow in the diagram.
Let's recap the dispatch
flow:
-
user clicks on a position filter item ->
setPositionFilter
action is called passingname
as payload, -
positionFilter
reducer function is called, setting the actionname
as the currentstate.positionFilter
, -
updated
state.positionFilter
is then used in theEmployeeList
component to filter the employee array (getEmployeeList
function)
That's it for this part. Stay tuned for more details about Redux and React in the upcoming posts.