Supporting React.forwardRef and Beyond

Jordan Brown
Flow
Published in
5 min readDec 13, 2018

--

In Flow v0.89.0, we’re releasing React.AbstractComponent, a new type that we use to model forwardRef 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 React.AbstractComponent here, and instructions for using it in HOCs here. This blog post will briefly introduce React.AbstractComponent, how to use React.forwardRef in Flow, and how you can make your existing HOCs safer using React.AbstractComponent.

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 void and for a class component the instance type is an instance of the class. When you pass a ref to a component, the current 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 React.AbstractComponent was primarily designed to address forwardRef, we’ve found that it can also be used to type Fragment, Suspense, StrictMode, ConcurrentMode, memo, and lazy. 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 React.ComponentType to be an alias for React.AbstractComponent 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 defaultProps not matching the types specified in Props. Be sure to use the --show-all-branches flag if any errors seems particularly weird. --show-all-branches 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 React.AbstractComponent 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}
}

ClassComponent has props of type Props and defaultProps of type DefaultProps, so its config type is {foo?: number, bar: number}. A ClassComponent element has an instance type of ClassComponent. Here’s how that interacts with React.AbstractComponent:

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

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

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

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

React.forwardRef Support

With our newly-landed support for forwardRef, 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 forwardRef 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 forwardRef.

//@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 React.AbstractComponent<Config, Instance>, use Instance.

//@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 div or button use the type defined in the dom.js libdef, like HTMLDivElement or HTMLButtonElement.

//@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 forwardRef, since the instance type of a function component is void.

Using React.AbstractComponent for Higher-Order Components

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

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

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

This HOC takes a TComponent and returns a component with the same config. Well, since React.ElementConfig 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 React.ComponentType<Config> is a type alias for React.AbstractComponent<Config, any>. We can get even more precise types if we use React.AbstractComponent 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 React.ComponentType with React.AbstractComponent to get mixed as the instance type instead of any:

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 void, 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 ref 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!

--

--