ReactJS + FlowJS in Nuclide Tutorial 2018 Part II

Nick Hartunian
6 min readMay 18, 2018

--

So we have some type-checked pandas.

If you don’t have type-checked pandas, you can get some in Part I of this tutorial.

If you don’t have type-checked pandas, and you don’t care to obtain any type-checked pandas, you should probably go.

…because with Flow’s synthetic event checking, React refs, and enforcing children element types, our panda factory is about to go fully operational.

In the components folder, create the file PandaCreateForm.js. Let’s start with the basics:

// @flow
import React, { Component } from 'react'
import { type PandaType } from './Panda.js'
type Props = {
addNewPanda: (panda: PandaType) => void,
getUpdatedCount: () => number
}

The only new bits here are the types we declared for this component’s props. We demand that this component will receive an addNewPanda function that has a PandaType as a parameter and doesn’t return anything. We also demand that this component receive a getUpdatedCount function that doesn’t have any parameters and returns a number. If you remember, we wrote two functions just like those in App.js!

Only a few more lines before we can get started on the class:

type State = {
name: string
}
const hobbies = ["Being a panda", "Eating bamboo", "Growling quietly", "Fighting crime",
"Building a tree house", "Rolling around a bit", "Race car driving", "Full-stack development"]

Our state holds a name string that will reflect the status of what’s currently in the text box we’re going to make. The other features of the panda will be randomized, which is where the hobbies array will come into play. I encourage you to take a moment, and give the pandas some of your own hobbies.

Superb, now let’s start the component class:

class PandaCreateForm extends Component<Props, State> {
textInput: ?HTMLInputElement
constructor(props: Props) {
super(props)
this.state = {name: ''}
this.getRandAge = this.getRandAge.bind(this)
this.getRandHobby = this.getRandHobby.bind(this)
this.getRandImg = this.getRandImg.bind(this)
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}

As expected, we have some of the all-important binding explained in Part I, and other familiar things. However, there’s a new piece: textInput: ?HTMLInputElement . What this line basically does is say that within the this context of the PandaCreateForm class, there’s going to be a reference to an HTML input element. The question mark signifies that the element may not be rendered to the screen at the moment.

That will come in handy later, but first, let’s write some fun functions:

getRandAge = (): number => Math.floor(Math.random()*20+1)getRandHobby = (): string => hobbies[Math.floor(Math.random()*(hobbies.length))]getRandImg = (): string => "" + Math.floor(Math.random()*3+1) // hard coded to three imgs available

Once again, we’re using arrow functions, so that Flow will let us bind in the constructor. We also declare the return types of the functions, which is intuitive for these random generator utilities. Tossing :void on every other function might be a tad of overkill, so I didn’t specify their (lack of) return types.

Okay, now for some handlers:

handleSubmit = (event: SyntheticEvent<HTMLButtonElement>) => {
event.preventDefault()
if (!this.state.name) {
alert("This panda has no name.")
return
}
this.props.addNewPanda({
id: this.props.getUpdatedCount(),
name: this.state.name,
age: this.getRandAge(),
hobby: this.getRandHobby(),
img: this.getRandImg()
})
this.setState({name: ''})
if (this.textInput && this.textInput.value)
this.textInput.value = ''
}
handleChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
this.setState({name: event.currentTarget.value})
}

As you can see, we’re using Flow to tell these handler functions what type of event to expect. There are cheat-sheets available for all the event and element types.

Let’s examine handleSubmit. We prevent the default action of the event because submit buttons typically refresh the page after being clicked. If we allowed that, it would erase all our previous pandas. Which would be awful.

We also don’t allow the user to create a panda if there’s nothing in the “name” text box, and double-check to make sure textInput and its value property exist before altering them.

textInput is the React ref we declared earlier that allows us to directly clear the text box after the user clicks submit. This is necessary because clearing the state string we have assigned to observe the text box doesn’t actually alter the contents of the text box.

Also, Flow makes us wrap that text box-clearing logic in an if statement because we declared the ref with ?HTMLInputElement at the top of the class. That means it might not exist, and Flow certainly isn’t going to let you go around mutating properties that don’t exist.

The Flow docs also recommend we use event.currentTarget in the handleChange function instead of the traditional event.target. And, they have a pretty good point.

Alright, let’s finish the file:

render() {
return (
<form onSubmit={this.handleSubmit}>
<label>Desired Panda Name: </label>
<input ref={textInput => (this.textInput = textInput)} onChange = {this.handleChange} type="text" name="name" />
<input type="submit" value="Generate" />
</form>
)
}
}
export default PandaCreateForm

The ref syntax might seem a bit weird, but it basically checks to see if the element the arrow function is inside exists, and, if it does, makes a ref to it that the entire class can access.

Now let’s go back to our App.js in root, and add our new component right below the header:

import PandaCreateForm from './components/PandaCreateForm.js'***
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Panda Factory</h1>
</header>
<br />
<PandaCreateForm getUpdatedCount={this.getUpdatedCount} addNewPanda={this.addNewPanda} />
<div className="App-intro">
{pandaComponents}
</div>

Awesome — enjoy generating some type-checked pandas.

For completeness sake, in App.js, let’s do a little better than using a div to hold our pandaComponents . We’re going to use the React children type-enforcing feature of Flow.

Under the components directory, create PandaList.js. This file is going to be short, so I’m pasting the entire thing here:

// @flow
import React, { type ChildrenArray, type Element } from 'react'
import Panda from './Panda.js'
type Props = {
children: ChildrenArray<Element<typeof Panda>>
}
const PandaList = (props: Props) => (
<div>
{props.children}
</div>
)
export default PandaList

Check out the first import statement. We specifically name two Flow types we want to import. The other way to do this is to make the import statement import * as React from 'react', and then change the code to:

type Props = {
children: React.ChildrenArray<React.Element<typeof Panda>>
}

However, I opted for destructuring the import to maintain consistency with the other files.

The next thing to look at is the Props type we declared above. It basically maintains that the children this component receives (the components that will go between the two div tags) must be Panda components.

Please note that we’re using typeof Panda rather than importing the PandaType because for React children, Flow wants a type associated with the fully-fleshed out component, not just the data the component is based on.

Now, let’s add PandaList to App.js. Here’s the final version of 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'
import PandaCreateForm from './components/PandaCreateForm.js'
import PandaList from './components/PandaList.js'
type Props = {||} // none actuallytype State = {
pandas: Array<PandaType>,
count: number
}
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)
}
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})
}
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 />
<PandaCreateForm getUpdatedCount={this.getUpdatedCount} addNewPanda={this.addNewPanda} />
<PandaList className="App-intro">
{pandaComponents}
</PandaList>
</div>
)
}
}
export default App

And that’s it! Notice how the children panda components go between the PandaCreateForm tags?

I hope you enjoyed making this type-checked panda bear factory and learned about tricky things in React and Flow to watch out for.

The full project code is available here.

Enjoy your week. Enjoy type-checking.

--

--