I've been using React a lot on some recent projects, and enjoying it. The surrounding community and range of libraries on offer is (unsurprisingly) great, and if you're building a common component then it's often easy to find a well-documented open source version of what you're about to build.
You can see this code on GitHub, and it's also deployed to Netlify.
One tool I can no longer live without is [Flow][flow], a static type checker. It helps me spot errors with passing objects that might be incompatible, accessing variables that might not exist, and a whole host of other common pitfalls.
This weekend, I've been playing around with writing generically-typed components, that is: components where the type definition takes a parameter that is also a type. One of the most obvious examples is a list, so I'll use that as an example.
The types
By using a type parameter, <T>
, we can define a type that
takes an array of items, and a method that renders a single item. At the
moment, think of `T` as a placeholder: we'll fill it in later.
#!javascript
// @flow
import * as React from "react";
// The list itself takes an array of items
// and a function to render each one
type Props<T> = {
items: Array<T>,
renderItem: T => React.Node
};
The component
Here, we're making a component that takes props of type
Props<T>
, and ensuring that the type is an object that
defines a readable `key` property. It doesn't do anything too clever: it
renders the items by calling `map` with our `renderItem` property:
#!javascript
export type ListItem = { +key: string };
export default class GenericList<T: ListItem> extends React.Component<
Props<T>
> {
renderListItem: T => React.Node = (item: T) => {
return <li key={item.key}>{this.props.renderItem(item)}</li>;
};
render() {
return <ul>{this.props.items.map(this.renderListItem)}</ul>;
}
}
Using the component
Once we've defined our `GenericList` component, we can extend it, and define a class that fills in the `T` with a specific type. In this case, we'll make a list of `TodoItem` objects:
#!javascript
export type Task = ListItem & {
title: string,
isCompleted: boolean
};
type Props = {
items: Array<Task>
};
export default class TaskList extends React.Component<Props> {
renderItem = (task: Task) => {
return (
<span>
{task.title} ({task.isCompleted ? "Done" : "Not done"})
</span>
);
};
render() {
return (
<GenericList items={this.props.items} renderItem={this.renderItem} />
);
}
}
And that's about it: our `GenericList` will only typecheck if the objects passed in have a `key` property, and our types will ensure that each item can always be rendered correctly.
Think of the `GenericList` like a contract: given a type `T`, and a function
T => React.Node
, we can render a list of any object with `key`
property, and Flow will type check it all.
And that's it... I've written lists, tables, typeahead searches and more, and doing so has felt pleasant. A big thanks has to go to [William Chargin](https://github.com/wchargin) who helped me out when I [raised the question](https://github.com/facebook/flow/issues/7145) of how to do this. Thanks!