My first novel is available!

Read Singular

React 15.5 Components: Part 2

Similar to my last post on converting to ES6 Classes, this will cover the “recipe” for moving from prop-types in a React component to Flow, a static type checker for Javascript.

The basic idea is rather than getting warnings in your browser, wouldn’t it be nice to get them in your editor? Not just that, but very extensive warnings that will ostensibly prevent run-time bugs. Sounds promising.

It takes a good bit of work and the pay-off is directly related to fully migrating component hierarchies. While that is more work than I can put into a blog post, let’s convert an “interesting” component and then reference some gotchas.

Install Flow

First we need to install Flow itself. It is recommended that you install Flow per project, which is pretty simple:

yarn add --dev babel-cli babel-preset-flow
yarn add --dev flow-bin

Follow the instructions for your package manager and compiler. I’m on Yarn and Babel, so I followed their suggestions.

Before we go any further, I should come clean on something. I’m doing this configuration for a Rails app with React components (under /app/assets/javascripts), so my config might be a little different than a pure JS app.

As such, my .flowconfig looks like:

[ignore]

[include]

[libs]

[options]
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=app/assets/javascripts

Yes, the .flowconfig is in a super weird format but I read that they working on something better. Breaking this down, you can see that I have ./node_modules in my Rails app, but that my actual javascript root is in ./app/assets/javascripts/.

This config is necessitated by the way Flow treats its .flowconfig. It assumes whatever directory you put it in is the “root” of your project. Works great for javascript projects, not so well for a react-rails combo.

And my .babelrc contains:

{
  "presets": ["es2015", "flow", "react"]
}

Makes sense really.

If you run yarn run flow from the command line, it should give a nice false positive:

No errors!

Until we start annotating files, it’s not technically looking at anything.

Flow in Atom

While running from the command line is an option, I’d rather have my editor integrate more tightly. Several options exist but I eventually settled on Nuclide. The integrated auto-complete is very handy.

To get your flow remarks to play nicely with eslint, I recommend adding the eslint-plugin-flowtype package to your devDependencies in package.json.

yarn add --dev eslint-plugin-flowtype

Now, if everything is setup correctly, you should be able to get Flow errors in the Nuclide diagnostics panel (where the rest of the eslint stuff comes through).

PropTypes Component

Let’s look at what we’re trying to update:

import React from 'react'

const { PropTypes } = React

class NiftyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      items: []
    }
    this.handleClick = this.handleClick.bind(this)
  }

  render() {
    return (
      <div>
        <a onClick={this.handleClick}>Click Me!</a>
        <a onClick={e => this.props.onClick()}>
      </div>
    )
  }

  handleClick(event) {
    // something happens
  }
}

NiftyComponent.propTypes = {
  onClick: PropTypes.func,
  values: PropTypes.arrayOf(PropTypes.number)
}

Hey, that looks like the nifty component from our last blog post! Indeed it is, let’s bring it up to Flow!

Flow’d Component

Before you attempt any of this, you really need to spend an afternoon reading everything in the Flow docs. If it’s your first time with a type system, you really will have a lot to incorporate into your mindset. If you’ve done this a few times, you already know the objective: specificity to eliminate a category of bugs.

That said, you have to be specific! Thankfully Flow will complain if you pick an Event when it’s really a SyntheticEvent or KeyboardEvent, so spend the time to get it exactly right. Superclasses are garbage for our purposes here.

Let’s see what it looks like when it’s fully converted:

// @flow
import React from 'react'

type Props = {
  onClick: (event: Event) => void,
  values: Array<number>
}

class NiftyComponent extends React.Component {
  props: Props
  state: {
    items: array<number>
  }
  constructor(props: Props) {
    super(props)
    this.state = {
      items: []
    }
  }

  render() {
    return (
      <div>
        <a onClick={e => this.handleClick(e)}>Click Me!</a>
        <a onClick={e => this.props.onClick(e)}>
      </div>
    )
  }

  handleClick(event: Event) {
    // something happens
  }
}

See that comment at the very top? That tells Flow to consider this file. You must have it!

PropTypes

Since we are trying to focus on just the prop-types “deprecation”, let’s zoom in on just that for now. The other pieces of detail might help your thrashing about as you get started.

Rather than having:

NiftyComponent.propTypes = {
  onClick: PropTypes.func,
  values: PropTypes.arrayOf(PropTypes.number)
}

You move all that crap to the top of the file as a type Props and replace the PropTypes with the corollary in Flow-land.

type Props = {
  onClick: (event: Event) => void,
  values: Array<number>
}

I tend to like the Flow syntax for these sorts of things (put a ? in front of optional types or values, | between options). Read the docs for more details.

As a side note, you can skip the constructor entirely if all it would contain is the super(props)

Libraries

More than likely, you won’t have much trouble with your own code, and if you do, it will be a super arcane error message like, “covariant property incompatible with contravariant use” or “object literal not compatible with object type” (both of which make sense when you dig into what is really going on over the course of an hour or two). There are solutions for these. Keep learning and clicking on the little chain-links in your editor hints to get the details on what is expected from Flow.

Case in point, which will likely be my next post. I was converting jQuery.ajax to fetch and kept getting errors about my method POST which I had declared to be a string.

Well, if you note my feedback at the beginning…be specific…you’d know that that is NOT a string but actually a MethodType according to the library in question. You must use that libraries types, which means you need to look for a .js.flow or a Flow-Typed lib-def to help you out.

On day one of fiddling with this stuff, I would have thought that last sentence was in Hebrew (usually people say Greek, but I studied both Greek and Hebrew for years and I still can’t remember my Hebrew). Bottom line, you have to immerse yourself in this stuff and then it will magically make sense.

Have fun!

Published under programming, twitter