React application state management with Redux

by
Tags: , , , ,
Category:

Overview

Recently, I put together a short 3-hour tutorial on using React
with the Redux state management API. I chose to write a simple D&D style game
using simple icons and a backing sound track, complete with (very
rudimentary) non-player characters. All of this was in service of making
the seminar fun, and giving the students something to hack on.

After the talk, I got to talking with one of the attendees, and our
discussions made the codebase even better (it’s great when you can learn
from your attendees and make what you built even better), and I spent a
few days really making the project more simple and dirct.

This post is the beginning of a series that delves into the Redux world,
using that codebase to illustrate some techniques and challenges. Here,
we’ll start first with a very simple introduction into React state
management, then discuss how Redux can help streamline state management
for your application.

Later posts will delve even deeper into Redux. I hope that we come out
the other side with a good set of techniques and helpful signposts to
aid your learning along the way.

The repository, if you’re interested in looking ahead, is at
github.com/krimple/fooberry.

React and State Management

React projects break data up into two categories:

  • state: read-write data that lives within a component
  • props: read-only data sent to a component, may be updated later by
    the sending component (a parent)

Anyone who has spent significant time in a React application realizes
that, as the component graph gets bigger, the connections between the
deeper components in the tree and their parents can become a chore to
manage.

Component connections like this become common:

passing props, the hard way through other components.

What about a design that involves containers for each task for layout
purposes?

passing props, the hard way through other components.

Of course, using something like { this.props.children } within the
container to embed the child can help reduce the over-passing of
properties in decorative components like cards and panels:

// in TaskList.js
render() {
  const cards = this.props.tasks.map(t => {
   return (
     <Card>
       <Task key={t.id} task={t} />              <1>
     </Card>
   );
}
// in Card.js
render() {
  <div className="card">
    { this.props.children }                            <2>
  </div>
}
  1. A nested component in your template
  2. Emits all nested child components (in our case, the Task)

Event Propagation: a cross-cutting challenge

Consider a functional component at the bottom of a component graph, one
that needs to signal a logout event or message to the application.

Typically the component that needs to receive the message / event will
pass one of its functions to handle the event downward to the child
component that needs it:

// in top component
handleLogout => () {
  // log out!
}
render() {
  ...
  <MainPanel onLogout={ this.handleLogout } />
  ...
}
// MainPanel
render() {
  <ButtonBar onLogout={ this.props.onLogout } />
}
// ButtonBar
<Button onClick={ this.props.onLogout }>Logout</Button>

These types of events may require cleanup in more than just a single
parent component. Perhaps the accessible features delivered in a menu
need to change, or the main view needs to clean up and remove any
secured data until the next time the user logs in.

These concerns, then, are cross-cutting. They affect more than one
component, and if determined to be global, will have to be factored into
a bi-directional data and event binding model that can become seriously
complex very quickly.

When you end up sending props from parent to child in a deeper hierarchy
or sending events from child to parents, it becomes difficult to
refactor your code. Any intermediate component that merely passes the
prop needs to lose it if the prop is sent down another path, and it
becomes difficult to quickly move a component from one tree to another.

Applications with prop-passing also attempt to limit the amount of data
in any one level by storing the state at the component where it makes
the most topological sense, not just at the root.

But that strategy breaks down for the cross-cutting concerns.

Tip

In storing lots of small state data in a smattering of places, you
lose control over the state in the large, and you have trouble sharing
that state content to siblings or other non-tree levels.

When you’re considering building an application with dozens or hundreds
of individual components and use cases, you need a way to manage state
in a more organized way. Enter Redux.

Redux – state management for Javascript

Redux is a simple state management engine for Javascript. It is not tied
directly to React or any other view technology; however, because it is
single-purpose and extensible, it is a perfect companion for React
projects.

Note

Some AngularJS, Angular and Ember developers also use Redux or a
similar project as a way to manage shared application state.

Redux has a simple API, composed of a State Store, Actions, Action
Creators, and Reducers:

  • An action is an event processed by Redux. It has a type, and a
    payload of request data.
  • An 'action creator` is a function that creates the data for an
    action. It assembles the action with a type and with the
    appropriate payload so that the caller of the API doesn’t need to
    know the internal action structure.
  • A reducer is a function that takes two parameters, the existing
    state
    and an action request. The reducer function uses the action
    type and payload to determine what work to perform. Ultimately the
    reducer either returns the existing state unchanged, or a new state
    based on activities requested for that action type.
  • Actions are created by action creators and dispatched by the
    state store.

Most importantly, this activity happens synchronously.

Data flow in Redux.

Click to enlarge…


To translate in words instead: an Action Creator creates an Action
object, which is then sent to the Redux Store’s dispatch function to
be processed by the Reducer(s) installed in the system. Optional
middleware sits between the dispatching process and the reducer and
can modify the behavior before or after (or in replacement of) the
dispatch of the request to the store.

Tip

Middleware APIs deal with incoming requests or outgoing results, and
in aspect-oriented programming terms provide a sort of around
advice
. Some common middlewares: Redux
Logger
, Redux
Thunk
, and Redux
Saga
are common middleware
APIs.

An example State Store

Let’s review how we could use Redux to manage the state of a simple
mathematical calculator. Although a trivial example, it shows all the
working pieces needed.

Note: we’re going to use the Ducks Design
Pattern
to organize our
reducer. This pattern places the actions, action creators and reducer in
a single file for clarity. We’ll also use ES2015+ syntax so you can see
how the latest spread operators make managing the state quite easy.

We’ll start with the actions:

In src/redux/calcReducer.js:

const ADD = 'calculator/redux/calc/ADD'; <1>
const SUBTRACT = 'calculator/redux/calc/SUBTRACT';                             <1>
  1. Actions should contain the value of the app or module, followed by
    the term 'redux', followed by the reducer subject area, followed by
    the uppercase constant of the action variable.

Next we have the actual reducer function. This function is run every
time a caller dispatches an action request:

function reducer(state = 0, action) {      <1>
  switch(action.type) {
  case ADD:                                                                <2>
    return action.a + action.b;
  case SUBTRACT:                                                           <2>
    return action.a - action.b;
  default:                                                                 <3>
    return state;
}
  1. Reducers are functions that receive the current state and action
    request
    and return a replacement state or the same state
  2. Each of our actions keys off of an action type – this property is
    required in all actions
  3. Each reducer should have a default response which returns the
    existing state. The Redux engine sends various probe operations to
    the state store, and also if you end up setting up multiple reducers
    to each manage a portion of a store, your reducer may receive
    requests it doesn’t need to act on

Reducers must return a brand new state reference each time they change
something. In the case above, we’re only using a single value, so we
replace it. But if we wanted to hold a calculation history, we could do
that too:

Reducers take an action and existing state, and make a new state (or
not).

function reducer(state = { history: [], answer: 0 }, action) {
  switch(action.type) {
  case ADD:
    let answer = action.a + action.b;
    let logEntry = action.a + ' + ' + action.b + ' = ' answer;
    return {
      answer: action.a + action.b,
      history: [ ...history, logEntry ]
    };
  case SUBTRACT:
    let answer = action.a + action.b;
    let logEntry = action.a + ' + ' + action.b + ' = ' answer;
    return {
      answer: action.a + action.b,
      history: [ ...history, logEntry ]
    };
  default:
    return state;
}

This could be simplified further. The main thing to focus on is the fact
that each time, we return a new state object. If the operation wasn’t
one we care about, we ignore it and return the existing state.

Finally, the action creators:

Action creators simply create the payload for an action. They can
validate parameters and serve as pre-storage business logic functions
too.

export function add(a, b) {
  return {
    type: ADD,
    a: a,
    b: b
  };
}
export function subtract(a, b) {
  return {
    type: SUBTRACT,
    a: a,
    b: b
  };
}

These action creators are, as you can see, simple functions that return
the appropriate payload.

Testing A Reducer

Since our reducer is a simple Javascript function, we can use any
testing engine to test it. A simple test using Jasmine might look like:

Testing a Reducer with Jasmine.

import reducer, { add, subtract } from './calcReducer';
describe('Calculator Spec', () => {
  it('should add', () => {
    const newState = reducer(0, add(5, 2));              <1>
    expect(newState).toBe(7);                            <2>
  });
  it('should subtract', () => {
    const newState = reducer(0, subtract(5, 3));         <1>
    expect(newState).toBe(2);                            <2>
  });
});
  1. Reducers are pure functions. They have no side effects. Therefore
    you can call them directly. We import the reducer directly here in
    our test.
  2. Call the reducer with the current state (assume 0 for the current
    calculator answer) and the action. This will return the new state.
  3. Verify that the new state values are correct for the given inputs.

Reducers are simple. Action Creators can be Complex.

That’s all there is to it. Reducers simply boil down dispatches of
action requests to new states.

Most of the complexity of a Redux store is in fetching or validating the
state change requests. This should not be done within the reducers,
which only update the data of the store, but in the action creators.
Action creators are also where asynchronous work happens before or
after updating the store.

Subscribe to a store to get updated state

There are two query APIs – getState() and subscribe(). getState is
meant to be used when you imperatively have to know, right now, what the
data is in the store. subscribe lets you be notified whenever the
state changes.

The key to decoupling your React component tree from the data itself is
to strategically dispatch action requests and bind subscriptions to
pieces of the tree where needed, letting Redux own the data and the
components own the user interface.

You could integrate Redux with your components by hand, as in something
like this:

Integrating Redux by hand. Don’t do this.

import store from './my-state-store';
export class LogDisplayer extends Component {
  constructor(props) {
    store.subscribe((tree) => {
      this.setState({
        logMessages: tree.logmessages
      });
    });
  }
...
}

The problem: now you have state both in the reducer, and a shadow copy of the state
in the component. As long as you are strict about not modifying the state inside the
component, you could live with this, but it’s not ideal.

You could subscribe to the store in a parent component, feeding the data
back into a child Task Manager via props, but isn’t that seeming rather
familiar?

You could even get funky and use the Context object, which lets you
share data context between components, but that’s supposed to be a
non-final API and subject to change. (Actually, this IS the API that
some of our integration libraries use, but you never touch it directly).

What’s a budding Reactive Reduxian to do?

Enter React-Redux, a Redux integration library

The react-redux library
is dubbed the official bindings API for Redux for the React API. It
consists of three APIs:

  • The <Provider /> container component, which wraps Redux into
    your component tree and makes the next feature available to all
    child components
  • connect – A function with a terrible name (should be
    connectComponentToRedux or something like that) that wraps your
    component with a subscription to Redux, and updates read-only
    props in your component when anything changes. It also adds a prop
    named dispatch that can be used to dispatch requests to the state
    store
  • mapStateToProps – a function that you can call from connect to
    perform the mapping from redux state to your react props.
  • mapDispatchToProps – a function that can wire action creators to
    the dispatch method automatically, making them appear as normal
    member methods on the props object. Note, this is only
    really used when you want to pass a function to a sub-component but don’t want it to know
    anything about Redux. We show the syntax, but since the dispatch method is
    passed to the component when it is connected, don’t bother with this mapping function
    when connecting single component to Redux.

A Reactive Reduxian Calculator

Now our calculator can use the simplest form of integration with the
react-redux library, and bind data from the store to the properties it
needs to display and edit.

The top-level component for our Calculator app, wired to Redux with
React-Redux.

// Our App.js starting component
import React from 'react';
import { Provide } from 'react-redux';                            <1>
import store from './my-state-store';
import Calculator from './src/Calculator.js';
export default const App => () {
  return(
    <Provider store={store}>                                      <2>
      <Calculator />
    </Provider>
  )
}
  1. The react-redux library provides this wrapper component, which
    stores the state in the React Context object.
  2. We surround our outermost component with the Provider component,
    passing it our created Redux store. Now any component inside of this
    hierarchy can access the store.
  3. Our Calculator component now has access to the Redux store.

The Calculator itself, wired now with Redux via the React-Redux
integration library.

// The Calculator.js component
import React, { Component } from 'react';                                 <1>
import { connect } from 'react-redux';
import { add, subtract } from './calcReducer';
class Calculator extends Component {
  render() {
    return (
      <input value={ this.state.a } onChange={ this.setA }><br/>          <2>
      <input value={ this.state.b } onChange={ this.setB }><br/>          <2>
      <div> Result { this.state.answer } </div>
      <h3>Actions</h3>
      <div>
        <button onClick={ this.add }> + </button>                         <3>
        <button onClick={ this.subtract }> - </button>                    <3>
      </div>
    );
  }
  // handle events
  this.setA = (event) => { this.state.a = event.target.value; };          <4>
  this.setB = (event) => { this.state.b = event.target.value; };          <4>
  this.add = () => {                                                      <5>
    this.dispatch(add(this.state.a, this.state.b));
  };
  this.subtract = () => {                                                 <5>
    this.dispatch(subtract(this.state.a, this.state.b));
  };
}
// here's where we map the state from Redux to our props
function mapStateToProps(state) {                                         <6>
  return {
     a: state.a || 0,
     b: state.b || 0,
     answer: state.answer || 0
  };
}
// the way to connect a component to redux is
// to return its connected proxy
export default connect(mapStateToProps)(Calculator);                      <7>
  1. Components within the Provider will now use the connect function
    to subsribe themselves into the Redux store.
  2. Expose controlled inputs which are fed by the Redux state received
    by the component
  3. Expose method calls that dispatch requests to Redux to add or
    subtract
  4. Provide methods to ingest changes to bound state for the operands
  5. Execute the dispatch method (provided by the connect function
    below, which dispatches an action request to Redux
  6. Provide a Redux State to React Props mapping function. This
    is executed every time a state change is made in redux, so it
    should be a very simple and quick function. In our case, we just
    receive the updated state for a, b, and answer and bind them
    to props with the same name. The dispatch function is bound to
    props for free with this function.
  7. Finally, we call connect, which takes our Redux state mapping
    function, and returns a function that can wrap our React class
    with the proper Redux proxy. This is then called with our class to
    provide a Redux-connected React component.

Tip

mapStateToProps is poorly named. So is connect. Whoever came up
with these too-generic names should probably fix that someday.
Remember it’s map redux state to react props, and connect react
component
to redux store. If it helps you, write the mapping
function as mapReduxStateToReactProps. Since connect is a named
import, you’d have to go out of your way to rename it, so we suggest
you don’t.

Wrap-up

This sample is a trivial one, so don’t infer that you want to send all
of your Redux state data into a single component, manage all application
state in Redux, or that there isn’t more to the mapping logic. In the
next few blog posts, we’ll explore how to improve the request
dispatching process, manage asynchronous updates, how to manage complex
state effectively, and how to test your reducers.

The Plug – React and Redux Training

Since you've read this far, and hopefully you found the article useful, consider
attending our React and Friends training course.
Offered onsite at your location, for your employees, we take you from the basics
of building a React application through calling RESTful APIs, dealing with forms,
handling state in props/state and Redux, and learning how to use the React Router.