In this tutorial, we will discover what Redux is all about and how you can leverage from it when building your next project.
Redux! Redux!! Redux!!! What in the world is Redux and why do I need it? I asked myself this question when I started learning how to build single page apps (SPA) to include rich interaction on my apps. SPA has the ability to re-render different parts of the UI without requiring server roundtrip. This is achieved by separating the different data which represent the state of the application, from the presentation of these data.
The view layer renders a representation of these data to the UI. A view can be made up of different components. As an example, consider an online store with a product listing page. The page could contain components that represent the different products and their prices, a visual count of the total items in the cart, and a component to suggest similar products to purchased items.
The model layer contains data to be rendered by the view layer. Each component in the view are independent of each other, each rendering a predictable set of UI elements for the given data, but multiple components can share the same data. When there is a change in the model, the view re-renders and updates the component affected by the model update.
The application state can be stored in random objects in-memory. It’s also possible to keep some state in the DOM. But having the state scattered around can easily lead to unmanageable code. It gets hard to debug. If multiple views or components share similar data, it’s possible to have that data stored in a different memory location, and the view components will not be in sync with each other.
With a separation of views from models, data gets passed from the model to the view. If there are changes based on user interactions, this will update the model and this model update could possibly trigger an update to another model and also update another view component(s) which can also trigger an update to a model.
One of the known issues with this unpredictable flow of data was the notification bug on Facebook. When you’re logged in to Facebook, you see a notification for new messages. When you read it, the notification clears. After some interactions on the site, the notification comes up again, then you check and there are no new messages and the notification clears. When you interact more with the app, the notification comes back again and this goes on in a cycle.
It is easy to add complexity to the code if the state isn’t managed properly. Therefore, it is better to have one place where the data lives, particularly when the same data has to be shown in multiple places in the view. With a haphazard flow of data, it becomes hard to reason about state changes and predict the possible outcome of a state change.
It is easy to add complexity if the state isn’t managed properly. Therefore, it is better to have one place where the data lives, particularly when the same data has to be shown in multiple places in the view. View components should read data from this single source and not keep their own version of the same state separately. Hence, the need for a single source of truth.
At Facebook they wanted an easier way to predict state changes and so came up with a pattern called Flux. Flux is a data layer pattern for managing the flow of data. It stipulates that data should only flow in one direction, with application state contained in one location (the source of truth), and the logic to modify the state just in one place.
Flux
The diagram above describes the flow of data in flux.
By implementing an application architecture that allows data to only flow in one direction, you create more predictable application states. If a bug shows up, a unidirectional data flow will make it much easier to pinpoint where the error is, as data follows a strict channel.
Redux
There are varying implementations of this pattern. We have Fluxxor, Flummox, Reflux, etc, but Redux stands tall above them all. Redux took the concepts of Flux and evolved it to create a predictable state management library that allows for easy implementation of logging, hot reloading and time traveling, undo and redo, taking cues from Elm architecture and avoiding the complexity of implementing those.
Dan Abramov, the Redux creator, created it with the intention of getting better developer tools support, hot reloading and time travel debugging but still keeping the predictability that comes with Flux. Redux attempts to make state mutations predictable.
Redux, following in the footsteps of Flux, has 3 concepts:
1{ 2 "cartItem" : [ 3 { 4 "productName" : "laser", 5 "quantity" : 2 6 }, 7 { 8 "productName" : "shirt", 9 "quantity" : 2 10 } 11 ], 12 "selectedProduct" : { 13 "productName" : "Smiggle", 14 "description" : "Lorem ipsum ... ", 15 "price" : "$30.04" 16 } 17 }
1store.dispatch({ 2 type: 'New_CART_ITEM', 3 payload: { 4 "productName" : "Samsung S4", 5 "quantity" : 2 6 } 7 }) 8 dispatch(action) emits the action, and is the only way to trigger a state change. To retrieve the state tree, you call store.getState().
1function shoppingCart(state = [], action) { 2 switch (action.type) { 3 case 'New_CART_ITEM': 4 return [...state, action.payload] 5 default: 6 return state 7 } 8 }
What we did was to return a new state which is a collection of the old cart items, in addition to the new one from the action. Rather than mutate the previous state, you should return a new state object, and this really helps with time travel debugging. There are things you should never do inside a reducer, and they are:
To demonstrate the workings of Redux, we’re going to make a simple SPA to show how we can manage data in Redux and present the data using React.
To set up, run the following commands in the terminal:
1$ git clone git@github.com:StephenGrider/ReduxSimpleStarter.git 2 $ cd ReduxSimpleStarter 3 $ npm install
We’ve just cloned a starter template for what we’ll be building in this section. It’s set up react and downloaded the Redux and react-redux npm packages. We’ll be building an application that allows us to take short notes as To-do items or keywords that remind of something.
Actions are plain JavaScript objects that must have a type, and reducers determine what to do based on specified action. Let’s define constants to hold the different actions. Create a new file called types.js
in ./src/actions
with the following content:
1export const FETCH = 'FETCH'; 2 export const CREATE = 'CREATE'; 3 export const DELETE = 'DELETE';
Next, we need to define actions and dispatch them when needed. Action creators are functions that help create actions, and the result is passed to dispatch()
. Edit the index.js
file in the actions folder with the following content:
1import { FETCH, DELETE, CREATE } from './types'; 2 3 export function fetchItems() { 4 return { 5 type: FETCH 6 } 7 } 8 9 export function createItem(item) { 10 let itemtoAdd = { 11 [Math.floor(Math.random() * 20)]: item 12 }; 13 14 return { 15 type: CREATE, 16 payload: itemtoAdd 17 } 18 } 19 20 export function deleteItem(key) { 21 return { 22 type: DELETE, 23 payload: key 24 } 25 }
We defined 3 actions to create, delete and retrieve items from the store. Next, we need to create a reducer. Math.floor(Math.random() * 20
is used to assign a unique key to the new item being added. This isn’t optimal but we’ll use it here just for the sake of this demo. Add a new file in the reducer directory called item-reducer.js
:
1import _ from 'lodash'; 2 import { FETCH, DELETE, CREATE } from '../actions/types'; 3 4 export default function(state = {}, action) { 5 switch (action.type) { 6 case FETCH: 7 return state; 8 case CREATE: 9 return { ...state, ...action.payload }; 10 case DELETE: 11 return _.omit(state, action.payload); 12 } 13 14 return state; 15 }
Having defined a reducer, we need to connect it to our application using the combineReducer() function. Inside the reducer folder, open and edit the file index.js
:
1import { combineReducers } from 'redux'; 2 import ItemReducer from './item-reducer'; 3 4 const rootReducer = combineReducers({ 5 items: ItemReducer 6 }); 7 8 export default rootReducer;
We pass the reducer we created to the combinedReducer function, where the key is the piece of state that reducer is responsible for. Remember, reducers are pure functions that return a piece of the application state. For a larger application, we could have different reducers each for a specific application domain. With the combineReducer function, we’re telling Redux how to create our application state, thus, thinking and designing how to model your application state in Redux is something you should do beforehand.
With Redux setup on how to manage our state, the next thing is to connect the View (which is managed by React) to Redux. Create a new file item.js
inside the components directory. This will be a smart component because it knows how to interact with Redux to read state and request state change. Add the content below to this file:
1import React, { Component } from 'react'; 2 import { connect } from 'react-redux'; 3 import * as actions from '../actions'; 4 5 class Item extends Component { 6 handleClick() { 7 this.props.deleteItem(this.props.id); 8 } 9 10 render() { 11 return ( 12 <li className="list-group-item"> 13 {this.props.item} 14 <button 15 onClick={this.handleClick.bind(this)} 16 className="btn btn-danger right"> 17 Delete 18 </button> 19 </li> 20 ); 21 } 22 } 23 24 export default connect(null, actions)(Item);
This component displays an item and allows us to delete it. The connect()
function takes the React component in its dumb state (i.e has no knowledge of Redux nor how to interact with it) and produces a smart component, connecting the action creators to the component such that if an action creator is called, the returned action is dispatched to the reducers.
We will also make a second smart component that will render the previous component as a list of items and also allow us to add new items. Update the file app.js
inside the components folder with the content below:
1import _ from 'lodash'; 2 import React, { Component } from 'react'; 3 import { connect } from 'react-redux'; 4 import * as actions from '../actions'; 5 import Item from './item'; 6 7 class App extends Component { 8 state = { item: '' }; 9 10 componentWillMount() { 11 this.props.fetchItems(); 12 } 13 14 handleInputChange(event) { 15 this.setState({ item: event.target.value }); 16 } 17 18 handleFormSubmit(event) { 19 event.preventDefault(); 20 21 this.props.createItem(this.state.item, Math.floor(Math.random() * 20)) 22 } 23 24 renderItems() { 25 return _.map(this.props.items, (item, key) => { 26 return <Item key={key} item={item} id={key} /> 27 }); 28 } 29 30 render() { 31 return ( 32 <div> 33 <h4>Add Item</h4> 34 <form onSubmit={this.handleFormSubmit.bind(this)} className="form-inline"> 35 <div className="form-group"> 36 <input 37 className="form-control" 38 placeholder="Add Item" 39 value={this.state.item} 40 onChange={this.handleInputChange.bind(this)} /> 41 <button action="submit" className="btn btn-primary">Add</button> 42 </div> 43 </form> 44 <ul className="list-group"> 45 {this.renderItems()} 46 </ul> 47 </div> 48 ); 49 } 50 } 51 52 function mapStateToProps(state) { 53 return { items: state.items }; 54 } 55 56 export default connect(mapStateToProps, actions)(App)
This is a smart component (or container) that calls fetchItems()
action creator once the component is loaded. We’ve also used the connect function to link the application state in Redux to our React component. This is achieved using the function mapStateToProps
which takes in the Redux state tree object as an input parameter and maps a piece of it (items) to props of the React component. This allows us to access it using this.props.items
. The remainder of the file allows us to accept user input and add it to the application state.
Run the application using npm start
and try adding a few items, like in the image below:
Supporting rich interactions with multiple components on a page means that those components have many intermediate states. SPA has the ability to render and redraw any part of the UI without requiring a full page reload and server roundtrip. If data aren’t managed properly, scattered all over the UI or put in random object in-memory, things can easily get intertwined. So, it is much better to separate the view and the models for the view. Redux does a good job of clearly defining a way to manage your data and how it changes. It is driven by 3 core principles, which are:
Therefore making it a predictable state container for JavaScript application.
Find the source code here.