Supporting React.forwardRef and Beyond

Jordan Brown
Dec 13, 2018 · 5 min read

In Flow v0.89.0, we’re releasing , a new type that we use to model and other React components. This new representation is compatible with our current React typings, and it also fixes a few bugs with our previous representation. You can find documentation for here, and instructions for using it in HOCs here. This blog post will briefly introduce , how to use in Flow, and how you can make your existing HOCs safer using

Background

Throughout the rest of this post, I will refer to the “instance type” of a component. For a function component, the instance type is and for a class component the instance type is an instance of the class. When you pass a ref to a component, the field has the type of the instance of the component.

const ref: {current: null | HTMLButtonElement} =
// ^^^ ref ^^^^^^^^^^Instance type
React.createRef<HTMLButtonElement>();
<button ref={ref} />;
// ^^Object with a current field

For more information on refs and instances, I recommend reading the React docs.

The introduction of some newer React APIs required that we rethink how we model React components in Flow. In our new model, we emphasize the config of a component instead of the props. The config type of a component is the type of the props with all of the properties specified in defaultProps marked optional. Take this example:

type Props = {foo: number, bar: number};
type DefaultProps = {foo: number};
class Component extends React.Component<Props> {
static defaultProps: DefaultProps = {foo: 3};
}
// Since `foo` is specified in the defaultProps, `foo` is
// optional in the config of the component.
// Therefore, the type of the config of `Component` is:
type Config = {foo?: number, bar: number};

This new representation also finds many errors we were missing before.

Another goal of our design was to provide an abstraction that would help Flow stay more in sync with React changes. Though was primarily designed to address , we’ve found that it can also be used to type , , , , , and . We hope that it will also be able to model new React components in the future.

To start getting some of the benefits of the new representation, you won’t need to make any changes to your codebase (other than upgrading your Flow version). In 0.89.0, we replace the definition of to be an alias for instead of a union of function and class components. This change is likely to find errors in your code. Most of these errors will be missing required props and not matching the types specified in . Be sure to use the flag if any errors seems particularly weird. gives more information on errors with union types, which we use to model some of the React types. We also have some work in the pipeline to improve some of the error messages related to react, so stay tuned!

Understanding React.AbstractComponent

Let’s briefly touch on how works. Take the following example:

//@flowtype Props = {foo: number, bar: number};
type DefaultProps = {foo: number};
class ClassComponent extends React.Component<Props> {
static defaultProps: DefaultProps = {foo: 3}
}

has props of type and of type , so its config type is . A element has an instance type of . Here’s how that interacts with :

(ClassComponent: React.AbstractComponent<
{foo?: number, bar: number},
ClassComponent,
>); // This is safe!

Note that is optional in the config, since it is specified in the .

Since you may need to annotate a in your code but probably only have the type written out, we provide a utility to calculate a type from and .

(ClassComponent: React.AbstractComponent<
React.Config<Props, DefaultProps>, // Calculate the config
ClassComponent,
>); // This is safe!

React.forwardRef Support

With our newly-landed support for , you can safely forward refs in React components and always be sure that the instance type of the component is what you expect it to be.

You’ll notice that if you try to export a component without any annotations that Flow will ask you for one:

//@flowconst React = require('react');type Props = { foo: number };class Button extends React.Component<Props> {}// Error, missing annotation for Config and Instance in forwardRef
module.exports = React.forwardRef(
(props, ref) => <Button ref={ref} {...props} />,
);

[Try-Flow]

Like you would with a class or function component, we recommend that you use a type alias to specify the Props for your component and use that alias as a type argument to .

//@flowconst React = require('react');type Props = { foo: number };class Button extends React.Component<Props> {}module.exports = React.forwardRef<
Props,
Button,
>((props, ref) => <Button ref={ref} {...props} />,
);

[Try-Flow]

If your component that takes the ref is a class, then you can use the instance of the class to annotate the instance (as in the example above).

If your component is itself a , use .

//@flow 
const React = require('react');
function wrapInDivPreserveInstance<Config: {}, Instance>(
Component: React.AbstractComponent<Config, Instance>
): React.AbstractComponent<Config, Instance> {
return React.forwardRef<Config, Instance>(
(props, ref) => <div><Component ref={ref} {...props} /></div>,
);
}

If your component is a built-in, like or use the type defined in the libdef, like or .

//@flow
const React = require('react');
type Props = { foo: number };module.exports = React.forwardRef<Props, HTMLDivElement>(
(props, ref) => <span><div ref={ref} /></span>,
);

If your component is a function, you probably want to rethink your use of , since the instance type of a function component is .

Using React.AbstractComponent for Higher-Order Components

Using , we can get more precision when checking React components and HOCs.

removes the need to use in HOCs. Let’s take a look at a common signature for HOCs and see how we can change it to use for more safety.

function HOC<TProps: {}, TComponent: React.ComponentType<TProps>>(
Component: TComponent
): React.ComponentType<React.ElementConfig<TComponent>> {
// ...
}

This HOC takes a and returns a component with the same config. Well, since isn’t necessary anymore, we can get rid of it:

// We can completely get rid of React.ElementConfig in this type!
function HOC<Config: {}>(
Component: React.ComponentType<Config>,
): React.ComponentType<Config> {
// ...
}

But is a type alias for . We can get even more precise types if we use directly.

Many HOCs wrap a component in a function, so let’s take a look at that case in more detail:

function HOC<Config: {}>(
Component: React.ComponentType<Config>,
): React.ComponentType<Config> {
return props => <Component {...props} />;
}

As a first pass, we can just replace with to get as the instance type instead of :

function HOC<Config: {}>(
Component: React.AbstractComponent<Config>,
): React.AbstractComponent<Config> {
return props => <Component {...props} />;
}

But we know the instance type of a function is always , so we can get even more precise by using that explicitly in the return type:

function HOC<Config: {}>(
Component: React.AbstractComponent<Config>,
): React.AbstractComponent<Config, void> {
return props => <Component {...props} />;
}

Going through this exercise on your own HOCs might reveal missing props errors and places where you use a in an unexpected way. Try it out and see what you uncover!

Conclusion

This new type comes with many benefits, including:

  • More supported react features
  • More precise type checking
  • More expressive HOCs

… and all the while it doesn’t require any codemods to start using it. We’re excited to see what you build with it!

Flow

The official publication for the Flow static type checker…

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store