Explore Blog

5 Useful Tips For Real-World Redux

At Movio, we put our first React / Reduxapp into production a few weeks ago and our engineering team has picked up a few interesting patterns and tricks along the way. Here are five useful tips for real-word Redux.

1. Use a getters reducer to isolate the state shape

When using combineReducers and other higher-order reducers like redux-undo, your state shape will naturally become quite nested, and unless precautions are taken, your React components will directly depend your state’s nesting and general shape. We have seen people use reselect to mitigate this issue, but we opted instead for adding a reducer that adds getter properties that React components can use. Using this pattern, the state shape is completely decoupled from React components and is neatly documented in one place. Here is an example of such a getters reducer:

export default function gettersReducer(getStore) {
  const getters = {
    get baz() {
       return getStore().getState().foo.bar.baz;
    },
    get quux() {
       return getStore().getState().foo.bar.quux;
    }
  }
  return function (state, action) {
    return getters;
  }
}

Adding the getters reducers to your store is done like so:

const store = createStore(
  combineReducers({
    foo: fooReducer,
    …
    get: gettersReducer(() => store)
  })
);

Finally, here is an example usage of the getters reducer in react-redux’s mapStateToProps:

function mapStateToProps(state) {
  return {
    baz: state.get.baz,
    quux: state.get.quux
  }
}

2. Use JSON schema to validate the state shape

As our application grew in complexity, and as we added more and more reducers, we found it increasingly difficult to guarantee that all reducers were working together correctly to create a consistent and valid state shape. Additionally, we have found that simply glancing over a reducer’s implementation isn’t enough to understand the state shape that it is responsible for, so, we felt that some additional documentation was needed.

We opted to define JSON schemas for all of our reducers and, only when in development mode, we use a JSON schema validator in each reducer to detect validation errors. There is some overhead in having to write and maintain these schemas, but we have found that the benefits of added documentation and automatic validation largely outweigh the costs. If you want to get started using JSON schemas, we suggest reading this great reference by Michael Droettboom.

Here is an example of a JSON schema for a simple todo application (note that the “^todo[0-9]+$” id scheme is used for simplicity, we would recommend using v4 UUIDs in real-world apps):

// Redux state object  

{
  "todo1": {
    title: "learn React",
    completed: true
  },
  "todo2": {
    title: "learn Redux",
    completed: true
  },
  "todo3": {
    title: "write blog post",
    completed: false
  }
}
// JSON schema

{
  "type": "object",
  "patternProperties": {
    "^todo[0-9]+$": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string"
        },
        "completed": {
          "type": "boolean"
        }
      },
      "required": [
         "title",
         "completed"
      ],
      "additionalProperties": false
    }
  },
  "additionalProperties": false
}

3. Use middleware to validate state across reducers

JSON schemas are great to detect errors in your state such as a missing or mistyped property, but they can’t detect more subtle errors such as orphaned or inconsistent data. More generally, even if each reducer guarantees its own local consistency, you can still end up with an inconsistent state by looking at the Redux state as a whole. To perform these extra cross-reducer checks, we opted to write a dedicated Redux middleware, that we again only enable in development mode, and that validates the state according to global rules. Here is a basic implementation:

function checkState(state) {
   … // add global cross-reducer state checks here
}

export default function checkStateMiddleware(store) {
  return (next) => (action) => {
    const returnValue = next(action);
    checkState(store.getState());
    return returnValue;
  };
}

4. Catch and dispatch exceptions from reducers

In development, it is fine to let reducer exceptions uncaught and get a stack trace in the console. In production though, you'll probably want to catch them, forward them to a logging service, and alert the user that something went wrong. Here is a simple wrapper for the root reducer that dispatches all uncaught exceptions:

const catchReducerExceptions = (getStore) => (reducer) => (state, action) => {
  try {
    return reducer(state, action);
  } catch (e) {
    if (state === undefined) {
      // don't dispatch if exception occurs during initialization
      throw e;
    }
    console.error(e);
    // use setTimeout to avoid recursive call to dispatch()
    setTimeout(() => getStore().dispatch({type: 'REDUCER_ERROR', exception: e }));
    return state;
  }
}

const store = createStore(catchReducerExceptions(() => store)(
  combineReducers({
    foo: fooReducer,
    …
  })
));

5. Use stateless reducers for cross-cutting concerns

Cross-cutting concerns such as error management, logging, tracking, and alerting can be painful to implement as they have a tendency to “pollute” the code in a variety of places. Using Redux, we have found that many of those concerns can be solved in complete isolation from other parts of the code by writing reducers that don’t modify the state but act upon Redux actions in various ways. Here are a few examples:

  • We created a logger reducer that forwards front-end errors to our back-end for centralized logging. This means we can correlate front-end and back-end errors in a unique log stream
  • We created an alerting reducer that reacts to specific actions to display growl-style alert boxes to the user (we use react-s-alert which works really well)
  • We created an analytics reducer that forwards specific events to our analytics platform (we use Heap Analytics)

Here is a simple example of a stateless reducer that uses react-s-alert to display alert boxes:

import actionTypes from './actionTypes'
import Alert from 'react-s-alert';

const alerters = {
  [actionTypes.SAVE_SUCCEEDED]: (action) => (
    Alert.success('Todos saved')
  ),
  [actionTypes.SAVE_FAILED]: (action) => (
    Alert.error(`Could not save todos: ${action.error}`)
  ),
  …
};

export default (state = {}, action) => {
  const alerter = alerters[action.type];
  if (alerter) alerter(action);
  return state;
};

Hopefully, you've found these tips useful. Please feel free to share your own Redux tips with us in the comments below!

Find out more about joining the Movio Dev Team.

Comments

cancel replyPlease wait . . .Your comment has been posted!
Your comment has been posted, it will be visible for other users after approval.

Subscribe to our newsletter

Keep me
in the loop

Our monthly email update with marketing tips, audience insights and Movio news.