ReactJS + FlowJS in Nuclide Tutorial 2018

Nick Hartunian
7 min readMay 18, 2018

--

I want to spend a little longer than usual explaining who this tutorial is for. The target audience is readers who were at roughly the level of knowledge I was at two weeks ago:

  • You’ve coded projects in React before.
  • You see why the type-checking introduced by Flow would be helpful.
  • But a ton of issues setting up the development environment and writing code without Flow nagging you keep popping up.
  • Sometimes, you try copying and pasting code from the official websites, and that doesn’t even work.

Yeah, the new age of JavaScript can feel a little like the Wild West sometimes. If you just want to see the source code of a project that works with the latest versions of everything as of May 16, 2018, here you go.

But it’s moments like these, when I step back and make a panda bear factory, that I really learn a lot.

I’m going to be specific about how I set up the project but stay one level above the minutia of version numbers and copying the official install instructions for each package. What’s more important are the steps I took to get things working that weren’t obvious.

First, add Nuclide to your Atom IDE if you haven’t already. The automatic Flow server will be super useful, and you can always disable the package later.

Open your project in Nuclide after your create it with:

create-react-app panda_factory

Then follow the instructions at the Flow site to add it to the project. I used the Yarn + Babel options.

Alright, here’s where we go a bit off the beaten path.

  1. Check off “Use the Flow binary included in each project’s flow-bin” under: Settings -> Packages -> Nuclide -> Nuclide-flow (without this, the Flow server might not know what to serve)
  2. In .babelrc, add “react” to the presets array (without this, the babel command yarn run babel src/ -- -d lib/ won’t know how to handle both Flow and React)
  3. In the project root, execute the command yarn run flow init (without this, you won’t get a .flowconfig file, which was necessary for Flow to run in my case)

Add to the top of your index.js file:

// @flow

Did nothing happen? I needed to kickstart my Flow server by running

yarn run flow

Still nothing? Try restarting Nuclide and writing some atrociously wrong type-checked code like:

let x: number = “actually a string”

Okay, hopefully things are working now. And you should see Flow is mad at the default code in index.js.

Yeah, Flow is strict enough to start taking shots at the boilerplate in index.js. You need to change the body of the file from:

ReactDOM.render(<App />, document.getElementById('root')
registerServiceWorker()

to:

const root = document.getElementById('root')if (root)
ReactDOM.render(<App />, root)
registerServiceWorker()

Flow was concerned we might be rendering on an HTML element before confirming it exists.

Alright, now let’s structure our project a little. Under “src”, create “components” and “imgs” folders.

Please fill the imgs folder with the images here (courtesy of pixabay.com).

In components, please make a Panda.js file. Start writing what will become a classic header for your JavaScript:

// @flow
import React from 'react'

Okay, now we get to do some awesome Flow stuff. Let’s specify what kind of props our panda will take:

type Props = {
id: number,
name: string,
age: number,
hobby: string,
img: string
}

And let’s create a functional React component with those props:

const Panda = (props: Props) => (
<div>
<p>Name: {props.name} | Age: {props.age} | Hobby: {props.hobby}</p>
<img src={require("../imgs/panda"+props.img+".jpg")} alt="most likely a panda" />
</div>
)

Functional React components typically come in handy when there’s no state involved in the component, and you want to avoid writing a full-blown class.

But, wait! Flow is annoyed we’re not using a “string literal” in our require function. A string literal means that there is no variability in the string. "I like hamburgers" is a string literal. "I would like to eat " + x + " hamburgers" is not a string literal because there is some variability in what the string will turn out to be.

Flow wants us to only use string literals so that it can check to make sure the resource we’re “requiring” actually exists at a definitive location. In this case, we actually need there to be variability in what image the panda component uses. Therefore, to politely ask Flow to ignore the need for string literals in require calls, in .flowconfig (at the root of the project), add the following under [options]:

module.ignore_non_literal_requires=true

Finally, at the bottom of our Panda.js file, let’s export our work:

export type { Props as PandaType }
export default Panda

That export type syntax took a few tries for me to figure out, but now we can allow other files to import “PandaType” rather than “Props,” which doesn’t make sense out of context.

Let’s move up a level to our App.js file and actually get some pandas on the screen. To start the file:

// @flow
import React, { Component } from 'react'
import logo from './imgs/logo.svg'
import './App.css'
import Panda, { type PandaType } from './components/Panda.js'

Notice the fancy syntax to import a type as well as the Panda component from the Panda.js file we just made. In this case, the word type is inside the curly brackets rather than outside like it was when exporting.

Now let’s set up our props and state types:

type Props = {||}type State = {
pandas: Array<PandaType>,
count: number
}

Yeah, if you remember index.js, the App component doesn’t actually take any props. So, why is Props there at all? Well, when we write the component class, we’ll see that it has spots for props and state. After vigorous amateur experimentation, I found you can’t really get the state type without some placeholder for the props type. Here’s what I mean:

class App extends Component<?Props, State> {
constructor(props: ?Props) {
super(props)
this.state = {pandas: [], count: 0}
this.getUpdatedCount = this.getUpdatedCount.bind(this)
this.addNewPanda = this.addNewPanda.bind(this)
}

I tried to indicate that Props is a placeholder with the ?, which isn’t actually necessary for the code to run. {||} is a Facebook convention for empty object. The pipes mean “exact value” (i.e. nothing else can be added unlike other Flow types).

Another note that didn’t seem to be in the docs is that I got errors if I didn’t mark the props parameter in the constructor as the Props type:

constructor (props: Props) { ... }

We also bind two functions to the class in the constructor. Here’s a quick JavaScript refresher about why this is necessary since I always seem to forget (seriously I failed an interview because of this):

If we want to call this.getUpdatedCount() from within the App class, JavaScript by default won’t be able to find the function. You’ll get a dreaded undefined. That’s because even code inside the class probably has its this bound to the global context rather than the class’s context. Our getUpdatedCount function doesn’t exist out there! So, we have to bind getUpdatedCount to this class’s this specifically. Hope that wasn’t too confusing. There are auto-bind packages out there that will do this for you, but I prefer to do it manually for a small number of class functions.

Okay, now let’s write those getUpdatedCount and addNewPanda functions. These aren’t actually necessary right now, but let’s get them out of the way, so I don’t have to instruct you to jump up and down the file writing them later in Part II. In real life, it’s typically better to write functions one step at a time as they become necessary.

getUpdatedCount = (): number => {
const newCount = this.state.count + 1
this.setState({count: newCount})
return newCount
}
addNewPanda = (panda: PandaType) => {
const newPandas = [...this.state.pandas, panda]
this.setState({pandas: newPandas})
}

Why are we using arrow functions inside the class when we did constructor the normal way? This actually happens to be a workaround to allow us to bind the functions to the class in the constructor without Flow getting mad.

If you remember when we did that important binding, we were actually setting one of each function’s property’s with an assignment operator (=). That’s a big no-no for Flow, which mandates that functions must be read-only. This is one of the key principles that allows Flow to work. It would make the lives of the developers of Flow a lot harder if they also had to account for the fact that people might be changing the properties of functions.

So we skip around this issue by using an arrow function instead of directly using a function. Note that first workaround mentioned in the linked GitHub issue with (this:any) error’d for me.

Okay, let’s finish App.js:

render() {
const pandaComponents = []
for (let i=0; i<this.state.pandas.length; i++)
pandaComponents.push(<Panda key={i} id={this.state.pandas[i].id}
name={this.state.pandas[i].name} age={this.state.pandas[i].age}
hobby={this.state.pandas[i].hobby} img={this.state.pandas[i].img} />)
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Panda Factory</h1>
</header>
<br />
<div className="App-intro">
{pandaComponents}
</div>
</div>
)
}
}
export default App

Try filling the initial pandas array state declared in the constructor with some dummy data following the format of our PandaType. This will put some pandas on the screen! Example:

{id: 0, name: "Todd", age: 5, hobby: Dance Dance Revolution, img: "1"}

Also, I tweaked App.css a bit:

.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 200px;
}
.App-header {
background-color: #222;
height: 350px;
padding: 20px;
color: white;
}
.App-title {
font-size: 2em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

Try running yarn start or npm start .

I live for these moments — when all the JavaScript is working, and my screen is filled with pandas.

In Part II, we’re going to cover synthetic events, refs, and React children with Flow. Translation: we’ll make a small form where a user can specify what pandas they want to create and construct a container component.

See you there!

--

--