frantic.im

Using Redux with Flow

Redux is a great library for managing application’s state. But JavaScript is still a dynamically typed language, which comes with a lot of surprises at runtime. In this post I’ll show you how you can use Flow to embrace static type checking in your Redux-powered application. I choose to use Flow for the disjoint union support and the ability to convert the codebase incrementally. Read on to learn more.

This post assumes that you are already familiar with Redux: actions, action creators, reducers, etc.

Init

Flow is a static type checker for JavaScript. It works by analyzing your source code, reading (and inferring) type information and making sure there are no conflicts. It’s very easy to get started:

$ npm install --save-dev flow-bin
$ touch .flowconfig
$ ./node_modules/.bin/flow

Flow will check only the files that have special /* @flow */ or // @flow comment in them. See Flow’s Getting Starget guide for more information.

Reducers

Likely your store already has an implicit shape and you have a bunch of reducers controlling parts of it. We can use type statement to describe what it contains:

// @flow
type State = {
  isLoggedIn: boolean;
  name: string;
};

Having individual reducers state defined, it’s easy to add just a few type annotations to your reducer and start collecting benefits:

function user(state: State = initialState, action: Object): State {
}

Flow will make sure that your initialState and the return value of the user function always satisfy the State type.

As you’ve noticed, action’s type is Object. Redux makes no assumptions about the shape and the content on that object, however in most cases your app has a very specific idea what action is.

See adding Flow types for reducers diff.

Actions

Of course, we can define Action type to be as simple as:

type Action = { type: string; payload: Object };

I.e. any object that has type and payload properties can be an action. From what I’ve seen out there, people normally use constants for action types. This approach gives you some protection against typos in action names, however:

  1. constants require extra export/import boilerplate
  2. they don’t help you with the particular action’s payload – it’s still just a bag of untyped data.

We can do much better. Let’s start by defining all possible actions flowing through our app. We will use a union type for this. Even if you’ve never seen them before, I think it’s easy to figure out what it does by looking at example:

type Action = { type: 'LOGGED_IN', userName: string }
            | { type: 'LOGGED_OUT' };

In this case, { type: 'LOGGED_IN', userName: 'frantic' } is a valid Action, however

{ type: 'LOGGED_IN', login: 'frantic' } // `login` is not `userName`
{ type: 'LOGGED_IN' }                   // no `userName`
{ type: 'TYPO_HERE' }                   // `type` doesn't exist

are not.

Note: type is not some built-in magical field name for Flow, it can deal with any property name. See Flow’s documentation on Disjoint Unions for more information.

Having all possible actions typed like this is great, because it’s very clear what can happen inside your app. Take a look at actions/types.js from the F8 app for example.

I understand it can be a bit controversial, for example what to do with dynamic actions, or _LOADING/_SUCCESS/_FAILURE “sub-actions”. I’ll try to share my approach to using Redux in a separate blog post.

The great thing about Flow’s support for the tagged unions is that it can narrow down the action type depending on your code control flow:

function user(state: State, action: Action): State {
  if (action.type === 'LOGGED_IN') {
    // In this `if` branch Flow narrowed down the type
    // and it knows that action has `userName: string` field
    return { isLoggedIn: true, name: action.userName };
  }
}

(switch statement works just as well)

This is incredibly useful and ensures all payloads can be properly type checked, even if their content is different for each action type.

Action Creators

This one is easy, we can just declare that our action creators return Action:

function logOut(): Action {
  return { type: 'LOGGED_OUT' };
}

To make the example a little more interesting, let’s assume we are using a custom Redux middleware that supports dispatching Promises of actions.

async function logIn(login: string, pass: string): Promise<Action> {
  let response = await fetch(...);
  // Check response code, maybe do more fetching...
  return {
    type: 'LOGGED_IN',
    userName: login,
  };
}

See adding Flow types for actions diff.

Dispatch

We can take a step further and annotate our dispatch function. In the simplest case it’s just a function that accepts Action and returns void (nothing):

type Dispatch = (action: Action) => void;

However, depending on the list of the middleware you use, dispatch can support thunk actions, promises, arrays, etc. For our example we use Promise middleware so we can describe our dispatch function like this:

type Dispatch = (action: Action | Promise<Action>) => Promise;

To use this in React components we can simply tell Flow about dispatch prop that react-redux module injects via connect API:

class SettingsScreen extends React.Component {
  props: {
    isLoggedIn: boolean;
    dispatch: Dispatch;
  };

  render() { ... }
}

This code ensures that only Action or Promise<Action> can be passed into this.props.dispatch() call.

See adding Flow types for dispatch diff.

More

Flow is much more than just a type checker. It can power a lot of IDE-like features, for example auto-complete, jump-to-definition, etc. I highly recommend installing Flow plugin for your faviorite editor, this dramatically improves development experience.

Wrapping up

Flow is great because it lets us incrementally add type annotations to our app. Its support for disjoint unions is perfectly suited for describing various Flux action types.

In the next part we’ll explore how we can add types to connect function from react-redux.

All code is available on Github, take a look at commit-by-commit history to see how Flow can be adopted incrementally. For a bigger example check out the open-sourced F8 app.