Higher-order Components
Higher-order components are discouraged in modern React code and will not be updated for Component Syntax. Consider using a hook to accomplish your task instead.
A popular pattern in React is the higher-order component pattern, so it's important that we can provide effective types for higher-order components in Flow. If you don't already know what a higher-order component is then make sure to read the React documentation on higher-order components before continuing.
You can make use of the Component Types to annotate your higher order components.
The Trivial HOC
Let's start with the simplest HOC:
1import * as React from 'react';2
3function trivialHOC<Config: {...}>(4 Component: component(...Config),5): component(...Config) {6 return Component;7}This is a basic template for what your HOCs might look like. At runtime, this HOC doesn't do anything at all. Let's take a look at some more complex examples.
Injecting Props
A common use case for higher-order components is to inject a prop. The HOC automatically sets a prop and returns a component which no longer requires that prop. For example, consider a navigation prop. How would one type this?
To remove a prop from the config, we can take a component that includes the prop and return a component that does not. It's best to construct these types using object type spread.
1import * as React from 'react';2
3type InjectedProps = {foo: number}4
5function injectProp<Config: {...}>(6 Component: component(...{...Config, ...InjectedProps, ...})7): component(...Config) {8 return function WrapperComponent(9 props: Config,10 ) {11 return <Component {...props} foo={42} />;12 };13}14
15function MyComponent(props: {16 a: number,17 b: number,18 ...InjectedProps,19 ...20}): React.Node {}21
22const MyEnhancedComponent = injectProp(MyComponent);23
24// We don't need to pass in `foo` even though `MyComponent` requires it:25<MyEnhancedComponent a={1} b={2} />; // OK26
27// We still require `a` and `b`:28<MyEnhancedComponent a={1} />; // ERRORincompatible-typeCannot create MyEnhancedComponent element because property b is missing in props [1] but exists in object type [2].Preserving the Instance Type of a Component
Recall that the instance type of a function component is void. Our example
above wraps a component in a function, so the returned component has the instance
type void.
import * as React from 'react';
type InjectedProps = {foo: number}
function injectProp<Config>(
Component: component(...{...Config, ...InjectedProps})
): component(...Config) {
return function WrapperComponent(
props: Config,
) {
return <Component {...props} foo={42} />;
};
}
// A class component in this example
class MyComponent extends React.Component<{
a: number,
b: number,
...InjectedProps,
}> {}
const MyEnhancedComponent = injectProp(MyComponent);
// If we create a ref object for the component, it will never be assigned
// an instance of MyComponent!
const ref = React.createRef<MyComponent>();
// Error, mixed is incompatible with MyComponent.
<MyEnhancedComponent ref={ref} a={1} b={2} />;
We get this error message because component type doesn't declare the ref prop,
so it is treated as React.RefSetter<void>. If we wanted to preserve the instance type
of the component, we can use React.forwardRef:
import * as React from 'react';
type InjectedProps = {foo: number}
function injectAndPreserveInstance<Config: {...}, Instance>(
Component: component(ref?: React.RefSetter<Instance>, ...{...Config, ...InjectedProps})
): component(ref?: React.RefSetter<Instance>, ...Config) {
return React.forwardRef<Config, Instance>((props, ref) =>
<Component ref={ref} foo={3} {...props} />
);
}
class MyComponent extends React.Component<{
a: number,
b: number,
...InjectedProps,
}> {}
const MyEnhancedComponent = injectAndPreserveInstance(MyComponent);
const ref = React.createRef<MyComponent>();
// All good! The ref is forwarded.
<MyEnhancedComponent ref={ref} a={1} b={2} />;
Exporting Wrapped Components
If you try to export a wrapped component, chances are that you'll run into a missing annotation error:
1import * as React from 'react';2
3function trivialHOC<Config: {...}>(4 Component: component(...Config),5): component(...Config) {6 return Component;7}8
9type Props = Readonly<{bar: number, foo?: number}>;10
11function MyComponent({bar, foo = 3}: Props): React.Node {}12
13export const MyEnhancedComponent = trivialHOC(MyComponent); // ERRORsignature-verification-failureCannot build a typed interface for this module. You should annotate the exports of this module with types. Cannot determine the type of this call expression. Please provide an annotation, e.g., by adding a type cast around this expression.You can add an annotation to your exported component using component types:
1import * as React from 'react';2
3function trivialHOC<Config: {...}>(4 Component: component(...Config),5): component(...Config) {6 return Component;7}8
9type Props = Readonly<{bar: number, foo?: number}>;10
11function MyComponent({bar, foo = 3}: Props): React.Node {}12
13export const MyEnhancedComponent: component(...Props) = trivialHOC(MyComponent); // OK