Better Redux module management

Redux is fantastic. It provides a single, global state container for JavaScript apps. The elegance of a single state tree manipulated by focused, straightforward functions is simple and powerful. But we've noticed that as some of our projects grow in size and complexity, our setup becomes increasingly complex. We've developed some helpers that make this process quick and easy.

In this post, we're looking at spots in code that are common places for error as well as looking at where there is unnecessary redundancy in our code to try to save ourselves from over-typing.

A default Redux setup has lots of opportunities for error. Before long, code to handle the state tree piles up into one gigantic switch statement, constant exports are littered everywhere (don't misspell one of them, yikes), and the ever-growing file structure becomes unwieldy.

Conventions, such as ducks-modular-redux, offer configuration around the madness and have become pretty popular as a method for grappling with these mental complexities.

While using Redux over the past year, we've built tooling that makes our redux implementations sane and easy to work with. We're open-sourcing this tooling. We call it redux-module-builder. While it shares a lot of goals with redux packages, such as ducks-modular-redux, it offers a collection of helpers to make the process easier, reduce boilerplate, and speed-up developer-time, where these other conventions don't help out.

In addition, some actions require us to communicate with a back-end server. This being a common requirement, redux-module-builder package has some built-in mechanisms for dealing with APIs.

TL;DR

If you're familiar with Redux, the best way to understand the benefits of setting up your store in a modular way with redux-module-builder is to take a look at some of the same redux module code.

The following snippet demonstrates all the logic around signing a user in and out of an example application. It's easy to understand exactly what is going on:


import {createConstants, createReducer} from 'redux-module-builder';
import {createApiAction, createApiHandler} from 'redux-module-builder/api';

/**
 * Create some custom constants
 **/
export const types = createConstants('currentUser')(
  {'SIGN_IN': { api: true }},
  'SIGNED_OUT'
);

/**
 * Create our actions, one action which defines an api
 * and one pure, non-api action
 **/
export const actions = {
  signIn: (email, password) => createApiAction(types.SIGN_IN)(
    (client, opts) => client.post({
      path: '/user/login',
      params: {email, password}
    })),
  userSignedOut: msg => ({type: types.SIGNED_OUT, payload: msg})
};

/**
 * Create a reducer to handle the SIGN_IN api action and one
 * non-api action
 **/
export const reducer = createReducer({
  ...createApiHandler(types.SIGN_IN)((state, {payload}) => {
    ...state,
    user: payload
  }),
  [types.SIGNED_OUT]: (state, {payload}) => ({
    ...state,
    user: null
  })
});

// Define the initial state for `currentUser` module
export const initialState = {
  user: null
};

Redux convention traditionally encourages separating your Redux app across a file structure like this:

app/
  constants.js
  reducers.js
  actionCreators.js
  ...etc

In the traditional Redux code structure, we're organizing our code by type, rather than by functionality. Using the Redux module pattern, on the other hand, we're organizing code around the management of a particular component of the state tree.

With all our actions/reducers/types pertaining to a single part of the state tree housed in a single, easy-to-follow file, the functionality of our code and our intentions are clear and concise.

Thinking in Redux

This post assumes familiarity with Redux. If you aren't familiar with Redux now, there are plenty of fantastic "getting started with Redux" tutorials out there that we list in the appendix of this post, so check that out for more details about the basics of Redux.

For a deep-dive into Redux, our book at fullstackreact.com details this process in vanilla Redux. Starting from installation through production-ready, it covers Redux for the beginner and the professional alike.

Let's take a minute to think about what Redux offers and how to think about using Redux in practice.

When building an app using Redux, we like to think of it as a system for managing the flow of data. When thinking about how to implement data at rest and data in transit (through our app), we'll break up our single state tree into small parts (branches) based upon their responsibilities within the app. For instance, when building a blog, we might have a state tree with three separate branches: authors, posts, and comments.

Mentally, when thinking about the data at rest in our system (by data at rest, we mean after we fetch data and we have "stored" it locally) it's easy to break it up into logical, functional pieces.

We can store posts in a posts list, the comments in a comment object, users in a user dictionary, and so forth. However, in transit this division of data is not as simple.

This data separation offers a pretty straightforward way to connect pieces of the state-tree to components that care about them and (more importantly) ignore those pieces they don't care about.

These issues grow in complexity when working in a big team. Where do you store the actions for fetching new posts?

Using Redux with React (or other view libraries), we'll bind these branches to different components and fire actions that describe what happened. In Redux, reducers determine how the state should be updated after an action is fired.

While actions and reducers have a direct relationship, there is no clear convention in Redux about how to "bundle" the two together. The community will typically put action constants in one file, action creators in another, and reducers in yet another. This adds mental overhead to tracing actions as they flow through the system.

We like to build our systems using a modular structure where we group like-functionality and data together. In this model, every part of the Redux pipeline that works with a discrete part of the state tree lives inside one file. Thanks to Redux, this is easy.

In addition, using redux modules can offer interesting opportunities to add extra functionality.

Introducing redux-modules

redux-module-builder offers an approach to bundling reducers, actions, and constants together in a module. This module centralizes the management of a part of the data model into a single, easy-to-follow file.

redux-module-builder is written in component pieces so as to reduce the amount of integration with the entire project library into tiny functional parts. Every API we talk about with the exception of the api actions/reducers can be used independently of the rest of the stack.

Like one part, but not another? Use that one piece instead of the full stack. Like it all (like we do)? Use it all.

Let's get started using redux-module-builder to see how much it can improve and speed-up your redux workflow. redux-modules is available on npm as redux-module-builder. We can install it using npm in a project directory.

Once we have the redux-module-builder package installed, we will import different helpers from the package.

Let's skip right to the code before we even worry about setting it up (clone the example repo at https://github.com/fullstackreact/redux-module-builder-example to follow along).

A Twitter stream app

Let's walk through the features of redux-module-builder by building a relatively complex app to track events in realtime using the custom backend at https://github.com/fullstackio/realtime-news/ to serve a Twitter stream API which serves as a proxy for calling Twitter methods and handling Twitter streams.

We have a production version of the backend running at https://realtime.fullstackweb.org/ that you should feel free to use to complete this post. But we urge you to stand up your own if you want to keep your service running. We offer no QoS (quality of service) guarantees of the server being kept up.

In our app, we'll create a few redux modules, each with their own responsibility:

name file path description
users src/redux/modules/users.js responsible for keeping track of the current user
events src/redux/modules/event.js responsible for getting upcoming events
images src/redux/modules/images.js responsible for getting images associated with an event
currentEvent src/redux/modules/currentEvent.js responsible for connecting to a websocket and live updating an event's data

Building a redux module

The redux-module-builder provides a lot of convenient helpers to build a redux module.

We'll look through several different helpers throughout the process of building our redux modules. The README.md file of the repo contains the full, up-to-date API.

As we previously said, redux-module-builder is built in several different parts so that integration into an existing app is easy. Since it's written in multiple distinct parts, we can use only specific parts we like and not use the parts we don't. Have your own api client/middleware, but want to use the convenient createConstants() function? Use it!

In addition, the redux-module-builder is well-tested and in use in production today.

To get started quickly looking through building a redux module, let's clone our example project from github:

git clone https://github.com/fullstackreact/redux-module-builder-example.git

Let's also install it's dependencies using npm as well:

cd redux-module-builder-example
npm install

Configuring the Application

This app uses dotenv for configuration. In order to configure the application for your own api access, grab an api token from Twitter here and set it in a file called .env at the root for a key called __TWITTER_KEY__. For instance, say that your twitter key is: abc123. Your .env file should look like:

APP_NAME=liveStream
API_HOST=realtime.fullstackweb.org
API_URL=https://realtime.fullstackweb.org
WS_URL=wss://realtime.fullstackweb.org
TWITTER_KEY=abc123

Our twitter key (for our public back-end) is 9MgEB0T4vtNjcJYcGPPVyCToU. We offer zero quality of service on our back-end and it's possible it could go down at any time. Please boot your own back-end server if you want it to run in production. We've included a section on setting up your own back-end.

Awesome. All of the redux modules are located in the src/redux/modules directory of the app. We'll spend most of our time in this directory.

createConstants()

The Redux pattern does a fantastic job of helping us discern different actions based upon constant action types.

In the simplest redux setup, we'll usually export individual constants that may or may not be unique in an application from different files across the stack. For instance:

// In src/redux/actionTypes.js, we might have
export const ADD_TODO = 'ADD_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
// and we'll import these later to use them in another file, like:
import { ADD_TODO, REMOVE_TODO } from './actionTypes'

Not only does this get verbose and tedious, but it's error-prone and not necessarily globally unique, especially when creating constants that are the names of common tasks. At its heart, the issue is quite simple: Actions are just string literals. Actions that describe events intended for one part of the state tree might accidentally affect other parts of the state tree if you re-use the same string literal. This node module helps you namespace to avoid these collisions by providing the function createConstants().

The createConstants() function accepts a single argument (either a string or a configuration object) that defines methods for creating an object that holds unique values for each type.

Let's see an example:

import { createConstants } from 'redux-module-builder'

export const types = createConstants('users')(
  'SIGNUP',
  'LOGIN',
  'LOGOUT'
)

The createConstants() function creates 3 keys on the types object referenced by their name, but points to a unique string for each as the value of the type object.

The createConstants() function returns an object which prepends the 'users' string to the constant name, yet retains the name of the constant by the value. That is, the types object from above actually looks like:

{
  "SIGNUP": "USERS_SIGNUP",
  "LOGIN": "USERS_LOGIN",
  "LOGOUT": "USERS_LOGOUT"
}

We use these types from the users module by calling it from the type methods. For instance, later in the same file, we can reference the SIGNUP type by types.SIGNUP, rather than USERS_SIGNUP. To use this in a different file, we can import it using the import-as syntax:

// In the same file, later:
dispatch({type: types.SIGNUP});
// In another file, we can import this file by the name
import { types as userTypes } from 'redux/modules/users'
// and use it by the variable name `userTypes`
dispatch({type: userTypes.SIGNUP});

Using the createConstants() function helps create a clean and consistent way of creating constants, while avoiding name collisions in events.

API constants

While we'll look at the API constants more in-depth in a few sections, let's take a peek at this nifty functionality now.

Let's say that we're working with an API. We know we're going to have requests to the API for logging-in and logging-out. In Redux, it's conventional to emit actions corresponding to your request's lifecycle: loading, success, and error actions.

With redux-module-builder, we can specify that a particular constant represents an API request like this:

import { createConstants } from 'redux-module-builder'

export const types = createConstants('users')(
  'SIGNUP',
  {'LOGIN': { api: true }},
  {'LOGOUT': { api: true }}
)

Now, the createConstants() function creates a triad of new constants based upon this root word. Our new types object looks like this:

{
  "SIGNUP": "USERS_SIGNUP",
  "LOGIN_LOADING": "API_USERS_LOGIN_LOADING",
  "LOGIN_SUCCESS": "API_USERS_LOGIN_SUCCESS",
  "LOGIN_ERROR": "API_USERS_LOGIN_ERROR",
  "LOGIN": "USERS_LOGIN",
  "LOGOUT_LOADING": "API_USERS_LOGOUT_LOADING",
  "LOGOUT_SUCCESS": "API_USERS_LOGOUT_SUCCESS",
  "LOGOUT_ERROR": "API_USERS_LOGOUT_ERROR",
  "LOGOUT": "USERS_LOGOUT"
}

Notice that it creates constants matching the three states of our API requests, *_LOADING, *_SUCCESS, and *_ERROR. createConstants() basically guarantees we'll never have a naming collision between different modules again.

We can create custom constant types as well. For instance, let's imagine we want to add a custom websockets-like type that dictates if a system is online. We can create a custom type by passing in a customTypes object into createConstants() and using it in our constant creation.

For instance, let's create a sockets custom type and maybe a cached custom type. We'll use these to create constants in our system:

export const types = createConstants({
  prefix: 'users',
  customTypes: {
    'sockets': ['online', 'offline'],
    'cached': ['cached', 'uncached']
  }
})(
  {'SIGNUP': { types: 'sockets' }},
  {'LOGIN': { types: ['sockets', 'cached'] }}
)

Because we set the customTypes property, we'll have a bunch of new constants we can use:

{
  LOGIN:"USERS_LOGIN",
  LOGIN_CACHED:"USERS_CACHED_LOGIN_CACHED",
  LOGIN_OFFLINE:"USERS_SOCKETS_LOGIN_OFFLINE",
  LOGIN_ONLINE:"USERS_SOCKETS_LOGIN_ONLINE",
  LOGIN_UNCACHED:"USERS_CACHED_LOGIN_UNCACHED",
  SIGNUP:"USERS_SIGNUP",
  SIGNUP_OFFLINE: "USERS_SOCKETS_SIGNUP_OFFLINE",
  SIGNUP_ONLINE: "USERS_SOCKETS_SIGNUP_ONLINE"
}

Our twitter stream app constants

Although we won't always know what constants we'll need when we start out an app, we have a few that we already know we want to create.

Let's say that we are creating an images module (in our sample app, this is located at src/redux/modules/images.js). Our images constants are pretty straightforward. We'll need one to fetch (execute the request) and one to receive and populate the Redux state images:


export const types = createConstants('images')(
  {'GET_IMAGES': { api: true }},
  'MEDIA_ARRIVED'
);

Continuing with our modules, we'll create an events module (at src/redux/modules/event.js) that's responsible for getting upcoming events along with images for the latest events. Both of these we will call out an API, so we'll create themm with our api modifier:


export const types = createConstants('events')(
  {'GET_UPCOMING': { api: true }},
  {'GET_IMAGES': { api: true }}
);

Finally, let's create a currentEvent module which will connect over websockets to the back-end, so our constants won't need to have api state (although, we could create a custom type with websocket status, as we looked at previously):


export const types = createConstants('currentEvent')(
  'TWEET_ARRIVED',
  'MEDIA_ARRIVED',

  'DISCONNECTED'
);

One of the nice things about this setup already is that it's very clear where the division of responsibility is to handle types for each different module of functionality of our app.

Actions

Our app is kind of useless without any actions or actionCreators. One of the nicer features of using Redux is that we can work with React as though our data is tied to the UI, rather than mucking with the data container directly.

We'll use actions to dispatch events with a type that we can set up reducers to listen for events. Actions are simple objects with a type and an optional payload. In our Redux modules, we can export an actions object (with the key being the name of the action and the value being the object or function that returns an object) to compose the possible actions exposed by the module.

One of the features we get by setting up our app in the way that we have is that we've moved all of the responsibilities for creating these actions into their own individual "modules." The actions in a redux module are just objects.

export const actions = {
  doSomething: () => ({type: types.SOMETHING}),
  runSomething: (msg) => ({type: types.RUN, payload: msg})
}

For instance, let's start out by creating our users actions. Since we'll be using twitter to authenticate and make requests against, let's incorporate a signin process using our realtime-news repo app using the fantastic hello library.

We've included a how to set up hello.js section to set up the realtime-news server, so head over there if you need to set it up yourself, or continue along with us first.

In our src/redux/modules/user.js file (in the sample repo), let's create a loginWithTwitter() action in our users module so we can call it from an action in our view.

Since we're not using a Twitter login RESTful API with our loginWithTwitter() action in our users redux module actions export, we can set it as a simple function without any redux-module-builder helpers.

Let's make sure we have a few types in our users module:


export const types = createConstants('users')(
  'LOGIN',
  'LOGGED_IN',
  'ERROR_LOGGING_IN'
);

Now, we can use these types in calling the hello("twitter").login() function in an actions object. For instance:


export const actions = {
  loginWithTwitter: () => (dispatch, getState) => {
    hello('twitter').login()
      .then(res => {
        dispatch({type: types.LOGGED_IN, payload: res});
      }, err => {
        dispatch({type: types.ERROR_LOGGING_IN, payload: res});
      })
  }
}

With a login button, we can call this loginWithTwitter() action using the actions prop we'll pass down through the app.

Let's set up a login button. We can place the <Login /> component in our root view. Since this component is straightforward and pretty simple, we'll use a functional stateless component (aka a component that does not require a class/this object).

Inside the file src/views/main/Login/Login.js, let's store the <Login /> component like so:


import React from 'react';
import styles from './login.module.css'
import classnames from 'classnames'

export const Login = (props) => {
  const authorizeTwitter =
    () => props.actions.users.loginWithTwitter()

  return (
    <div className='ui middle aligned center aligned grid'>
      <div className={classnames(['column', styles.loginColumn])}>

          <div className="ui message">
            <p>This app is a demo for the blog post <a href="#">Better Redux with Redux Modules</a></p>

            <p>When you authenticate with Twitter below we will use the token to
            show you a demo app which shows a live stream of tweets.</p>
          </div>

          <form className="ui large form">
            <div className="ui stacked segment">

              <div className="ui fluid large pink submit button"
                   onClick={authorizeTwitter}
              >Login with Twitter</div>
            </div>
          </form>
      </div>
    </div>
  )
}

export default Login;

When our user clicks the button, the hello library will be called by the users.loginWithTwitter() action.

The redux-module-builder provides some helpers for dealing with API calls that deal with promises. In fact, it provides an API client, complete with request/response transformations and error handling.

Let's create an action to see how this works. We'll want to make a request to get our upcoming events from our backend.

Let's move to our src/redux/modules/events.js module and create an action that we'll call getUpcomingEvents().

The redux-module-builder provides a function called createApiAction() which will create an action that works with the apiMiddleware we set up in our configureStore() function.

The createApiAction() method takes two arguments, the first one being the action type to call. For instance, in our getUpcomingEvents() function, we'll use it to handle out GET_UPCOMING event:

export const types = createConstants('events')(
  {'GET_UPCOMING': { api: true }},
);

export const actions = {
  getUpcomingEvents: createApiAction(types.GET_UPCOMING)((client, opts) => {
    // This function is run when the `getUpcomingEvents()`
    // action is called
  })
}

The createApiAction() function returns a function that is called when the action itself is called with an instance of the apiClient, which is a simple proxy class that sits above the fetch function.

This function is for convenience purposes, but does not have to be used. The only requirement is that a promise is returned, so we can use any API client here if you prefer to use another one. The apiClient instance passed in is fairly feature complete, so we'll use it to make our api call:

export const types = createConstants('events')(
  {'GET_UPCOMING': { api: true }},
);

export const actions = {
  getUpcomingEvents: createApiAction(types.GET_UPCOMING)((client, opts) => {
    return client.get({
      path: '/events/upcoming'
    });
  })
}

More documentation on the apiClient itself can be found in the README of the redux-modules source.

We get access to all of the options our method is called with in the opts (second argument the function is executed with), which includes the apiMiddleware options. For instance, if we want to call the getUpcomingEvents() function with a start and end date, we might refactor the action function like so:

export const types = createConstants('events')(
  {'GET_UPCOMING': { api: true }},
);

export const actions = {
  // calling our action elsewhere like so:
  // getUpcomingEvents({startAt: '2016-01-01'})
  getUpcomingEvents: createApiAction(types.GET_UPCOMING)((client, opts) => {
    const startAt = opts.startAt || moment().subtract(1, 'year');
    const endAt = opts.endAt || moment().add(1, 'year');
    return client.get({
      path: '/events/upcoming',
      params: { startAt, endAt }
    });
  })
}

If we want to modify the return value of the api request after it has been received, we can create a chain and return the value of the promise chain as the return value of the function. For instance, our events api returns JSON with the key of events. Since we don't need to use any other information returned from the api request, we can modify the previous action to only return the events:


export const actions = {
  getUpcomingEvents: createApiAction(types.GET_UPCOMING)((client, opts) => {
    const {count} = opts;
    return client.get({
      path: '/events/upcoming',
      params: {count},
    }).then(res => res.events);
  })
};

While we are at it, let's add the api action to fetch images from our server. We'll use a straight-up api action, similar to what we did with the events module. Instead of just returning back the JSON response values, however, we'll modify the return value so that we collect only tweets with media attached and return back only the images:


export const actions = {
  // Create an API action that fetches images contained in tweets
  getLatestImages: createApiAction(types.GET_IMAGES)((client, opts) => {
    const {event} = opts;
    return client.get({
      path: '/tweets/images'
    }).then(({tweets}) => {
      const {statuses} = tweets;
      const images = [].concat
        .apply([],
          statuses
            .map(t => t.entities.media)
            .filter(t => !!t));
      return images;
    })
  })
}

Finally, let's complete our last modules action with our currentEvent module which creates a websocket connection to our backend.

We'll create this action in our src/redux/modules/currentEvent.js module, which is responsible for handling any updates to the current event our user is interested in. This action is not an API action, so although the implementation looks complex, it's a simple JavaScript method to connect to a websocket backend.

Without going into too much detail, the action is:

let ws; // the websocket
const actions = {
  wsConnect: (evt) => (dispatch, getState) => {
    const {currentUser} = getState().users;
    ws = new WebSocket(__WS_URL__ + '/ts');
    // on a message, dispatch the `TWEET_ARRIVED` action
    ws.onmessage = (event) => {
      dispatch({
        type: types.TWEET_ARRIVED,
        payload: JSON.parse(event.data)
      })
    }

    ws.onopen = () => {
      ws.send(JSON.stringify({
        type: 'searchTag', tag: evt.hashtag,
        ...credentials
      }))
    }
  }
}

When our websocket connects to our backend, we'll send the hashtag of the event we're interested in and our backend will do the work to connect through the twitter streaming api. When a twitter stream flows in, the websocket will receive a message and we'll dispatch a message of TWEET_ARRIVED. We don't need to mess with any data or react view underneath. It's just like every other action.

Redux is only useful when we have handlers for actions emitted by the front-end. The redux-module-builder also provides helpers for building reducers.

Handling the actions

In redux, we'll need to handle some actions. The redux terminology for these handlers are called reducers. Reducers are generally very simple functions that either return a simple object or return a function (in the case of redux-thunk) that dispatch an action object.

createReducer()

redux-module-builder exports a function called createReducer, which is actually a very thin wrapper to check for the existence of a handler for a particular type. If a handler is found, the handler is executed and the return value is expected to be the new, updated state. If no handler is found for the action type, then the current state is passed through as the new state.

Pseudocode of this process might look similar to the following. Assume we have an actions object and a reducer created as we do below:

// for instance, dummy example with a `user` service
// that provides the `.login()` function to log a user
// in and returns a promise
const actions = {
  login: (username, password) => {
    return (dispatch) => {
      user.login(username, password)
        .then((user) => dispatch({
          type: types.LOGGED_IN,
          user: action.payload
        }));
    }
  }
};

const reducer = createReducer({
  [types.LOGGED_IN]: (state, action) => ({
    ...state,
    user: action.payload
  })
})

When the action types.LOGGED_IN is dispatched (which would actually be the action USERS_LOGGED_IN when using the createConstants() method), the key is matched to the function in the createReducer() function and the function is called with the return value being the new state.

If an action is dispatched with types.LOGGED_OUT, on the other hand, the reducer does not handle it. Instead, the original state is passed through as the new state.

The createReducer() function provides a simple, consistent manner to handle reducer operations. In addition, it allows us to create a function as a reducer rather than a gigantic switch statement. Using a function gives us the ability to decorate them with additional functionality.

Let's handle our previous users redux module's USERS_LOGGED_IN function. When the types.LOGGED_IN action is dispatched, we know that it's getting called with the new user. Our src/redux/modules/users.js action can simply store the current user in the state:


export const reducer = createReducer({
  [types.LOGGED_IN]: (state, {payload}) => {
    return {
      ...state,
      currentUser: payload.authResponse
    }
  }
});

Since these are functions, we can decorate them with other functions. In addition, we can use the fact that they are simple functions to create multiple handlers for our virtual constant types (such as the api types).

createApiHandler()

The redux-module-builder exports another helper called createApiHandler() which creates a reducer responsible for handling different statuses of an api request.

For instance, our src/redux/modules/images.js module needs to handle the success action for when the GET_IMAGES action is fired.

Let's create a reducer that handles the response value from our getLatestImages() action. This action will fire the types.GET_IMAGES_SUCCESS constant. Our reducer simply needs to respond to the success action and store the images in the new state:


export const reducer = createReducer({
  ...createApiHandler(types.GET_IMAGES)((state, {payload}) => {
    return {...state, images: payload};
  })
});

Notice that we're extending the object by adding several api handlers at once into the reducer using the ... spread syntax.

The createApiHandler() function accepts a type as the first argument, which serves as the base type.

For the cases where we want to handle the LOADING and ERROR state, we can either set those as an action in the same was as we did with any other reducer. Alternatively, we can pass a second argument into the function to handle these states.

For example, our events module provides another case where we are going to respond to an api action (i.e. using the createApiAction() function). Let's add the handling of the LOADING and ERROR states in this function.

We'll pass a second argument to the createApiHandler() function, which is a function that accepts a single argument of apiTypes, which contains the custom API types of:

  • apiTypes.LOADING
  • apiTypes.SUCCESS
  • apiTypes.ERROR

We can use these constants to define handlers for these different actions:


export const reducer = createReducer({
  ...createApiHandler(types.GET_UPCOMING, (apiTypes) => ({
      [apiTypes.LOADING]: (state) => ({...state, loading: true}),
      [apiTypes.ERROR]: (state, {payload}) => ({...state, loading: false, errors: payload})
  }))((state, {payload}) => {
    return {
      ...state,
      loading: false,
      errors: null,
      events: eventsMap(payload)
    }
  }),

});

Last, let's move to working with our src/redux/modules/currentEvent.js module which connects through the websocket connection. We'll need to set up a handler that handles storing the new tweets when they arrive back from the websocket server.

We'll also want to only track the latest 10 tweets, so we'll create a "stack" in an array to only hold on to the latest ones. Let's start out by extending the Array class to include a function we'll call shiftMax.

The shiftMax function will essentially be a property on the array that holds only a certain count of numbers on the array itself. The implementation is straightforward enough. Let's create the shiftMax function on the Array.prototype in a file at src/utils/array.js:


Object.defineProperty(Array.prototype, 'shiftMax', {
  configurable: false,
  enumerable: false,
  writable: false,
  value: function(value, max) {
    if (this.length >= max) {
      this.splice(this.length - 1, 1);
    }
    return this.unshift(value);
  }
})

This is just one method for handling a maximum value array. There are many other methods for dealing with arrays with a maximum length.

With the shiftMax function, this will allow us to shift a value on any array and ensure that it only keeps the latest max values. Back in our reducer, we can use this new function to store only so many tweets on the tweets array:

export const reducer = createReducer({
    [types.TWEET_ARRIVED]: (state, {payload}) => {
      const {tweets} = state
      tweets.shiftMax(payload, 10);
      return {...state, tweets}
  }
});

The only issue with this as it stands right now is that the Twitter stream can sometimes send out duplicates (even if it doesn't mean to). We can handle filtering duplicates on the server or on the client-side. For sake of demonstration, we'll handle it here in the client-side.

Essentially, we'll want to keep track of the tweet ids that we have already received and only shift the new ones into our tweets array. We can modify our previous reducer to handle keeping track of our new tweet handler:

export const reducer = createReducer({
    [types.TWEET_ARRIVED]: (state, {payload}) => {
    if (state.tweetIds.indexOf(payload.id) < 0) {
      const {tweets} = state
      tweets.shiftMax(payload, 10);
      const tweetIds = tweets.map(t => t.id);
      return {...state, tweets, tweetIds }
    } else {
      return state;
    }
  }
});

Now, sometimes we'll want to use another module's constants in one module to the next. redux-module-builder does nothing to prevent this from happening. In fact, it's pretty easy to handle.

In our currentEvent module, let's watch for any new images to come in from the GET_IMAGES_SUCCESS action dispatch. Let's pull over the images types from the images redux module by importing a glob of objects in our src/redux/modules/currentEvent.js file:


import * as images from './images'

With the images object, we can move down to the reducer and set a reducer to run when the images.types.GET_IMAGES_SUCCESS action is dispatched:

export const reducer = createReducer({
    [types.TWEET_ARRIVED]: (state, {payload}) => {
    if (state.tweetIds.indexOf(payload.id) < 0) {
      const {tweets} = state
      tweets.shiftMax(payload, 10);
      const tweetIds = tweets.map(t => t.id);
      return {...state, tweets, tweetIds }
    } else {
      return state;
    }
  }
});

That's it! Now our currentEvent module will also run when the GET_IMAGES_SUCCESS action is dispatched.

Now that we've created a few redux modules, let's use them to create our demo application. The rest of this post is dedicated to set-up and discussions of usage of the redux modules.

Configure and set-up

Let's create a project where we'll use the new package.

In order to get going quickly, we'll use the same structure we offered in a previous post about building react apps called cloning yelp. This way, we can skip talking about how/why we set up react in a particular way and jump right into discussing redux-module-builder.

If you prefer to use another boilerplate or set-up, feel free to jump to the next section. redux-module-builder is not dependent upon this app structure. We're just using it in this post to get an app started up and running quickly.

We'll use the handy yeoman generator to generate the same structure for our app that we used in the Yelp post. We've pushed this up to npmjs, so it's easy enough to install in the usual manner.

Let's run our generator we dive into detail about in the cloning yelp post in a development directory:


mkdir app && cd $_
npm install yo generator-react-gen

In the same directory, let's create the default stack from the Yelp post. We can ask yeoman to generate this structure:


yo react-gen

If you want to skip this set-up process, the generator can handle a large part of it for us. See the Adding in redux section note for using the generator to build it for us.

We can boot up our app by using the npm start package script bundled with the generator:

npm run start

The app setup

This section is optional, as the example repo does this setup for us already. The following content discusses the process of configuring the store and the rootReducer.

As we've already started walking through our example app, we'll need to configure our store to use the redux modules as well as set up the redux modules in the root reducer.

1. Configuring the store

Let's configure our store. Let's create a a configureStore.js file to handle the store configuration for us. We'll import this file when we boot the app to configure our store.

In the src/redux/configureStore.js file, let's export a single function we'll call that we can use to configure our store:

export const configureStore = () => {

}

We like to store our routing configuration in the state.

Since we'll make our router state be reflected in our store, we'll need to set up the router middleware. Rather than export a some constants (actions, the store, etc.), we'll export a function, and return these constants. We can call the function configureStore() and pass through some configuration options in our configureStore() call and use them here.

Let's have the userInitialState defined alongside the react router history type:

export const configureStore = ({
  historyType = browserHistory,
  userInitialState = {}}) => {
    // create and return the store in here
}

Let's go ahead and start implementing our configureStore() function.

apiMiddleware

As we have some actions that require us to communicate with a back-end server, we'll need to set up a middleware to deal with API requests. In order to set this up, we'll need add a middleware. Since redux-module-builder allows us to define a generalized, globally-available middleware, we'll need to set it up in our redux store.

Our npm module makes this easy to handle. Let's import the createApiMiddleware() function from our package redux-module-builder. While we're at it, let's also import the redux-thunk middleware along with the routerMiddleware from react-router-redux:


import { createApiMiddleware } from 'redux-module-builder/api';
import { routerMiddleware, syncHistoryWithStore } from 'react-router-redux';

The createApiMiddleware() function is really simple and handles accepting methods we'll use in redux-module-builder to execute our API requests. Since we're dependent upon the thunk middleware, we'll need to include it in our middleware stack somewhere after our createApiMiddleware() function.

We like to create an array of middlewares in our configureStore() function, so let's look at how to do that inside the configureStore() method:


    let middleware = [
      createApiMiddleware({
        baseUrl: __API_URL__,
        headers: {
          'X-Requested-By': 'liveStream client'
        },
        requestTransforms: [
          (getState, opts) => (req) => {
            const {users} = getState();
            const {currentUser} = users;
            if (currentUser) {
              req.headers['x-auth-token'] = currentUser.oauth_token;
              req.headers['x-auth-secret'] = currentUser.oauth_token_secret;
            }
            return req;
          }
        ]
      }),
      thunkMiddleware,
      routerMiddleware(historyType)
    ]

Our simple middleware array contains three middlewares (the routerMiddleware is required for using react-router-redux, which is middleware that handles keeping the routing in redux).

Using this middleware stack is pretty straightforward. We'll use the compose() function provided by redux.

The compose() function takes a list of functions and provides the argument of the last function as the input for the previous function (FILO -- first in, last out). Mathematically, this is equivalent to:

compose(f, h, g) = f(g(h(...args)))

The compose() function returns a function which accepts a single function as the final function to call. With redux, we'll call the return function with createStore(), the function provided by Redux to create a function that to call createStore().

This process sounds complex. In overly-verbose pseudocode, this looks like:

let g = applyMiddleware(middleware1, middleware2);
let h = compose(middlewareStack);
let f = createStoreWithMiddlewareFunction(createStore);
// compose(f, h, g) =
//   f(h(g(rootReducer, userInitialState)))
const store = f(rootReducer, userInitialState);

However, this can functionally be a bit easier to create without all the verbosity. Back in our src/redux/configureStore.js file, let's create this f(h(g())) function:


    let finalCreateStore;
    finalCreateStore = compose(
      applyMiddleware(...middleware),
      ...tools
    )(createStore);

The tools variable here is a simple array. We'll use it to put some extra tooling in our finalCreateStore(), such as building DevTools in our store.

We can use the resulting function to create the actual store using a rootReducer:


    // initialState is imported from the rootReducer file
    // as exports and the userInitialState is a parameter
    // passed into the configureStore function
    const store = finalCreateStore(
      rootReducer,
      Object.assign({}, initialState, userInitialState)
    );

That's it. We've created the store. Well... almost. We haven't actually created/imported the rootReducer, which serves as the top reducer for our store. Let's leave the configureStore() function for the time being and build up our rootReducer.

rootReducer

Let's create (or open) the src/redux/rootReducer.js file. In here, we'll create a few exports so we can consume them in our configureStore() function.

In most redux projects, rootReducer.js contains the root reducer, generated by combineReducers(). The root reducer dictates which parts of the state tree are managed by which reducer functions.

Let's bring in the combineReducers() function first:


import { combineReducers } from 'redux';

Basically, we'll take all of our module's reducers (the module is what we'll build with the redux-module-builder package) and combine them into one.

For instance, we might have a few modules containing users, events, images, etc. Making the rootReducer might look similar to:

export const rootReducer = combineReducers({
  user: function(state, action) {},
  events: function(state, action) {},
  images: function(state, action) {}
})

For the time being, our rootReducer will be tiny. We'll only need to keep the routing reducer available in our store initially. Let's create a reducers variable to hold the reducers object:


export let reducers = {routing};

Although we only have one reducer in our reducers variable, to use it as the rootReducer we'll still use combineReducers():


export const rootReducer = combineReducers(reducers);

That's it. We now have a root-level reducer for our store.

Generating our rootReducer in our configureStore() function is set up. At this point, we have a working store.

Let's load our modules in an object in our rootReducer:


const modules = {
  events: require('./modules/events'),
  currentEvent: require('./modules/currentEvent'),
  images: require('./modules/images'),
  users: require('./modules/users')
};

We'll load each of these modules and combine them and export the three objects:

  1. initialState
  2. actions
  3. reducers

In our app, we can handle this in a straight-forward, programmatic method:


Object.keys(modules).forEach(key => {
  const container = modules[key];
  initialState[key] = container.initialState || {};
  actions[key] = container.actions;
  reducers[key] = container.reducer;
});

Alternatively, we can use the createRootReducer() function to handle this process for us, i.e.:


const mod = createRootReducer(modules, {
  initialInitialState: initialState,
  initialActions: actions,
  initialReducers: {routing}
})

Sometimes we'll want to start out with some initial objects, like a routing action and reducer. We can define the initial objects before we start looping through our modules:


export let actions = {
  routing: {
    navigateTo: path => dispatch => dispatch(push(path))
  }
};
export let initialState = {}
export let reducers = {routing};

This is the process we need to run to set up any redux store in any redux project, with or without redux-module-builder, so it's important to understand it.

Completing our configureStore()

Let's complete syncing our history with the store with the react-router-redux package. The package provides the syncHistoryWithStore() function to make this process easy enough to handle in a single line of code. The function returns the newly created history instance.

The syncHistoryWithStore() function accepts the history type (in our case, we'll pass this into the configureStore() function), the store, and some options:


    // Using the `const store =` variable we just created
    const history = syncHistoryWithStore(historyType, store, {
      adjustUrlOnReplay: true
    })

We'll need to pass back this history variable we just created. At the same time, we can return the store as well:

export const configureStore = ({
  historyType = browserHistory,
  userInitialState = {}}) => {

  // ...
  return {store, history}
}

3... 2... 1... Action

In redux-module-builder, we like to keep our actions bundled together in an actions object. Although this is a personal preference, we find bundling the actions together in a single object helps us maintain an easy, consistent interface to our apps, in testing and in practice.

Since we're currently only working with a single reducer (the routing reducer), our actions object will be simple. Let's create a single action with a single function: navigateTo which can push a new route on the routing stack.

In our src/redux/rootReducer.js file, let's export an actions object with this routing action. We'll rely on our middleware set up with redux-thunk.

Let's make sure we import the routerReducer as well as the push() function from react-router-redux:


import { routerReducer as routing, push } from 'react-router-redux';

Then we can use this function to build our navigateTo action we'll use to change the current browser route:


export let actions = {
  routing: {
    navigateTo: path => dispatch => dispatch(push(path))
  }
};

We have our actions object. None of our actions are aware of this routing action, nor is the store aware of them.

However, we can bind our actions to the store's dispatch function such that when any of our actions call dispatch, the store's reducers can manage the state response. This is where another redux-module-builder helper (the bindActionCreatorsToStore() function) can help us out.

Let's import the bindActionCreatorsToStore() function and pass through our imported actions and store object back in our src/redux/configureStore.js file:


import { bindActionCreatorsToStore } from 'redux-module-builder';

Now, we can bind the actions we created previously to the store's dispatch function. For the time being, we have one action we defined in the rootReducer file called navigateTo in the actions object. Binding the action to the store will allow us to call the dispatch() function and have the store's reducer-chain respond as we expect. Without this binding, calling dispatch() wouldn't do much of anything:


    const boundActions = bindActionCreatorsToStore(actions, store);

Last, let's return our boundActions object from configureStore() so we can use it in our view:


    const boundActions = bindActionCreatorsToStore(actions, store);
    return {store, actions: boundActions, history}

Now we can use the actions from our configureStore() function call.

Awesome! Now we have our actions returning from the configureStore() function and we're set to use this in our mounted React app component.

Tying the store to the app

We've completed the set-up of the redux store using basic Redux method. In this way, the entire responsibility of setting up redux is contained within the src/redux/ directory structure and we can use the set up by importing the src/redux/configureStore.js file and running the configureStore() function we created above.

We'll need to tie the store itself to our app. This means we'll run the configureStore() function and get back the store that we can use throughout our app. Since we've done all the heavy-lifting up to this point, we can just import and call the function in our app code.

In our src/app.js file (the main app file), we can handle this in a couple of lines of code. Let's bring in the history package hashHistory type and pull in the configureStore() function from our src/redux/configureStore.js file:


import {hashHistory} from 'react-router'
import {configureStore} from './redux/configureStore'

We can call the configureStore() function with an initial state, be it empty or not. We'll need this as we set it as an argument in the configureStore() function.


const {store, actions, history} =
  configureStore({initialState, historyType: hashHistory});

We can pass our new store, actions, and the history objects into our <App /> container component (which we'll look at shortly):

const makeRoutes = require('./routes').default;
const routes = makeRoutes(store)
// ...
const mountNode = document.querySelector('#root');
ReactDOM.render(
  <App history={history}
        store={store}
        actions={actions}
        routes={routes} />, mountNode);

We don't need to worry about the routerKey for the time being, but it's good to know about. We'll look through it when we talk about hot module reloading later in this post.

With react-redux, we can tie a redux store to a component using the package's <Provider /> component.

Let's run this with our src/containers/App/App.js file, let's accept these props and render out our component wrapped in the <Provider /> component.

Let's update the render() function to include the <Provider /> component wrapped around the content in our src/containers/App/App.js file, so that our component can have access to the Redux store:

export class App extends React.Component {
  // ...
  render () {
     return (
       <Provider store={this.props.store}>
         <div>
           {this.content}
         </div>
        </Provider>
     )
   }
}

For every route we create through the react-router, we'll want to pass through the actions prop we export from our configureStore() function. Since the content of our <App /> container contains the <Router /> element, we can handle setting it up through the content property on in the <App />.

We can render our routes using the routes passed in through the props along with the history object. Since we'll want to pass the actions props all the way through the app, even regardless of the component level we are at, we will need to create each element with the actions prop.

The react-router package makes this simple by passing in a createElement prop which will get called for every route render. We can use this function to clone the component and add custom props. Specifically, its allows us to pass through the actions prop.


  get content() {
    const { history, routes, routerKey, store, actions } = this.props;
    let newProps = {
      actions,
      ...this.props
    }

    const createElement = (Component, props) => {
      return <Component {...newProps} {...props} />
    }

    return (
      <Router
        key={routerKey}
        routes={routes}
        createElement={createElement}
        history={history} />
    )
  }

We're done with our set-up. Let's start using the redux-module-builder to build some redux modules.

To skip this set-up process entirely (i.e. have it set-up automatically), we can pass the --redux feature flag to our yeoman generator>

yo react-gen --redux

It is a good idea to understand what this generator is doing and creating for us.

Developer tooling (optional)

One of the coolest features of using redux is the developer tooling is just so fantastic. Let's set up our app to use these devtools so we can use them in building our app. Let's install the redux-devtools:

npm install redux-devtools

Let's also install a few of the monitors. Although there are several custom monitors, we'll use some basic ones:

npm install redux-devtools-log-monitor redux-devtools-dock-monitor

When using the devTools, let's create a container to mount our dev tools inside, hiding it by default. It's a straightforward component that will be mounted inside the body of our app. The component might look something similar to:


import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'

export default createDevTools(
  <DockMonitor
    toggleVisibilityKey='ctrl-h'
    changePositionKey='ctrl-q'
    defaultIsVisible={false}>
    <LogMonitor />
  </DockMonitor>
)

The component itself has many options. Personally, we like to prefer hiding it by default (and pressing ctrl+h to show it):

We'll need to place our devtools in our view (otherwise they won't be available in the DOM). We like to do this in our <App /> container. Since we already have our <App /> container built, we can handle this in the same manner as we display our content, using a prototype version:


  render () {
     return (
       <Provider store={this.props.store}>
         <div>
           {this.content}
           {this.devTools}
         </div>
        </Provider>
     )
   }

And the implementation of the devTools parameter might look like:


  get devTools () {
    if (__DEBUG__) {
      if (!window.devToolsExtension) {
        const DevTools = require('containers/DevTools/DevTools').default
        return <DevTools />
      }
    }
  }

When we're running in development mode, we can display the DevTools and have the provider publish nothing if we aren't in development mode using the already set up __DEBUG__ flag from webpack.

Our webpack.config.js already sets up the __DEBUG__ flag to be automatically replaced by webpack when running in the NODE_ENV == 'development'.

class App extends React.Component {
  get devTools() {
    if (__DEBUG__) {
      // ...
    }
  }
  // ...

When running in development mode, the devTools() function returns the <DevTools /> component, while in production, the __DEBUG__ flag is false and we won't include the element.

In addition to setting up the view with the devTools, we'll also need to add the devTools tooling to our store. It's for this reason why we create the tools array along with the middleware array in configureStore().

Back in our configureStore() file, let's push the devTools() extension into the compose() function to create our store:


    let tools = [];
    if (__DEBUG__) {
      const DevTools = require('containers/DevTools/DevTools').default;
      let devTools = window.devToolsExtension ? window.devToolsExtension : DevTools.instrument;
      if (typeof devTools === 'function') {
        tools.push(devTools())
      }
    }

Reloading the page and pressing ctrl-h now will show the devTools.

Hot reloading

Since we are developing some files that have a tough time being reloaded, such as the routes, we'll need to set up our app to handle hot reloading in somewhat of a manual way. At least we'll set up the routes to hot reload the page, rather than a hard refresh when we edit the file.

In order to understand how to handle hot reloading in a custom way, we'll need to know how the render function works with ReactDOM.render(). Essentially, the ReactDOM.render() function saves the current state and compares new state to this old state. If any elements are modified, the element will be rerendered in the virtual DOM and subsequently in the browser's DOM.

We can "fake" these changes by looking for any hot changes and changing a prop in the render to change even if there are no major variable changes. Let's update the src/app.js so that we can call the ReactDOM.render() function when we get a hot change as well as before we receive any changes:


let render = (routerKey = null) => {
  const makeRoutes = require('./routes').default;
  const routes = makeRoutes(store)

  const mountNode = document.querySelector('#root');
  ReactDOM.render(
    <App history={history}
          store={store}
          actions={actions}
          routes={routes}
          routerKey={routerKey} />, mountNode);
}

In our render() function, we are passing in an argument we're calling renderKey which is just some random number or undefined. The point is that our routerKey is just a prop that we can change when necessary. We'll call this render() function on first load:


render();

Now, when we receive a hot update, we can call our render function again, this time with a new version of our routes. Since we'll only have hot reloading enabled in development mode, we can run a check to see if we even need to enable this functionality quickly, as we'll see in our app usage:


if (__DEBUG__ && module.hot) {
  const renderApp = render;
  render = () => renderApp(Math.random())

  module.hot.accept('./routes', () => render());
}

Since we'll be working with our redux modules in real-time and changing their structure, and the likes. Let's run through the same process with our rootReducer.

Where we set up our rootReducer, we can call store.replaceReducer to update the reducers under the hot reloading scenario. Let's set this hot reloading up in our configureStore() function so that we can detect a change to the rootReducer (which means any child reducer of the rootReducer) changes under a hot scenario.

Somewhere after we create the store, let's check for the module.hot status and set up an accept process for an updated rootReducer and call store.replaceReducer() when our webpack build chain detects a change:


    if (module.hot) {
      module.hot.accept('./rootReducer', () => {
        const {rootReducer} = require('./rootReducer');
        store.replaceReducer(rootReducer);
      });
    }

Setting up hello

To set up hello, we'll need to install it. The easiest way to install the package is using npm:

npm install --save hellojs

We'll need to set up a twitter app and get some client_id and client_secret variables so we can attach to our backend. Currently, the realtime.fullstackweb.org works using our twitter app keep.

To run your own, you'll need to grab your own copy of the realtime-news server and your own twitter app. You can get a twitter app at the dev.twitter.com dashboard.

Once you have an app, clone the repo and install the dependencies:

git clone https://github.com/fullstackio/realtime-news.git
cd realtime-news && npm install

Once the dependencies are installed, we can either deploy it locally in development (where every change to the backend will restart the server) or to production. First, we'll need to tell the app what our Twitter keys are. The app itself uses a .env file in the root and environment variables.

Either source the environment variables in your environment or set your TWITTER_KEY and TWITTER_SECRET in a .env file at the root:

TWITTER_KEY=YOUR_TWITTER_KEY
TWITTER_SECRET=YOUR_TWITTER_SECRET

Once that's set, to launch the server in a development environment, use the start command:

npm run dev

Launching into production takes a tad more work (but not much). We'll need to build the app first and then run start:

npm run build
npm start

We've also include a docker compose file and a Dockerfile to launch and run it with Docker. Although the server itself uses the local filesystem, using leveldb, it can be switched to a heavier-duty database, such as mongodb or postgresql pretty seamlessly.

In any case, launching the realtime-news stack using docker-compose is pretty easy once you have a docker-machine environment. Setting up docker-machine is out of the scope for this tutorial, but there are plenty of great resources available.

In any case, with a docker-machine environment set up, to launch the realtime-news stack, call the start command to boot it up:

docker-compose start

To set up hello in our app, we'll need to initialize it. Back in our client-side app, we'll need to initialize hello using the TWITTER_KEY. Similar to the realtime-news server, we like to keep these keys in a .env file at the root of the app. If you set up your app to use the yeoman generator, your build system already takes care of wrapping environment variables for different deployment environments.

Let's store the TWITTER_KEY in a development config file at config/development.config.js. We'll also want to make sure the API_URL matches the root of the realtime-news server we started. For now, we'll set this to be localhost:

API_HOST=localhost:8001
API_URL=http://localhost:8001
TWITTER_KEY=YOUR_TWITTER_KEY

When we launch the app, this value can be used in the variable __TWITTER_KEY__. Since we'll be using hello in our app and it's most particularly relevant in our users authentication process, we'll initialize it in our users module at src/redux/modules/users.js (this, of course is a matter of preference).

We'll need to call init on the hello object with our twitter key (to use Twitter). Additionally, we'll also need to pass set the oauth_proxy variable to match our backend in our src/redux/modules/users.js module:


import hello from 'hellojs'

hello.init({
  twitter: __TWITTER_KEY__
}, {
  oauth_proxy: `${__API_URL__}/oauthproxy`
});

Let's boot our app using the usual npm start command at the root:

npm start

We can visit our app in the browser at localhost:3000 by default.

Conclusion

We worked through building a large app with lots of different moving pieces in a sane, comprehendible way using an MIT-licensed package for handling redux apps. We can't recommend this enough and hope that it helps simplify your apps.

If you’re stuck, have further questions, feel free to reach out to us by:


Ari Lerner

Hi, I'm Ari. I'm an author of Fullstack React and ng-book and I've been teaching Web Development for a long time. I like to speak at conferences and eat spicy food. I technically got paid while I traveled the country as a professional comedian, but have come to terms with the fact that I am not funny.

Connect with Ari on Twitter at @auser.