Supporting React.forwardRef and Beyond
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!