Link: medium.com/jrwebdev/r…

High-order Components (HOCs) in React are a powerful tool for component reuse. However, developers often complain when working with TypeScript that it is difficult to set types for them.

This article will assume that you already have a basic knowledge of HOCs, and will show you how to set types for HOCs with a step-by-step example. In this article, higher-order components are divided into two basic modes, which we name enhancers and Injectors:

  • Enhancers: Wrap components with additional functions /props.
  • Injectors: inject props into a component.

Please note that the examples in this article are not best practices; this article mainly shows how to set types in HOCs.


Enhancers

We’ll start with enhancers because it’s easier to set types. A basic example of this pattern is a HOC that adds loading props to a component and displays a loading diagram when it is set to true. Here is an example with no types:

const withLoading = Component= >
  class WithLoading extends React.Component {
    render() {
      const{ loading, ... props } =this.props;
      return loading ? <LoadingSpinner /> : <Component {...props} />;
    }
  };
Copy the code

And then you add types

interface WithLoadingProps {
  loading: boolean;
}

constwithLoading = <P extends object>(Component: React.ComponentType<P>) => class WithLoading extends React.Component<P & WithLoadingProps> { render() { const { loading, . props } = this.props; return loading ? <LoadingSpinner /> : <Component {... props as P} />; }};Copy the code

There’s something going on here, so we’ll break it down:

interface WithLoadingProps {
  loading: boolean;
}
Copy the code

Here, declare an interface for props that will be added to the wrapped component.

<P extends object>(Component: React.ComponentType<P>)
Copy the code

Here we use generics: P for props of the component passed to HOC. React.Com ponentType < P > is the React FunctionComponent < P > | React. ClassComponent < P > the alias, said to the HOC components can be a class or function components.

class WithLoading extends React.Component<P & WithLoadingProps>
Copy the code

Here, we define the component returned from HOC and specify that it will include props (P) of the incoming component and props (WithLoadingProps) of HOC. They are combined by ampersand.

const{ loading, ... props } =this.props;
Copy the code

Finally, we use loading props to conditionally display a loading graph or a component that passes its own props:

returnloading ? <LoadingSpinner /> : <Component {... props as P} />;Copy the code

Note: Due to possible bugs in typescript, starting with typescript V3.2, type conversions are required (props as P).

Our Withloading HOC can also be rewritten to return function components instead of classes:

constwithLoading = <P extends object>( Component: React.ComponentType<P> ): React.FC<P & WithLoadingProps> => ({ loading, ... props }: WithLoadingProps) => loading ? <LoadingSpinner /> : <Component {... props as P} />;Copy the code

Here, we have the same problem with the rest/spread object, so we solve this problem by setting the explicit return type react.fc

, but only WithLoadingProps in stateless functional components.

Note: React.FC is short for React.FunctionComponent. In the early version of the @ types/react, is the react. The SFC or react. StatelessFunctionalComponent.

Injectors

Injectors are the more common form of HOC, but are more difficult to type for. In addition to injecting props into components, in most cases, when wrapped, they also remove the injected props so that they can no longer be set externally. React Redux’s Connect is an example of injector HOC, but in this article, we’ll use a simpler example that injects a counter value and calls back to increase and decrease that value:

import { Subtract } from 'utility-types';

export interface InjectedCounterProps {
  value: number;
  onIncrement(): void;
  onDecrement(): void;
}

interface MakeCounterState {
  value: number;
}

const makeCounter = <P extends InjectedCounterProps>(
  Component: React.ComponentType<P>
) =>
  class MakeCounter extends React.Component<
    Subtract<P, InjectedCounterProps>,
    MakeCounterState
  > {
    state: MakeCounterState = {
      value: 0,
    };

    increment = () => {
      this.setState(prevState => ({
        value: prevState.value + 1,
      }));
    };

    decrement = () => {
      this.setState(prevState => ({
        value: prevState.value - 1,
      }));
    };

    render() {
      return (
        <Component
          {...this.props as P}
          value={this.state.value}
          onIncrement={this.increment}
          onDecrement={this.decrement}
        />
      );
    }
  };
Copy the code

Here are a few key differences:

export interface InjectedCounterProps {  
  value: number;  
  onIncrement(): void;  
  onDecrement(): void;
}

Copy the code

We declare an interface for the props to be injected into the component. This interface will be exported so that the props can be used by the ad-wrapped component:

import makeCounter, { InjectedCounterProps } from './makeCounter'; interface CounterProps extends InjectedCounterProps { style? : React.CSSProperties; }const Counter = (props: CounterProps) = > (
  <div style={props.style}>
    <button onClick={props.onDecrement}> - </button>
    {props.value}
    <button onClick={props.onIncrement}> + </button>
  </div>
);

export default makeCounter(Counter);
Copy the code
<P extends InjectedCounterProps>(Component: React.ComponentType<P>)
Copy the code

We’ll use generics again, but this time, make sure that the component passed into HOC contains props injected into it, otherwise you’ll get a compile error.

class MakeCounter extends React.Component<
  Subtract<P.InjectedCounterProps>,    
  MakeCounterState  
>
Copy the code

The component returned by HOC uses a subtract in Piotrek Witek’s Utility-Types package, which subtracts the injected props from the props of the incoming component, which means that if they are set on the generated package component, a compile error will be received:

Enhance + Inject

Combining these two patterns, we will build on the counter example to allow min and Max counter values to be passed to HOC, which is then intercepted and used by it, without passing them to the component:

export interface InjectedCounterProps {
  value: number;
  onIncrement(): void;
  onDecrement(): void; } interface MakeCounterProps { minValue? : number; maxValue? : number; } interface MakeCounterState {value: number;
}

constmakeCounter = <P extends InjectedCounterProps>( Component: React.ComponentType<P> ) => class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps> & MakeCounterProps, MakeCounterState > { state: MakeCounterState = { value: 0, }; increment = () => { this.setState(prevState => ({ value: prevState.value === this.props.maxValue ? prevState.value : prevState.value + 1, })); }; decrement = () => { this.setState(prevState => ({ value: prevState.value === this.props.minValue ? prevState.value : prevState.value - 1, })); }; render() { const { minValue, maxValue, ... props } = this.props; return ( <Component {... props as P} value={this.state.value} onIncrement={this.increment} onDecrement={this.decrement} /> ); }};Copy the code

Subtract the props of the injected component from the props of the COMPONENT by combining the intersection of types:

Subtract<P, InjectedCounterProps> & MakeCounterProps
Copy the code

Other than that, there are no real differences to emphasize compared to the other two patterns, but this example does bring up some higher-order component issues. None of this is really typescript-specific, but it’s worth going into detail so that we can discuss how to use typescript to solve these problems.

First, minValues and maxvalues are intercepted by HOC rather than passed to the component. However, you might want them to be so that you can disable the increment/decrement button based on these values, or display a message to the user. If you use HOC, you can also simply modify it to inject these values, but if you don’t (for example, it comes from an NPM package), this will be a problem.

Second, a prop injected by HOC has a very generic name; This name may conflict with other injected prop if it is being used for other purposes, or if a prop is being injected from multiple HOC. You could change the name to a less generic solution, but as solutions go, that’s not a very good solution!