Redux Flow Tutorial

Redux Flow Tutorial

Dominic Chambers bio photo By Dominic Chambers

Introduction

In this tutorial I’m going to show you how you can add Facebook’s Flow to your Redux work-flow, and stop relying on the older propTypes feature for JSX template validation. Flow is a static typechecker for JavaScript that is broadly comparable to TypeScript, with the main difference being that types are optional as Flow has a powerful type inference engine. If you’re not already familiar with Flow then I’d highly recommend the Introductory Video.

Although Immutable.js is commonly paired with Redux to guarantee immutability, using something like Immutable.js reduces the insight Flow has into your code, and so reduces the number of bugs that it will detect. Instead, I’m going to use the ES6/7 rest/spread operators & Ramda with plain old JavaScript arrays and objects. This is possible since Ramda’s utility functions and the ES6/7 rest/spread operators never mutate the data you give them, and this has the additional benefit that you can increase the amount of functional re-use you can achieve due to Ramda’s excellent auto-currying support, and of course that you’ll be using native JavaScript types instead of Immutable’s proprietary types.

It won’t all be plain sailing, and you’ll see along the way that Flow and the Flow infrastructure still have a few rough edges, but that Flow often has an insight into your code that you won’t have experienced with traditional compilers.

If you don’t have time to follow the tutorial you can just read along. The redux-flow-tutorial contains all of the resultant source code for you to look at, with commits at the various milestones throughout the article so you can track how we arrive at the final conclusion.

What about TypeScript?

At this point it’s worth talking more about TypeScript. TypeScript also provides static typing for JavaScript, but uses a type system that started life being much more similar to what you get with Java and C#, and so didn’t work with lots of idiomatic JavaScript. More recently, TypeScript has begun adding the same type of features you find in Flow (like union-types, intersection-types and action-guards), but it still hasn’t achieved parity, and it still can’t be used to type an idiomatic Redux reducer, or lots of other idiosyncratic JavaScript patterns we find in the wild.

There’s nothing particularly wrong with that, but it does mean that TypeScript is better suited for Angular 2 development then it is for Redux development right now, though the two do seem to be slowly converging. Additionally, Flow fits better into the NPM eco-system, and can be used alongside stellar tools like Babel and ESLint, which is another reason you might prefer it over TypeScript. This is a shame though, because at this point TypeScript has a more mature eco-system than Flow; partly due to it being an older project, but also because it’s a less technically challenging endeavour.

Before We Start

Before we start, let’s upgrade to a recent version of Node.js so we can (almost) have ES6 support without using a transpiler:

nvm install 5.11.0

If you don’t use NVM then you may want to upgrade some other way, or just not bother since it’s actually not too critical to the rest of this tutorial.

Let’s Go…

We’ll start by setting up a new project:

mkdir flow-redux-tutorial
cd flow-redux-tutorial
npm init -f

and installing Redux and Ramda:

npm install --save redux ramda

Now you can create a src/reducers/todo-items-reducer.js file with the following contents:

import {compose as lens, lensIndex as i, lensProp as p, remove, set} from 'ramda';

const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';
const EDIT_TODO = 'EDIT_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const COMPLETE_ALL = 'COMPLETE_ALL';
const CLEAR_COMPLETED = 'CLEAR_COMPLETED';

export const todoItemsReducer = (todoItems = [], action) => {
  switch(action.type) {
    case ADD_TODO: {
      const newTodoItem = {text: action.text, completed: false};
      return [...todoItems, newTodoItem];
    }

    case DELETE_TODO: {
      return remove(action.index, 1, todoItems);
    }

    case EDIT_TODO: {
      return set(lens(i(action.index), p('text')), action.text, todoItems);
    }

    case TOGGLE_TODO: {
      const completed = todoItems[action.index].completed;
      return set(lens(i(action.index), p('completed')), !completed, todoItems);
    }

    case COMPLETE_ALL: {
      return todoItems.map((todoItem) => ({...todoItem, completed: true}));
    }

    case CLEAR_COMPLETED: {
      return todoItems.filter((todoItem) => !todoItem.completed);
    }

    default: {
      return todoItems;
    }
  }
};

This reducer is using ES6/7 features like the array/object spread operators to ensure that the intent of the program will be understood by Flow when we introduce it.

Testing our reducer

Let’s install Mocha, Chai & Babel so we can test our reducer:

npm install --save-dev mocha chai babel-core babel-preset-modern babel-plugin-transform-object-rest-spread

Here we’ve installed babel-preset-modern instead of babel-preset-es2015, which will reduce what’s transpiled to pretty much just the import and export statements — use babel-preset-es2015 instead if you didn’t upgrade to a recent version of Node.js at the beginning of the tutorial.

You’ll need to create a .babelrc file with the following contents before Babel can have any effect:

{
  "presets": ["modern"],
  "plugins": [
    "transform-object-rest-spread"
  ]
}

Now create a src/reducers/todo-items-reducer.spec.js file with the following contents:

import {todoItemsReducer} from './todo-items-reducer';
import {describe, it} from 'mocha';
import {expect} from 'chai';

describe('todo-items-reducer', () => {
  const initialTodoItems = [
    {text: 'Do stuff.', completed: true},
    {text: 'Do more stuff.', completed: false}
  ];

  it('allows items to be added', () => {
    const todoItems = todoItemsReducer(undefined, {type: 'ADD_TODO', text: 'Do stuff.'});
    expect(todoItems).to.deep.equal([{text: 'Do stuff.', completed: false}]);
  });

  it('allows items to be removed', () => {
    const todoItems = initialTodoItems;
    const updatedTodoItems = todoItemsReducer(todoItems, {type: 'DELETE_TODO', index: 0});
    expect(updatedTodoItems).to.deep.equal([
      {text: 'Do more stuff.', completed: false}
    ]);
  });

  it('allows items to be edited', () => {
    const todoItems = initialTodoItems;
    const editedTodoItems = todoItemsReducer(todoItems, {type: 'EDIT_TODO', index: 0, text: 'Do some stuff.'});
    expect(editedTodoItems).to.deep.equal([
      {text: 'Do some stuff.', completed: true},
      {text: 'Do more stuff.', completed: false}
    ]);
  });

  it('allows items to be marked as completed', () => {
    const todoItems = initialTodoItems;
    const modifiedTodoItems = todoItemsReducer(todoItems, {type: 'TOGGLE_TODO', index: 1});
    expect(modifiedTodoItems).to.deep.equal([
      {text: 'Do stuff.', completed: true},
      {text: 'Do more stuff.', completed: true}
    ]);
  });

  it('allows completed items to be marked as uncompleted', () => {
    const todoItems = initialTodoItems;
    const modifiedTodoItems = todoItemsReducer(todoItems, {type: 'TOGGLE_TODO', index: 0});
    expect(modifiedTodoItems).to.deep.equal([
      {text: 'Do stuff.', completed: false},
      {text: 'Do more stuff.', completed: false}
    ]);
  });

  it('allow all items to be completed at once', () => {
    const todoItems = initialTodoItems;
    const modifiedTodoItems = todoItemsReducer(todoItems, {type: 'COMPLETE_ALL'});
    expect(modifiedTodoItems).to.deep.equal([
      {text: 'Do stuff.', completed: true},
      {text: 'Do more stuff.', completed: true}
    ]);
  });

  it('allows completed items to be removed', () => {
    const todoItems = initialTodoItems;
    const modifiedTodoItems = todoItemsReducer(todoItems, {type: 'CLEAR_COMPLETED'});
    expect(modifiedTodoItems).to.deep.equal([
      {text: 'Do more stuff.', completed: false}
    ]);
  });
});

and replace the test script in package.json with this:

"test": "mocha --compilers js:babel-core/register src/**/*.spec.js"

If you now run npm test you will see that all of the tests successfully pass. The eagle eyed among you may have noticed that we didn’t use action-creator functions within the test. Action creator functions have always felt to me like necessary boiler-plate, the need for for which would disappear if our JSON structures could be automatically type checked; well soon they will be!

Adding a linting pre-test step

We’ll need to install ESLint so we can lint our code:

npm install --save-dev eslint

and add an .eslintrc config file to enable the default set of linting rules and ES6/7 support, which will look like this:

{
  "extends": "eslint:recommended",
  "env": {
    "es6": true
  },
  "parserOptions": {
    "sourceType": "module",
    "ecmaFeatures": {
      "experimentalObjectRestSpread": true
    }
  }
}

We can now add a pretest script above the test script in package.json:

"pretest": "eslint src",

which will also cause linting tests to run when you run npm test again. So far so good…

Ideally, you should configure your editor so that it can display ESLint errors in-line since this will make development much easier — for example, I use linter-eslint for Atom.

Time for some Flow annotations!

Next, I’m going to walk you through the process of annotating the code we’ve written for Flow, but you may want to wait until we reach the Putting it all together… section before updating any files.

Within todo-items-reducer.js, we previously described a reducer that took some state and an action, and returned some modified state. If we say that state is of type TodoItems, and that action is of type TodoAction, then we can create an annotated version of the todoItemsReducer function where the function signature changes to look like this:

export const todoItemsReducer = (todoItems: TodoItems = [], action: TodoAction): TodoItems => {

leaving us only the task of defining TodoItems and TodoAction.

The TodoItems type

TodoItems can be defined as an array of TodoItem, like so:

export type TodoItems = Array<TodoItem>;

where TodoItem can be defined as an object having an item property of type string and a completed property of type boolean, for example:

type TodoItem = {
  text: string,
  completed: boolean
};

The neat thing here is that the empty array literal [] qualifies as being of type TodoItems, and any correctly typed object (e.g.{text: 'Do stuff.', completed: false}) qualifies as being of type TodoItem. It’s got nothing to do with which constructor was used to create an object, and even if you don’t use action-creator functions to create your actions they will still be of the right type.

Even code that builds a type up in stages will type check fine, like this for example:

const obj = {text: 'Do stuff.'};
const todoItem: TodoItem = {...obj, completed: false};

Again, Flow comprehends the flow of our code … nice!

The TodoAction type

We saw previously that action.type had one of six possible values (e.g. ADD_TODO and DELETE_TODO), and that the reducer’s switch statement took a different path depending on which value it had. This really means that there are six different types here (e.g. AddTodoAction and DeleteTodoAction), and so TodoAction can be defined as a union type, like so:

type TodoAction = AddTodoAction | DeleteTodoAction | EditTodoAction |
  ToggleTodoAction | CompleteAllAction | ClearCompletedAction;

Subsequently, the types themselves can be defined like this:

type AddTodoAction = {
  type: 'ADD_TODO',
  text: string
};
type DeleteTodoAction = {
  type: 'DELETE_TODO',
  index: number
};
type EditTodoAction = {
  type: 'EDIT_TODO',
  index: number,
  text: string
};
type ToggleTodoAction = {
  type: 'TOGGLE_TODO',
  index: number
};
type CompleteAllAction = {
  type: 'COMPLETE_ALL'
};
type ClearCompletedAction = {
  type: 'CLEAR_COMPLETED'
};

In all cases, instead of defining type as a string, it’s defined as a string with a particular value.

This is important since the values of type are distinct within the union type TodoAction, so that for any action of type TodoAction where action.type is equal to 'ADD_TODO', we can unambiguously say that the type of that action is AddTodoAction, rather than DeleteTodoAction, or some other action.

This means that while this code will error:

const f = (action: TodoAction) => {
  action.type; // okay
  action.text; // will error!!!
};

this code will type check fine because of the qualifying if guard:

const f = (action: TodoAction) => {
  if(action.type === 'ADD_TODO') {
    action.text;
  }
};

That’s pretty sweet!

Putting it all together…

When you put it all together you should end up with a todo-items-reducer.js that looks like this:

/* @flow */
import {compose as lens, lensIndex as i, lensProp as p, remove, set} from 'ramda';

export type TodoItems = Array<TodoItem>;
type TodoItem = {
  text: string,
  completed: boolean
};

type TodoAction = AddTodoAction | DeleteTodoAction | EditTodoAction |
  ToggleTodoAction | CompleteAllAction | ClearCompletedAction;
type AddTodoAction = {
  type: 'ADD_TODO',
  text: string
};
type DeleteTodoAction = {
  type: 'DELETE_TODO',
  index: number
};
type EditTodoAction = {
  type: 'EDIT_TODO',
  index: number,
  text: string
};
type ToggleTodoAction = {
  type: 'TOGGLE_TODO',
  index: number
};
type CompleteAllAction = {
  type: 'COMPLETE_ALL'
};
type ClearCompletedAction = {
  type: 'CLEAR_COMPLETED'
};

const ADD_TODO = 'ADD_TODO';
const DELETE_TODO = 'DELETE_TODO';
const EDIT_TODO = 'EDIT_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const COMPLETE_ALL = 'COMPLETE_ALL';
const CLEAR_COMPLETED = 'CLEAR_COMPLETED';

export const todoItemsReducer = (todoItems: TodoItems = [], action: TodoAction): TodoItems => {
  switch(action.type) {
    case ADD_TODO: {
      const newTodoItem = {text: action.text, completed: false};
      return [...todoItems, newTodoItem];
    }

    case DELETE_TODO: {
      return remove(action.index, 1, todoItems);
    }

    case EDIT_TODO: {
      return set(lens(i(action.index), p('text')), action.text, todoItems);
    }

    case TOGGLE_TODO: {
      const completed = todoItems[action.index].completed;
      return set(lens(i(action.index), p('completed')), !completed, todoItems);
    }

    case COMPLETE_ALL: {
      return todoItems.map((todoItem) => ({...todoItem, completed: true}));
    }

    case CLEAR_COMPLETED: {
      return todoItems.filter((todoItem) => !todoItem.completed);
    }

    default: {
      return todoItems;
    }
  }
};

Unfortunately, if you run npm test at this point you’ll notice that it now fails on the linting step, and so we’ll need to fix that next…

Making ESLint & Babel support Flow

We can fix the linting errors we saw previously by installing the following packages:

npm install --save-dev babel-eslint eslint-plugin-babel eslint-plugin-flow-vars

and updating .eslintrc to have a plugins and parser section, so that it becomes:

{
  "extends": "eslint:recommended",
  "env": {
    "es6": true
  },
  "plugins": [
    "flow-vars"
  ],
  "parser": "babel-eslint",
  "parserOptions": {
    "sourceType": "module",
  }
}

If you now run npm run pretest you’ll see that the linting is working again, but if you run npm test you’ll see that the tests themselves still fail.

We can fix this by installing the following Babel plug-in:

npm install --save-dev babel-plugin-transform-flow-strip-types

and adding transform-flow-strip-types to the plugins section of .babelrc, so you end up with:

"plugins": [
  "transform-object-rest-spread",
  "transform-flow-strip-types"
]

at which point npm test will now work again!

Adding a typing pre-test step

Before we forget, let’s start by adding a /* @flow */ comment to the first line of todo-items-reducer.spec.js.

Now, it’s finally time to install Flow:

npm install --save-dev flow-bin

Warning: The flow-bin package doesn’t currently work on Windows or 32bit Linux. Windows users can probably install this themselves, ensuring that it’s on the path, given there are now non-official Windows binaries being made available.

The Flow library should be initialized as follows, which will cause it to create a .flowconfig file for your project:

flow init

Warning: If you don’t have ./node_modules/.bin permanently added to your path then you’ll need to run ./node_modules/.bin/flow init instead.

Let’s now replace the pretest script in package.json with this, so that we have separate pretest:lint and pretest:typecheck scripts:

"pretest": "npm run pretest:lint && npm run pretest:typecheck",
"pretest:lint": "eslint src",
"pretest:typecheck": "flow",

Because Flow and babel-core don’t get on at present, you’ll need to add the following line to the ignore section of .flowconfig:

.*node_modules/babel-core.*
.*node_modules/fbjs.*

If you run npm test again you should see that type-checking happens now too, and that everything passes!

If you run npm test a second time you’ll notice that it’s quicker the second time around due to the fact that the flow command spawned a background daemon the first time around. You can confirm this for yourself by running:

ps -ef | grep flow-bin

Getting type feedback in your editor

Just like it is with linting, not having typing feedback in your editor makes for a bad developer experience. I’ll assume you’re using Atom here since that’s what I use, and that you’ve already installed linter-eslint so you already have linting feedback within your editor.

Given this is the case, you’ll next want to install the linter-flow plug-in. After installing you should get Flow errors displayed within the Atom — just try breaking stuff!

Warning: Depending on which OS you use and how you installed Node.js, you may now need to start Atom from the command-line so that Atom has access to your normal environment variables.

Kicking the tyres

At this point you may want to have a play around, and see exactly what Flow is and isn’t capable of. For example, if you now change the definition of AddTodoAction to this:

type AddTodoAction = {
  type: 'ADD_TODOX',
  text: string
};

and then re-run npm test, you’ll see the following errors within the terminal output:

src/reducers/todo-items-reducer.spec.js:13
 13:     const todoItems = todoItemsReducer(undefined, {type: 'ADD_TODO', text: 'Do stuff.'});
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function call
 13:     const todoItems = todoItemsReducer(undefined, {type: 'ADD_TODO', text: 'Do stuff.'});
                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object literal. This type is incompatible with
 43: export const todoItemsReducer = (todoItems: TodoItems = [], action: TodoAction): TodoItems => {
                                                                         ^^^^^^^^^^ union: AddTodoAction | DeleteTodoAction | EditTodoAction | ToggleTodoAction | CompleteAllAction | ClearCompletedAction. See: src/reducers/todo-items-reducer.js:43

and you should see the same errors displayed in your editor too.

Warning: Supporting union and intersection types in a way where the error messages you receive still make sense in all cases is non-trivial, and there are presently still a few edge cases you may run into. These are slowly being fixed as far as I can tell, and things will hopefully improve before too much longer.

Adding a simplistic view

Before we can show how Flow’s typing can be used instead of React’s propType feature, we’ll need to install React so we can create a rudimentary view.

Begin by installing the library dependencies we’ll need:

npm install --save react react-dom react-redux

followed by installing the development dependencies we’ll need:

npm install --save-dev babel-preset-react eslint-plugin-react

Next, add react to the list of presets in .babelrc, so it looks like this:

"presets": ["modern", "react"],

and similarly add react to the list of plug-ins in .eslintrc so it looks like this:

"plugins": [
  "flow-vars",
  "react"
],

Then, add the following block to the parserOptions section of .eslintrc:

"ecmaFeatures": {
  "jsx": true
}

and finally update the extends definition in .eslintrc to be an array so we can add a plugin:react/recommended entry, as follows:

"extends": ["eslint:recommended", "plugin:react/recommended"],

With the config in place, you can now add the following new files:

src/Todo.jsx:

import React, {PropTypes} from 'react';

export const Todo = ({text, completed, onClick}) => (
  <li
    onClick={onClick}
    style=
  >
    {text}
  </li>
);

Todo.propTypes = {
  text: PropTypes.string.isRequired,
  completed: PropTypes.bool.isRequired,
  onClick: PropTypes.func.isRequired
};

export default Todo;

src/TodoList.jsx:

import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import Todo from './Todo';

export const TodoList = ({todos, onTodoClick}) => (
  <ul>
    {
      todos.map((todo, index) =>
      <Todo
        key={index}
        {...todo}
        onClick={() => onTodoClick(index)}
      />
    )}
  </ul>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(PropTypes.shape({
    text: PropTypes.string.isRequired,
    completed: PropTypes.bool.isRequired
  }).isRequired).isRequired,
  onTodoClick: PropTypes.func.isRequired
};

export const TodoListHOC = connect(
  (state) => ({
    todos: state
  }),
  (dispatch) => ({
    onTodoClick: (index) => {
      dispatch({type: 'TOGGLE_TODO', index});
    }
  })
)(TodoList);

export default TodoListHOC;

and src/app.js:

/* global document */
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import {todoItemsReducer} from './reducers/todo-items-reducer';
import TodoList from './TodoList';

const store = createStore(todoItemsReducer);
const appElem = document.createElement('div');
appElem.id = 'app';

store.dispatch({type: 'ADD_TODO', text: 'Do stuff.'});
store.dispatch({type: 'ADD_TODO', text: 'Do more stuff.'});
store.dispatch({type: 'TOGGLE_TODO', index: 0});

document.body.appendChild(appElem);

render(
  <Provider store={store}>
    <TodoList />
  </Provider>,
  appElem
);

You should now be able to successfully run npm test again.

Viewing the running app

To view our running app we’ll install Budo:

npm install --save-dev budo babelify

and then add the following start script to package.json:

"start": "budo src/app.js -- -t babelify --extension=jsx"

so that we can start the server like this:

npm start

Even if you’re not following along with the tutorial you can demo the running app here.

JSX Validation Without propType

Adding type annotations to Todo.jsx and TodoList.jsx first requires us to add a /* @flow */ comment to both files, then to update the renderer in Todo.jsx to have this type signature:

type TodoArgs = {text: string, completed: boolean, onClick: Function};
export const Todo = ({text, completed, onClick}: TodoArgs): Object => (

and the function signature in TodoList.jsx to have this type signature:

import type {TodoItems} from './reducers/todo-items-reducer';

type TodoListArgs = {todos: TodoItems, onTodoClick: Function};
export const TodoList = ({todos, onTodoClick}: TodoListArgs): Object => (

At which point if you intentionally create an invalid JSX component like this in Todo.jsx:

<Todo invalid-prop/>

then … nothing!

JSX Validation Without propType (Attempt Two)

Unfortunately, at present, Flow does not support stateless functional React components, and only supports React.createClass and extends React.Component. Let’s update both Todo.jsx and TodoList.jsx to use extends React.component syntax.

Before we do this we’ll need to add support for ES7 class properties by running this:

npm install babel-plugin-transform-class-properties

and updating the plugins section of .babelrc to this:

"plugins": [
  "transform-class-properties",
  "transform-object-rest-spread",
  "transform-flow-strip-types"
]

Once this is done you can update Todo.jsx to look like this:

/* @flow */
import React from 'react';

type TodoArgs = {text: string, completed: boolean, onClick: Function};
export default class Todo extends React.Component {
  props: TodoArgs;

  constructor(props: TodoArgs) {
    super(props);
  }

  render() {
    return (
      <li
        onClick={this.props.onClick}
        style=
      >
        {this.props.text}
      </li>
    );
  }
}

and TodoList.jsx to look like this:

/* @flow */
import React from 'react';
import {connect} from 'react-redux';
import Todo from './Todo';
import type {TodoItems} from './reducers/todo-items-reducer';

type TodoListArgs = {todos: TodoItems, onTodoClick: Function};
export class TodoList extends React.Component {
  props: TodoListArgs;

  constructor(props: TodoListArgs) {
    super(props);
  }

  render() {
    return (
      <ul>
        {
          this.props.todos.map((todo, index) =>
          <Todo
            key={index}
            {...todo}
            onClick={() => this.props.onTodoClick(index)}
          />
        )}
      </ul>
    );
  }
}

export const TodoListHOC = connect(
  (state) => ({
    todos: state
  }),
  (dispatch) => ({
    onTodoClick: (index) => {
      dispatch({type: 'TOGGLE_TODO', index});
    }
  })
)(TodoList);

export default TodoListHOC;

Now, the same invalid JSX component within Todo.jsx will yield type errors as expected.

Type Verification on Higher Order Components

I held back from publishing this article for over a week because of this Redux issue comment which seems to indicate that it should be possible to do type verification on HOCs created with connect(), but ten days later and the Redux version is yet to magically appear! So, I’m publishing anyway, but with the hope that further progress in this area is made, and JSX validation starts to work for HOCs too.