Skip to main content

Higher-order Components

danger

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}): React.Node {}20 21const MyEnhancedComponent = injectProp(MyComponent);22 23// We don't need to pass in `foo` even though `MyComponent` requires it:24<MyEnhancedComponent a={1} b={2} />; // OK25 26// We still require `a` and `b`:27<MyEnhancedComponent a={1} />; // ERROR
7:17-7:22: Cannot use `Config` [1] as a component rest param. Component rest params must use an object type and cannot be optional. [incompatible-type]
27:2-27:20: Cannot create `MyEnhancedComponent` element because property `b` is missing in props [1] but exists in object type [2]. [incompatible-type]

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.

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 15// A class component in this example16class MyComponent extends React.Component<{17 a: number,18 b: number,19 ...InjectedProps,20}> {}21 22const MyEnhancedComponent = injectProp(MyComponent);23 24// If we create a ref object for the component, it will never be assigned25// an instance of MyComponent!26const ref = React.createRef<MyComponent>();27 28// Error, mixed is incompatible with MyComponent.29<MyEnhancedComponent ref={ref} a={1} b={2} />;
7:17-7:22: Cannot use `Config` [1] as a component rest param. Component rest params must use an object type and cannot be optional. [incompatible-type]

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:

1import * as React from 'react';2
3type InjectedProps = {foo: number}4
5function injectAndPreserveInstance<Config: {...}, Instance>(6  Component: component(ref?: React.RefSetter<Instance>, ...{...Config, ...InjectedProps})
7): component(ref?: React.RefSetter<Instance>, ...Config) {8 return React.forwardRef<Config, Instance>((props, ref) =>9 <Component ref={ref} foo={3} {...props} />
10 );11}12 13class MyComponent extends React.Component<{14 a: number,15 b: number,16 ...InjectedProps,17}> {}18 19const MyEnhancedComponent = injectAndPreserveInstance(MyComponent);
20 21const ref = React.createRef<MyComponent>();22 23// All good! The ref is forwarded.24<MyEnhancedComponent ref={ref} a={1} b={2} />;
6:64-6:69: inexact `Config` [1] is incompatible with exact object type [2]. [incompatible-exact]
9:7-9:48: Cannot spread props because Flow cannot determine a type for props [1]. `Config` [2] is inexact, so it may contain `ref` with a type that conflicts with `ref`'s definition in props [1]. Try making `Config` [2] exact. [cannot-spread-inexact]
19:29-19:53: Cannot call `injectAndPreserveInstance` because `Instance` [1] is underconstrained by call of `injectAndPreserveInstance` [2]. Either add explicit type arguments or cast the expression to your expected type. [underconstrained-implicit-instantiation]

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); // ERROR
13:36-13:58: Cannot 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. [signature-verification-failure]

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