React Render Props in TypeScript

Keywords: Javascript React less TypeScript

Original Link: https://medium.com/@jrwebdev/...

As with previous articles, this article also requires you to have some background in render props, if not Official Documents It may be of great help to you.This article will use functions as render props mode for children and context API combined with React as examples.If you want to use render props like render, just use children from the example below as the props you want to render.

To show render props, we'll rewrite the previous article makeCounter HOC .Let's start with the HOC version:

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

interface MakeCounterProps {
  minValue?: number;
  maxValue?: number;
}

interface MakeCounterState {
  value: number;
}

const makeCounter = <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}
        />
      );
    }
  };

HOC injects value and two callback functions (onIncrement and onDecrement) into the component, and uses two props, minValue and maxValue, inside the HOC without passing them to the component.We discussed how not to pass props can be problematic if the component needs to know these values, and if multiple HOC wrapping components are used, the naming of injected props may conflict with that of other HOC-injected props.

makeCounter HOC will be rewritten as follows:

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

interface MakeCounterProps {
  minValue?: number;
  maxValue?: number;
  children(props: InjectedCounterProps): JSX.Element;
}

interface MakeCounterState {
  value: number;
}

class MakeCounter extends React.Component<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() {
    return this.props.children({
      value: this.state.value,
      onIncrement: this.increment,
      onDecrement: this.decrement,
    });
  }
}

Here are some changes that need to be noted.First, injectedCounterProps are retained because we need to define an interface t for props on the render props function call instead of props passed to the component (as with HOC).MakeCounter (MakeCounterProps) props have changed, with the following added:

children(props: InjectedCounterProps): JSX.Element;

This is render prop, and then you need a props injected with the function strip inside the component and return the JSX element.Here is an example of how it highlights this point:

interface CounterProps {
  style: React.CSSProperties;
  minValue?: number;
  maxValue?: number;
}

const Counter = (props: CounterProps) => (
  <MakeCounter minValue={props.minValue} maxValue={props.maxValue}>
    {injectedProps => (
      <div style={props.style}>
        <button onClick={injectedProps.onDecrement}> - </button>
        {injectedProps.value}
        <button onClick={injectedProps.onIncrement}> + </button>
      </div>
    )}
  </MakeCounter>
);

MakeCounter's own component declaration is much simpler; it is no longer wrapped in a function because it is no longer temporary and the input is simpler, requiring no intersection of generics, differences, and types.It has only simple MakeCounterProps and MakeCounterState, just like any other component:

class MakeCounter extends React.Component<
  MakeCounterProps, 
  MakeCounterState
>

Finally, render () does less work; it's just a function call with an injected props-the props extension operator that doesn't need to be destroyed and objects expanded!

return this.props.children({
  value: this.state.value,
  onIncrement: this.increment,
  onDecrement: this.decrement,
});

The render prop component then allows more control over the naming and flexibility of props, which is equivalent to HOC:

interface CounterProps {
  style: React.CSSProperties;
  value: number;
  minCounterValue?: number;
  maxCounterValue?: number;
}

const Counter = (props: CounterProps) => (
  <MakeCounter
    minValue={props.minCounterValue}
    maxValue={props.maxCounterValue}
  >
    {injectedProps => (
      <div>
        <div>Some other value: {props.value}</div>
        <div style={props.style}>
          <button onClick={injectedProps.onDecrement}> - </button>
          {injectedProps.value}
          <button onClick={injectedProps.onIncrement}> + </button>
        </div>
        {props.minCounterValue !== undefined ? (
          <div>Min value: {props.minCounterValue}</div>
        ) : null}
        {props.maxCounterValue !== undefined ? (
          <div>Max value: {props.maxCounterValue}</div>
        ) : null}
      </div>
    )}
  </MakeCounter>
);

With all these benefits, especially simpler inputs, why not keep using render props?Of course, there are no problems with this, but be aware of some issues with the render props component.

First, there's a problem outside of focus; the MakeCounter component is now placed inside the Counter component instead of wrapping it, making it more difficult to isolate the two components for testing.Second, since props are injected into the rendering function of the component, they cannot be used in lifecycle methods (provided the counter is changed to a class component).

These two issues are easy to solve because you can simply generate a new component using the render props component:

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>
);

interface WrappedCounterProps extends CounterProps {
  minValue?: number;
  maxValue?: number;
}

const WrappedCounter = ({
  minValue,
  maxValue,
  ...props
}: WrappedCounterProps) => (
  <MakeCounter minValue={minValue} maxValue={maxValue}>
    {injectedProps => <Counter {...props} {...injectedProps} />}
  </MakeCounter>
);

Another problem is that, in general, it is not very convenient and users now need to write many template files, especially if they only want to wrap components in a separate temporary file and use props as is.This can be remedied by generating HOC s from render props components:

import { Subtract, Omit } from 'utility-types';
import MakeCounter, { MakeCounterProps, InjectedCounterProps } from './MakeCounter';

type MakeCounterHocProps = Omit<MakeCounterProps, 'children'>;

const makeCounter = <P extends InjectedCounterProps>(
  Component: React.ComponentType<P>
): React.SFC<Subtract<P, InjectedCounterProps> & MakeCounterHocProps> => ({
  minValue,
  maxValue,
  ...props
}: MakeCounterHocProps) => (
  <MakeCounter minValue={minValue} maxValue={maxValue}>
    {injectedProps => <Component {...props as P} {...injectedProps} />}
  </MakeCounter>
);

Here, the techniques in the previous article, along with the existing types of render props components, are used to generate HOCs.The only thing to note here is that we have to remove render prop(children) from the props of HOC in order not to expose it in use:

type MakeCounterHocProps = Omit<MakeCounterProps, 'children'>;

Finally, the trade-off between HOC and render props components boils down to flexibility and convenience.This can be solved by writing the render props component first, then generating a HOC from it, which allows the user to choose between the two.This approach is becoming more common in reusable component libraries, such as the excellent render-fns library.

As far as TypeScript is concerned, it is undoubtedly much more difficult to define the type of hocs; although the examples in these two articles show that this burden is borne by the HOC provider, not the user.In terms of usage, you can think that using HOC is easier than using render props components.

Prior to react v16.8.0, I recommended using the render props component to improve typing flexibility and simplicity. If necessary, such as building a reusable component library, or for render props components that are simply used in a project, I will only generate HOC from them.After releasing the react hook s in react v16.8.0, I strongly recommend using them on two higher-order components or render props where possible, since their types are simpler.

Posted by echo10 on Wed, 08 May 2019 23:54:38 -0700