Type checking state in React components with Flow
Flow is a static analysis tool for JavaScript. It lets you gradually add type annotations, and will check that you adhere to the type constraints you’ve added. I’ve never used this in production code, but here’s what I learned trying to use this to enforce constraints on a React component’s state. Specifically, I wanted to see if Flow would support encoding algebraic data types in JavaScript and help catch mistakes I made when using them.
Here’s a toy app. Its source is on GitHub.
It shows some text , and you can switch between editing and viewing modes. In a real situation, you’d probably use two separate components here, so the branching here isn’t a great example. It’s just where I started with hacking on this, and would love a more real-world example to use in its place if anyone has suggestions. ☺
The important bit here is that the representation of UI state can have a different shape depending on the mode. This is easy to do in JavaScript with object literals, but Flow lets us add on a layer of light type checking on top. This isn’t always useful, and being this rigid with types isn’t always something you want; here I’m just exploring a new option that Flow allows.
First, we’ll define an algebraic data type for this component’s state, using Flow’s type annotations:
// Describe an algebraic data type for the component’s state.
type AppState = ViewingState | EditingState;
type ViewingState = { mode: ‘viewing’ };
type EditingState = { mode: ‘editing’; articleId: number };
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L6
This says that the `AppState` type can be one of two other types, and they’re defined right here. This kind of construction is common in other languages, and works particularly well when paired with destructuring or pattern matching; my own first encounter with the idea was in Scala.
Next, let’s tell Flow that our component needs to adhere to this type. This example is using React 0.13.0-beta.2 and Babel’s browserify transform to transpile ES6 classes and type annotations (see package.json for setup).
class AppComponent extends React.Component {
state: AppState; constructor(props: any, children: any) {
this.state = States.Viewing();
} // This is a shim to be able to attach flow annotations
// and enforce type safety in calls to `setState` within
// this component.
setState(object: AppState) {
super.setState(object);
} ...
}
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L54
One difference in React 0.13 that may look strange is that `getInitialState` has been replaced by a plain property on the instance. The important bit for us here is the `setState` shim that calls the superclass’ `React.Component.setState` method. The generic `setState` supports any object being passed to it, and we want our component to have stronger type constraints and only allow `AppState` types to be passed in, so this tells Flow to enforce that.
So now we’ve got some type safety on calls to `setState`! Check it out:
onEditModeClicked() {
this.setState({
mode: ‘editing-OOPSATYPO’,
articleId: 42
});
}
When running `flow check` on this code, flow can detect that the typo in the string is a bug:
This is pretty awesome, since usually these kinds of mistakes in dynamic languages like JavaScript or Ruby won’t get caught unless you have other test coverage that reveals them.
One other way to improve this situation in dynamic languages is to define functions that generate valid data structures. This might look like this:
// These are helper functions for creating data structure
// describing the component’s state. These are useful in
// general, but with the Flow annotations, we can catch
// mistakes even without this structure.
var States = {
Editing(articleId: number): AppState {
return {
mode: ‘editing’,
articleId: articleId
};
}, Viewing(): AppState {
return {
mode: ‘viewing’
};
}
};
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L12
And then the handler methods become:
onEditModeClicked() {
this.setState(States.Editing(42));
}onViewModeClicked() {
this.setState(States.Viewing());
}
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L67
This is probably still a good idea if you want to be explicit and emphasize correctness. But the cool part of Flow is that it could catch typo mistakes without these kinds of methods, using only those few lines of type annotations.
One more interesting bit, showing where Flow can be even more helpful.
We can also use these type annotations when reading from `this.state`. In this toy example, we want to render different UIs based on the data in `this.state`, and with a little ceremony Flow can help us enforce type safety here as well.
// This method jumps through a hoop to preserve some
// correctness about the type information. It’s
// essentially performing a runtime type check and
// then a cast that would appear unsafe to Flow.
//
// By adding more specific type annotations
// ourselves, Flow can enforce type constraints
// within the methods to enforce that they work
// correctly with specific states.
render() {
var anyState: any = this.state;
switch (anyState.mode) {
case ‘editing’:
var editingState: EditingState = anyState;
return this.renderEditing(editingState);
case ‘viewing’:
var viewingState: ViewingState = anyState;
return this.renderViewing(viewingState);
default:
throw new Error(‘invalid state’ + anyState.mode);
}
}
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L75
This lets us maintain stronger type checks within `renderEditing` and `renderViewing`, like this:
renderViewing(viewingState: ViewingState) {
return dom.div({},
this.renderNavigation(),
‘hello, we are viewing the page!’
);
}renderEditing(editingState: EditingState) {
return dom.div({},
this.renderNavigation(),
dom.div({}, ‘hello, we are editing the page!’),
dom.div({}, ‘and the articleId is: ‘ + editingState.articleId)
);
}
https://github.com/kevinrobinson/flow-adts/blob/master/src/app.js#L109
And Flow will even catch if we make a typo on the `articleId` property name, or if we forget and try to read it when in the viewing mode:
// oops I typoed the property name
renderEditing(editingState: EditingState) {
return dom.div({},
this.renderNavigation(),
dom.div({}, ‘hello, we are editing the page!’),
dom.div({}, ‘and the articleId is: ‘ + editingState.atrcicleID)
);
}
Flow catches this for me:
Conclusion
Adding a basic algebraic data type for the state of a React component worked just fine, and Flow was able to help enforce that I was using it correctly.
Work to gradually add type constraints in dynamic languages is becoming more widespread, with projects like Flow, TypeScript and SoundScript for JavaScript and Typed Clojure for Clojure or ClojureScript. These are interesting lines of work, since they allow new kinds of tradeoffs for web developers. Projects like Flow can help make it more fluid to move between emphasizing speed in early product iterations and enforcing correctness as a product grows and becomes more mature.