Eradicate Runtime Errors in React with Flow

Flow is pretty awesome.

Oh, more details… Ok, so, Flow is great for large or growing codebases that could use a hand in the static analysis department. Even outside of the context of React (heh), Flow shines by ensuring type safety across modules; that is, it allows you to annotate variables, functions, et al. with type annotations that Flow can parse and use to spot errors in your application before they happen.

Why Flow? Our team settled on Flow because we already had multiple fairly substantial React codebases with complicated-enough build systems that we wanted to avoid modifying (the build systems), which pretty much discounted TypeScript, at least for the immediate gains we were seeking. Luckily, Flow gave us the opportunity to incrementally add types to our applications with a simple /* @flow */ pragma and little else.

If you haven’t given it a chance yet I strongly advise giving it five minutes. In fact, despite balking at some of the syntax at first, the first time I saw a Property cannot be accessed on possibly null or undefined value on a Maybe Type I pretty much spammed our team’s Slack channel in excitement.

It was, as the thesis states, awesome.

Type Safety and React

Unlike what some recent rumors may have implied, PropTypes are not going anywhere, at least any time soon. However, that doesn’t mean they’re perfect; there are a number of places where they fall short, notably:

Flow solves all of these concerns and is an invaluable addition to the typical workflow, perhaps as a pre-commit hook or as part of your CI process. But enough with the words already, let’s write some code. Everyone is rightfully sick of TodoMVC, so we’ll change it up by doing some TodoMVC with Flow.

/* @flow */
import React from 'react'
import autobind from 'autobind-decorator'
import TodoListItem from './TodoListItem'

type Props = {
  todos: Array<Object>,
}
class TodoList extends React.Component {
  props: Props;

  // Yes, I could use an arrow function, I get it.
  @autobind
  _handleCompleteTodo(id: string): void {
    // do something to mark this todo as complete
  }

  render(): React.Element {
    return (
      <ul>
        {this.props.todos.map((todo) => (
          <li key={todo.id}>
            <TodoListItem
              todo={todo}
              onComplete={() => this._handleCompleteTodo(todo.id)}
            />
          </li>
        ))}
      </ul>
    )
  }
}

Ok, back to it. So this is pretty cool, we’ve added some fairly basic type annotations to our <TodoList /> component, where we defined the property todos as an array of objects and annotated the component’s two methods. I’m not going to cover the fact that Flow will type check these properties, as that’s Flow’s fundamental offering, so let’s continue by taking a look at <TodoListItem />:

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

type Props = {
  todo: Object,
  onComplete: Function,
}
class TodoListItem extends React.Component {
  props: Props;

  render() {
    return (
      <div>
        <h3>{this.props.todo.text}</h3>
        <button onClick={this.props.onComplete}>Complete</button>
      </div>
    )
  }
}

Not bad, our type of todo (Object) fits right in with what <TodoList /> expects for todos (an array of Objects), but we can take this a step further. As many of you will recognize, this looks very similar to PropTypes.shape:

// ...
type Props = {
  todo: {
    id: string,
    text: string,
    completed: boolean,
  },
  onComplete: Function,
}
// ...

We’ve now defined the shape of a todo inline, however, we’re not done yet. We can improve this by creating (and exporting!) a reusable Type Alias:

// ...
export type Todo = {
  id: string,
  text: string,
  completed: boolean,
}

type Props = {
  todo: Todo,
  onComplete: Function,
}
// ...

This type alias can then be imported from other components or even made into an app-wide declaration if you find yourself using it often enough. So, how does Flow help us? Well, let’s tweak our <TodoListItem />’s render method to display a bit more information:

// Our updated Todo:
export type Todo = {
  id: string,
  text: string,
  completed: boolean,

  // the `?` signifies that this property might not exist.
  assignedUser?: {
    id: string,
    name: string,
  },
}

// and the new and improved render method:
render() {
  const { todo } = this.props

  return (
    <div>
      <h3>{todo.text}</h3>
      <h4>Assigned to: {todo.assignedUser.name}</h4>
      <button onClick={this.props.onComplete}>Complete</button>
    </div>
  )
}

If we run flow check on this, we get an error:

Flow can spot accessors on nullable types

Flow recognized that assignedUser might not exist and correctly fails the analysis. This sort of safeguard is invaluable as applications grow since it’s no longer left to the developer to account for these scenarios; even the best of us can forget these things.

Here’s how a fixed version of this component might look:

// ...
class TodoListItem extends React.Component {
  props: Props;

  render() {
    const { todo } = this.props

    // a bit roundabout, but y'know, clarity...
    let assignedToContent
    if (todo.assignedUser) {
      assignedToContent = `Assigned to: ${todo.assignedUser.name}`
    } else {
      assignedToContent = 'This is not assigned to anyone!'
    }

    return (
      <div>
        <h3>{todo.text}</h3>
        <h4>{assignedToContent}</h4>
        <button onClick={this.props.onComplete}>Complete</button>
      </div>
    )
  }
}

Function Properties

Now let’s say you are, rightfully, a fan of the React ESLint rule jsx-no-bind. In this case you will probably convert your <TodoList /> component to look something more like this:

// ...
import type { Todo } from './TodoListItem'

type Props = {
  todos: Array<Todo>,
}
class TodoList extends React.Component {
  props: Props;

  @autobind
  _handleCompleteTodo(id: string): void {
    // do whatever it was we were doing before
  }

  render(): React.Element {
    return (
      <ul>
        {this.props.todos.map((todo) => (
          <li key={todo.id}>
            <TodoListItem
              todo={todo}
              onComplete={this._handleCompleteTodo} // <-- important
            />
          </li>
        ))}
      </ul>
    )
  }
}

Notice that we are no longer creating a function with each iteration of this.props.todos. Whether or not we do this with .bind or an arrow function is beside the point; the fact is, we are now delegating this responsibility to <TodoListItem />. Let’s dive in and see what that the modified component would look like (removing some demo code for succinctness):

// ...
type Props = {
  todo: Todo,
  onComplete: Function,
}
class TodoListItem extends React.Component {
  props: Props;

  // Ok, so when a user clicks the button to complete this
  // todo, we invoke the `onComplete` callback provided by
  // our parent component, passing it our todo so it knows
  // which todo was completed.
  @autobind
  _handleComplete(): void {
    this.props.onComplete(this.props.todo)
  }

  render(): React.Element {
    return (
      <div>
        <button onClick={this._handleComplete}>Complete</button>
      </div>
    )
  }
}

Looks great, except there’s a bug, which is… not so great. The <TodoList /> component’s _handleCompleteTodo method is still expecting an argument id of type string, not the entire todo object. What we have right now is still fairly equivalent to what is possible with regular PropTypes, meaning Function does not say anything about its signature or return type. Luckily, we can go a step further with Flow.

Since we know exactly how our component plans to call onComplete, we can explicitly define that signature in the Props type definition, which will require any implementers to abide by that signature. Check it out:

type Props = {
  todo: Todo,
  onComplete: Function,
}

// "Let's kick it up a notch!" - Emeril Lagasse

type Props = {
  todo: Todo,
  onComplete: (todo: Todo) => any,
}

At this point, we’ve defined the property onComplete to be a function that takes a Todo as its first and only argument. We don’t actually care what that function returns, it’s simply a callback, and those guys are super forgettable, so we specify any as the return type. With this in place, let’s run flow check and see what we get:

Flow can spot errors in function signatures across modules

If you’re incredibly attentive you may notice that this error was spotted before onComplete was ever even used in the child component, it was caught as the onComplete prop was provided to <TodoListItem /> from <TodoList />.

Pairing with Flux

Our most recent applications are built on top of the popular Redux library, which Flow has complemented incredibly nicely, though it would presumably do the same with any Flux library, or even any non-Flux architecture… well, you get the point. We’ll take a brief look at two areas in particular that we’ve seen quick and easy benefits with:

Action Creators

One easy place to start adding Flow type annotations to is your action creators. If you’re familiar with Flux Standard Action, you could create a global declaration for this interface:

type Action = {
  type: string,
  payload?: any,
  error?: boolean,
  meta?: any,
}

Granted, FSA is actually fairly weak in its type constraints, so you could enforce this more strictly as it suits you and your team. Still, this helps when creating your Flux modules:

export const completeTodo = (id: string): Action => ({
  type: TODO_COMPLETE, // constant declared above
  payload: {
    id,
  },
})

If you were to return an object that didn’t adhere to the Action type Flow would learn about it, hunt you down, and kill you fail your CI build.

State Shapes

The obvious benefit here is that we can create a type alias for each reducer’s shape, allowing Flow to analyze that each action handler in a reducer produces a conforming state shape. This is fairly trivial once you’ve annotated a function or two, since reducers are just plain functions, but there’s an even bigger benefit to be had.

Say we have a reducer responsible for some search state, perhaps a form with a handful of fields, nothing crazy. When the filters are submitted (in this example), we update the store to cache that state for returning visitors to provide some decent UX. This is super high-level, so please forgive me for not going through it in its entirety.

// your filter state shape (presumably in a redux module)
export type MySearchFilterState = {
  selectedUserIds: Array<string>,
  searchFromDate: number,
  searchToDate: number,
  hasSomeFilterEnabled: boolean,
}

// consumer component (parent smart component):
import type { MySearchFilterState } from 'my/redux/module'

class MySmartViewOrSomething extends React.Component {
  _handleSubmitFilters(filters: MySearchFilterState): void {
    // dispatch an action to cache these filters
  }

  render(): React.Element {
    return (
      // a bunch of stuff...
      <MyFormComponent onSubmit={this._handleSubmitFilters} />
    )
  }
}

// consumer component's child
type Props = {
  filters: MySearchFilterState,
  onSubmit: Function,
}
class MyFormComponent extends React.Component {
  // ...
  _handleSubmitFilters(): void {
    this.props.onSubmit({
      selectedUserIds: [...],

      // "The Witch" is pretty good, you should see it.
      someBadProperty: 'Black Phillip',
    })
  }
  // ...
}

The amazing part about this setup is that if <MyFormComponent /> decides to not abide by the shape of the filters (as it fails to do above), Flow will catch it without ever having to run the application. It will not only validate the shape of those filters, but will follow them through their entire lifecycle, far outside of the reach of component properties. Whether the error is an additional/malformed property, a missing required property, or an incorrect type, Flow has you covered. If you ever change the shape of your state, Flow will help you confirm that the rest of the application continues humming along.

Flow isn’t perfect, it doesn’t catch everything just yet and sometimes requires you to write your code in a way that’s a bit more explicit, but it’s a worthy tradeoff, especially considering the fact that it’s actively maintained and constantly improving. It can make guarantees about your codebase that PropTypes would only be able to offer as uncaught exceptions in production.

Seriously, Flow is awesome.