Useful Tips for
Real-World Redux

Nicolas Maquet

Full-Stack Dev at Movio

@nicmaquet

github.com/nmaquet


Slides & Blog Post

https://nmaquet.github.io/redux-akljs-talk

http://movio.co/blog

<ReduxIntro>

One global Redux store


  const store = Redux.createStore(…);
          

The store contains one state object


  store.getState();
          

The UI dispatches actions to the store


  const Todo = ({ id, text, completed }) => (
     <p
       style={{textDecoration: completed ? 'line-through' : 'none'}}
       onClick={() => store.dispatch({ type: 'TOGGLE_TODO', id })}
     >
       {text}
     </p>
  )
          

The UI subscribes to store updates

and re-renders using the global state


  function render() {
    ReactDOM.render(
      <App state={store.getState()} />,
      document.getElementById('app')
    );
  }

  render();
  store.subscribe(render);
          

The State updates using
reducer functions


  const initialState = … ;

  function reducer (state, action) {
    if (state === undefined) {
      return initialState;
    }
    switch (action.type) {
       … // return newly computed state
    }
  }
          

  const store = Redux.createStore(reducer);
          

source: http://makeitopen.com/tutorials/building-the-f8-app/data/

Redux Learning Resources

Awesome resources by Dan Abramov (the Redux creator):

  • http://redux.js.org/docs/introduction/index.html
  • https://egghead.io/courses/getting-started-with-redux
  • https://egghead.io/courses/building-react-applications-with-idiomatic-redux

</ReduxIntro>

Tip #1

Use JSON schema to validate the state

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
}

JSON Schema Specs

http://json-schema.org/

JSON Schema Practical Guide

https://spacetelescope.github.io/understanding-json-schema/

Online Validator

http://www.jsonschemavalidator.net/
https://github.com/tdegrunt/jsonschema
npm install --save jsonschema

State validation function

import invariant from 'invariant';
import { Validator } from 'jsonschema';
import stateSchema from 'json!stateSchema.json';

const validator = new Validator();

export function validateState(state) {
  if (__DEV__) {
    const validation = validator.validate(obj, stateSchema);
    invariant(validation.valid, `invalid state: ${validation}`);
  }
  return state;
}

Self-validating reducer

import validateState from './validateState';

export function reducer(state, action) {
  return validateState(
    …
  );
}

Tip #2

Use middleware for cross-reducer validation

Cross-reducer validation

  • individual reducer schemas
  • also need to check for inconsistent data across reducers
function checkState() {
  //  check for orphaned data
  //  check for inconsistent data
  …
}

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

Tip #3

Dispatch reducer exceptions as actions

Catch & Dispatch Exceptions

const catchExceptions = (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 = Redux.createStore(catchExceptions(() => store)(
  Redux.combineReducers({
    foo: fooReducer,
    bar: barReducer,
    …
  })
));

Tip #4

Use middleware for cross-cutting concerns

Cross-cutting concerns

  • alerting
  • logging
  • error management
  • analytics



http://redux.js.org/docs/advanced/Middleware.html

Alerting middleware

export default function alertMiddleware({ getState }) {
  return (next) => (action) => {
    switch(action.type) {
      case "SAVE_SUCCEEDED":
        Alert.success('Todos saved');
        break;
      case "SAVE_FAILED":
        Alert.error(`Could not save todos: ${action.error}`);
        break;
      case "REDUCER_ERROR":
        Alert.error(`Unexpected error: ${action.error}`);
        break;
    }
    return next(action);
  };
};

UX Analytics Middleware

const trackedActionTypes = [
  "SAVE_TODOS",
  "COMPLETE_TODO",
  "CREATE_TODO"
];

export default function heapAnalyticsMiddleware({ getState }) {
  return (next) => (action) => {
    if (_.includes(trackedActionTypes, action.type) {
       heap.track(action.type);
    }
    return next(action);
  };
};

Thanks!