Generically-typed React components

3 November 2018

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.

Note: you can see this code on GitHub, and it’s also deployed to Netlify.

One tool I can no longer live without is 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.

// @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:

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:

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 who helped me out when I raised the question of how to do this. Thanks!