Chapter 9. React Redux

In Chapter 6, we learned how to construct React components. We built the color organizer app using React’s state management system. In the last chapter, we learned how to use Redux to manage our application’s state data. We completed building a store for our color organizer app that is ready to dispatch actions. In this chapter, we are going to combine the UI that we created in Chapter 6 with the store that we created in the last chapter.

The app that we developed in Chapter 6 stores state in a single object in a single location—the App component.

export default class App extends Component {

    constructor(props) {
        super(props)
        this.state = {
            colors: [
                {
                    "id": "8658c1d0-9eda-4a90-95e1-8001e8eb6036",
                    "title": "Ocean Blue",
                    "color": "#0070ff",
                    "rating": 3
                },
                {
                    "id": "f9005b4e-975e-433d-a646-79df172e1dbb",
                    "title": "Tomato",
                    "color": "#d10012",
                    "rating": 2
                },
                {
                    "id": "58d9caee-6ea6-4d7b-9984-65b145031979",
                    "title": "Lawn",
                    "color": "#67bf4f",
                    "rating": 1
                },
                {
                    "id": "a5685c39-6bdc-4727-9188-6c9a00bf7f95",
                    "title": "Party Pink",
                    "color": "#ff00f7",
                    "rating": 5
                }
            ]
        }
        this.addColor = this.addColor.bind(this)
        this.rateColor = this.rateColor.bind(this)
        this.removeColor = this.removeColor.bind(this)
    }

    addColor(title, color) {
        ...
    }

    rateColor(id, rating) {
        ...
    }

    removeColor(id) {
        ...
    }

    render() {
        const { addColor, rateColor, removeColor } = this
        const { colors } = this.state
        return (
            <div className="app">
                <AddColorForm onNewColor={addColor} />
                <ColorList colors={colors}
                           onRate={rateColor}
                           onRemove={removeColor} />
            </div>
        )
    }

}

The App component is the component that holds state. State is passed down to child components as properties. Specifically, the colors are passed from the App component’s state to the ColorList component as a property. When events occur, data is passed back up the component tree to the App component via callback function properties (Figure 9-1).

Figure 9-1. Data flow through the component tree

The process of passing data all the way down and back up the tree introduces complexity that libraries like Redux are designed to alleviate. Instead of passing data up the tree through two-way function binding, we can dispatch actions directly from child components to update application state.

In this chapter, we’ll take a look at various ways to incorporate the Redux store. We will first look at how the store can be used without any additional frameworks. After that, we will explore react-redux, a framework that can be used to integrate a Redux store with React components.

Explicitly Passing the Store

The first, and most logical, way to incorporate the store into your UI is to pass it down the component tree explicitly as a property. This approach is simple and works very well for smaller apps that only have a few nested components.

Let’s take a look at how we can incorporate the store into the color organizer. In the ./index.js file, we will render an App component and pass it the store:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'
import storeFactory from './store'

const store = storeFactory()

const render = () =>
    ReactDOM.render(
        <App store={store}/>,
        document.getElementById('react-container')
    )

store.subscribe(render)
render()

This is the ./index.js file. In this file, we create the store with the storeFactory and render the App component into the document. When the App is rendered the store is passed to it as a property. Every time the store changes, the render function will be invoked, which efficiently updates the UI with new state data.

Now that we have passed the store to the App, we have to continue to pass it down to the child components that need it:

import AddColorForm from './AddColorForm'
import SortMenu from './SortMenu'
import ColorList from './ColorList'

const App = ({ store }) =>
    <div className="app">
        <SortMenu store={store} />
        <AddColorForm store={store} />
        <ColorList store={store} />
    </div>

export default App

The App component is our root component. It captures the store from props and explicitly passes it down to its child components. The store is passed to the SortMenu, AddColorForm, and ColorList components as a property.

Now that we have passed the store from the App, we can use it inside the child components. Remember we can read state from the store with store.getState, and we can dispatch actions to the store with store.dispatch.

From the AddColorForm component, we can use the store to dispatch ADD_COLOR actions. When the user submits the form, we collect the color and the title from refs and use that data to create and dispatch a new ADD_COLOR action:

import { PropTypes, Component } from 'react'
import { addColor } from '../actions'

const AddColorForm = ({store}) => {

    let _title, _color

    const submit = e => {
        e.preventDefault()
        store.dispatch( addColor(_title.value, _color.value) )
        _title.value = ''
        _color.value = '#000000'
        _title.focus()
    }

    return (
        <form className="add-color" onSubmit={submit}>
            <input ref={input => _title = input}
                   type="text"
                   placeholder="color title..." required/>
            <input ref={input => _color = input}
                   type="color" required/>
            <button>ADD</button>
        </form>
    )

}

AddColorForm.propTypes = {
    store: PropTypes.object
}

export default AddColorForm

From this component, we import the necessary action creator, addColor. When the user submits the form, we’ll dispatch a new ADD_COLOR action directly to the store using this action creator.

The ColorList component can use the store’s getState method to obtain the original colors and sort them appropriately. It can also dispatch RATE_COLOR and REMOVE_COLOR actions directly as they occur:

import { PropTypes } from 'react'
import Color from './Color'
import { rateColor, removeColor } from '../actions'
import { sortFunction } from '../lib/array-helpers'

const ColorList = ({ store }) => {
    const { colors, sort } = store.getState()
    const sortedColors = [...colors].sort(sortFunction(sort))
    return (
        <div className="color-list">
            {(colors.length === 0) ?
                <p>No Colors Listed. (Add a Color)</p> :
                sortedColors.map(color =>
                    <Color key={color.id}
                        {...color}
                        onRate={(rating) =>
                            store.dispatch(
                                rateColor(color.id, rating)
                            )
                        }
                        onRemove={() =>
                            store.dispatch(
                                removeColor(color.id)
                            )
                        } />
                )
            }
        </div>
    )
}

ColorList.propTypes = {
    store: PropTypes.object
}

export default ColorList

The store has been passed all the way down the component tree to the ColorList component. This component interacts with the store directly. When colors are rated or removed, those actions are dispatched to the store.

The store is also used to obtain the original colors. Those colors are duplicated and sorted according to the store’s sort property and saved as sortedColors. sortedColors is then used to create the UI.

This approach is great if your component tree is rather small, like this color organizer. The drawback of using this approach is that we have to explicitly pass the store to child components, which means slightly more code and slightly more headaches than with other approaches. Additionally, the SortMenu, AddColorForm, and ColorList components require this specific store. It would be hard to reuse them in another application.

In the next couple of sections, we will look at other ways to get the store to the components that need it.

Passing the Store via Context

In the last section, we created a store and passed it all the way down the component tree from the App component to the ColorList component. This approach required that we pass the store through every component that comes between the App and the ColorList.

Let’s say we have some cargo to move from Washington, DC, to San Francisco, CA. We could use a train, but that would require that we lay tracks through at least nine states so that our cargo can travel to California. This is like explicitly passing the store down the component tree from the root to the leaves. You have to “lay tracks” through every component that comes between the origin and the destination. If using a train is like explicitly passing the store through props, then implicitly passing the store via context is like using a jet airliner. When a jet flies from DC to San Francisco, it flies over at least nine states—no tracks required.

Similarly, we can take advantage of a React feature called context that allows us to pass variables to components without having to explicitly pass them down through the tree as properties.1 Any child component can access these context variables.

If we were to pass the store using context in our color organizer app, the first step would be to refactor the App component to hold context. The App component will also need to listen to the store so that it can trigger a UI update every time the state changes:

import { PropTypes, Component } from 'react'
import SortMenu from './SortMenu'
import ColorList from './ColorList'
import AddColorForm from './AddColorForm'
import { sortFunction } from '../lib/array-helpers'

class App extends Component {

    getChildContext() {
        return {
            store: this.props.store
        }
    }

    componentWillMount() {
        this.unsubscribe = store.subscribe(
            () => this.forceUpdate()
        )
    }

    componentWillUnmount() {
        this.unsubscribe()
    }

    render() {
        const { colors, sort } = store.getState()
        const sortedColors = [...colors].sort(sortFunction(sort))
        return (
            <div className="app">
                <SortMenu />
                <AddColorForm />
                <ColorList colors={sortedColors} />
            </div>
        )
    }

}

App.propTypes = {
    store: PropTypes.object.isRequired
}

App.childContextTypes = {
    store: PropTypes.object.isRequired
}

export default App

First, adding context to a component requires that you use the getChildContext lifecycle function. It will return the object that defines the context. In this case, we add the store to the context, which we can access through props.

Next, you will need to specify childContextTypes on the component instance and define your context object. This is similar to adding propTypes or defaultProps to a component instance. However, for context to work, you must take this step.

At this point, any children of the App component will have access to the store via the context. They can invoke store.getState and store.dispatch directly. The final step is to subscribe to the store and update the component tree every time the store updates state. This can be achieved with the mounting lifecycle functions (see “Mounting Lifecycle”). In componentWillMount, we can subscribe to the store and use this.forceUpdate to trigger the updating lifecycle, which will re-render our UI. In componentWillUnmount, we can invoke the unsubscribe function and stop listening to the store. Because the App component itself triggers the UI update, there is no longer a need to subscribe to the store from the entry ./index.js file; we are listening to store changes from the same component that adds the store to the context, App.

Let’s refactor the AddColorForm component to retrieve the store and dispatch the ADD_COLOR action directly:

const AddColorForm = (props, { store }) => {

    let _title, _color

    const submit = e => {
        e.preventDefault()
        store.dispatch(addColor(_title.value, _color.value))
        _title.value = ''
        _color.value = '#000000'
        _title.focus()
    }

    return (
        <form className="add-color" onSubmit={submit}>
            <input ref={input => _title = input}
                   type="text"
                   placeholder="color title..." required/>
            <input ref={input => _color = input}
                   type="color" required/>
            <button>ADD</button>
        </form>
    )

}

AddColorForm.contextTypes = {
    store: PropTypes.object
}

The context object is passed to stateless functional components as the second argument, after props. We can use object destructuring to obtain the store from this object directly in the arguments. In order to use the store, we must define contextTypes on the AddColorForm instance. This is where we tell React which context variables this component will use. This is a required step. Without it, the store cannot be retrieved from the context.

Let’s take a look at how to use context in a component class. The Color component can retrieve the store and dispatch RATE_COLOR and REMOVE_COLOR actions directly:

import { PropTypes, Component } from 'react'
import StarRating from './StarRating'
import TimeAgo from './TimeAgo'
import FaTrash from 'react-icons/lib/fa/trash-o'
import { rateColor, removeColor } from '../actions'

class Color extends Component {

    render() {
        const { id, title, color, rating, timestamp } = this.props
        const { store } = this.context
        return (
            <section className="color" style={this.style}>
                <h1 ref="title">{title}</h1>
                <button onClick={() =>
                    store.dispatch(
                        removeColor(id)
                    )
                }>
                    <FaTrash />
                </button>
                <div className="color"
                     style={{ backgroundColor: color }}>
                </div>
                <TimeAgo timestamp={timestamp} />
                <div>
                    <StarRating starsSelected={rating}
                                onRate={rating =>
                                    store.dispatch(
                                        rateColor(id, rating)
                                    )
                                } />
                </div>
            </section>
        )
    }

}

Color.contextTypes = {
    store: PropTypes.object
}

Color.propTypes = {
    id: PropTypes.string.isRequired,
    title: PropTypes.string.isRequired,
    color: PropTypes.string.isRequired,
    rating: PropTypes.number
}

Color.defaultProps = {
    rating: 0
}

export default Color

Color is now a component class, and can access context via this.context. Colors are now read directly from the store via store.getState. The same rules apply that do for stateless functional components. contextTypes must be defined on the instance.

Retrieving the store from the context is a nice way to reduce your boilerplate, but this is not something that is required for every application. Dan Abramov, the creator of Redux, even suggests that these patterns do not need to be religiously followed:

Separating the container and presentational components is often a good idea, but you shouldn’t take it as dogma. Only do this when it truly reduces the complexity of your codebase.2

Presentational Versus Container Components

In the last example, the Color component retrieved the store via context and used it to dispatch RATE_COLOR and REMOVE_COLOR actions directly. Before that, the ColorList component retrieved the store via context to read the current list of colors from state. In both examples, these components rendered UI elements by interacting directly with the Redux store. We can improve the architecture of our application by decoupling the store from components that render the UI.3

Presentational components are components that only render UI elements.4 They do not tightly couple with any data architecture. Instead, they receive data as props and send data to their parent component via callback function properties. They are purely concerned with the UI and can be reused across applications that contain different data. Every component that we created in Chapter 6, with the exception of the App component, is a presentational component.

Container components are components that connect presentational components to the data. In our case, container components will retrieve the store via context and manage any interactions with the store. They render presentational components by mapping properties to state and callback function properties to the store’s dispatch method. Container components are not concerned with UI elements; they are used to connect presentational components to data.

There are many benefits to this architecture. Presentational components are reusable. They are easy to swap out and easy to test. They can be composed to create the UI. Presentational components can be reused across browser applications that may use different data libraries.

Container components are not concerned with the UI at all. Their main focus is connecting the presentation components to the data architecture. Container components can be reused across device platforms to connect native presentational components to the data.

The AddColorForm, ColorList, Color, StarRating, and Star components that we created in Chapter 6 are examples of presentational components. They receive data via props, and when events occur, they invoke callback function properties. We are already pretty familiar with presentation components, so let’s see how we can use them to create container components.

The App component will mostly remain the same. It still defines the store in the context so that it can be retrieved by child components. Instead of rendering the SortMenu, AddColorForm, and ColorList components, however, it will render containers for those items. The Menu container will connect the SortMenu, NewColor will connect the AddColorForm, and Colors will connect the ColorList:

render() {
    return (
        <div className="app">
            <Menu />
            <NewColor />
            <Colors />
        </div>
    )
}

Any time you want to connect a presentational component to some data, you can wrap that component in a container that controls the properties and connects them to data. The NewColor container, Menu container, and Colors container can all be defined in the same file:

import { PropTypes } from 'react'
import AddColorForm from './ui/AddColorForm'
import SortMenu from './ui/SortMenu'
import ColorList from './ui/ColorList'
import { addColor, 
         sortColors, 
         rateColor, 
         removeColor } from '../actions'
import { sortFunction } from '../lib/array-helpers'

export const NewColor = (props, { store }) =>
    <AddColorForm onNewColor={(title, color) =>
                      store.dispatch(addColor(title,color))
                  } />

NewColor.contextTypes = {
    store: PropTypes.object
}

export const Menu = (props, { store }) =>
    <SortMenu sort={store.getState().sort}
              onSelect={sortBy =>
                  store.dispatch(sortColors(sortBy))
              } />

Menu.contextTypes = {
    store: PropTypes.object
}

export const Colors = (props, { store }) => {
    const { colors, sort } = store.getState()
    const sortedColors = [...colors].sort(sortFunction(sort))
    return (
        <ColorList colors={sortedColors}
                   onRemove={id =>
                       store.dispatch( removeColor(id) )
                   }
                   onRate={(id, rating) =>
                       store.dispatch( rateColor(id, rating) )
                   }/>
    )
}

Colors.contextTypes = {
    store: PropTypes.object
}

The NewColor container does not render UI. Instead, it renders the AddColorForm component and handles onNewColor events from this component. This container component retrieves the store from the context and uses it to dispatch ADD_COLOR actions. It contains the AddColorForm component and connects it to the Redux store.

The Menu container renders the SortMenu component. It passes the current sort property from the store’s state and dispatches sort actions when the user selects a different menu item.

The Colors container retrieves the store via context and renders a ColorList component with colors from the store’s current state. It also handles onRate and onRemove events invoked from the ColorList component. When these events occur, the Colors container dispatches the appropriate actions.

All of the Redux functionality is connected here in this file. Notice that all of the action creators are being imported and used in one place. This is the only file that invokes store.getState or store.dispatch.

This approach of separating UI components from containers that connect them to data is generally a good approach. However, this could be overkill for a small project, proof of concept, or prototype.

In the next section, we introduce a new library, React Redux. This library can be used to quickly add the Redux store to context and create container components.

The React Redux Provider

React Redux is a library that contains some tools to help ease the complexity involved with implicitly passing the store via context. This library is also brought to you by Dan Abramov, the creator of Redux. Redux does not require that you use this library. However, using React Redux reduces your code’s complexity and may help you build apps a bit faster.

In order to use React Redux, we must first install it. It can be installed via npm:

npm install react-redux --save

react-redux supplies us with a component that we can use to set up our store in the context, the provider. We can wrap any React element with the provider and that element’s children will have access to the store via context.

Instead of setting up the store as a context variable in the App component, we can keep the App component stateless:

import { Menu, NewColor, Colors } from './containers'

const App = () =>
    <div className="app">
        <Menu />
        <NewColor />
        <Colors />
    </div>

export default App

The provider adds the store to the context and updates the App component when actions have been dispatched. The provider expects a single child component:

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import storeFactory from './store'

const store = storeFactory()

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('react-container')
)

The provider requires that we pass the store as a property. It adds the store to the context so that it can be retrieved by any child of the App component. Simply using the provider can save us some time and simplify our code.

Once we’ve incorporated the provider, we can retrieve the store via context in child container components. However, React Redux provides us with another way to quickly create container components that work with the provider: the connect function.

React Redux connect

If we keep our UI components purely presentational, we can rely on React Redux to create the container components. React Redux helps us create container components through mapping the current state of the Redux store to the properties of a presentational component. It also maps the store’s dispatch function to callback properties. This is all accomplished through a higher-order function called connect.

Let’s create the Colors container component using connect. The Colors container connects the ColorList component to the store:

import ColorList from './ColorList'

const mapStateToProps = state =>
    ({
        colors: [...state.colors].sort(sortFunction(state.sort))
    })

const mapDispatchToProps = dispatch =>
    ({
        onRemove(id) {
            dispatch(removeColor(id))
        },
        onRate(id, rating) {
            dispatch(rateColor(id, rating))
        }
    })

export const Colors = connect(
    mapStateToProps,
    mapDispatchToProps
)(ColorList)

connect is a higher-order function that returns a function that returns a component. No, that’s not a typo or a tongue-twister: it’s functional JavaScript. connect expects two arguments: mapStateToProps and mapDispatchToProps. Both are functions. It returns a function that expects a presentational component, and wraps it with a container that sends it data via props.

The first function, mapStateToProps, injects state as an argument and returns an object that will be mapped to props. We set the colors property of the ColorList component to an array of sorted colors from state.

The second function, mapDispatchToProps, injects the store’s dispatch function as an argument that can be used when the ColorList component invokes callback function properties. When the ColorList raises onRate or onRemove events, data about the color to rate or remove is obtained and dispatched.

connect works in conjunction with the provider. The provider adds the store to the context and connect creates components that retrieve the store. When using connect, you do not have to worry about context.

All of our containers can be created using the React Redux connect function in a single file:

import { connect } from 'react-redux'
import AddColorForm from './ui/AddColorForm'
import SortMenu from './ui/SortMenu'
import ColorList from './ui/ColorList'
import { addColor, 
         sortColors, 
         rateColor, 
         removeColor } from '../actions'
import { sortFunction } from '../lib/array-helpers'

export const NewColor = connect(
    null,
    dispatch =>
        ({
            onNewColor(title, color) {
                dispatch(addColor(title,color))
            }
        })
)(AddColorForm)

export const Menu = connect(
    state =>
        ({
            sort: state.sort
        }),
    dispatch =>
        ({
            onSelect(sortBy) {
                dispatch(sortColors(sortBy))
            }
        })
)(SortMenu)

export const Colors = connect(
    state =>
        ({
            colors: [...state.colors].sort(sortFunction(state.sort))
        }),
    dispatch =>
        ({
            onRemove(id) {
                dispatch(removeColor(id))
            },
            onRate(id, rating) {
                dispatch(rateColor(id, rating))
            }
        })
)(ColorList)

In this example, each of our containers are defined using React Redux’s connect function. The connect function connects Redux to purely presentational components. The first argument is a function that maps state variables to properties. The second argument is a function that dispatches actions when events are raised. If you only want to map callback function properties to dispatch you can provide null as a placeholder for the first argument, as we have in the definition of the NewColor container.

In this chapter, we looked at various ways to connect Redux to React. We explicitly passed the store down the component tree to children as a property. We implicitly passed the store directly to the components that need to use it via context. We decoupled the store’s functionality from our presentation through the use of container components. And finally, we used react-redux to help us rapidly connect the store to presentation using context and container components.

At this point we have a working app that uses React and Redux together. In the next chapter, we will look at how we can write unit tests for all of the various parts of this application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset