Redux Flow Tutorial

By Dominic Chambers May 03, 2016

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:

Testing our reducer

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',()=>{constinitialTodoItems=[{text:'Do stuff.',completed:true},{text:'Do more stuff.',completed:false}];it('allows items to be added',()=>{consttodoItems=todoItemsReducer(undefined,{type:'ADD_TODO',text:'Do stuff.'});expect(todoItems).to.deep.equal([{text:'Do stuff.',completed:false}]);});it('allows items to be removed',()=>{consttodoItems=initialTodoItems;constupdatedTodoItems=todoItemsReducer(todoItems,{type:'DELETE_TODO',index:0});expect(updatedTodoItems).to.deep.equal([{text:'Do more stuff.',completed:false}]);});it('allows items to be edited',()=>{consttodoItems=initialTodoItems;consteditedTodoItems=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',()=>{consttodoItems=initialTodoItems;constmodifiedTodoItems=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',()=>{consttodoItems=initialTodoItems;constmodifiedTodoItems=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',()=>{consttodoItems=initialTodoItems;constmodifiedTodoItems=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',()=>{consttodoItems=initialTodoItems;constmodifiedTodoItems=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:

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:

The TodoItems type

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

typeTodoItem={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:'Dostuff.',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:

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:

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.

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:

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:

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

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

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:

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

<Todoinvalid-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:

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.