【譯】TypeScript中的React Render Props

玩弄心裡的鬼發表於2019-04-18

原文連結: medium.com/@jrwebdev/r…

和之前的文章一樣,本文也要求你對render props有一些知識背景,如果沒有官方文件可能會對你有很大的幫助。本文將會使用函式作為children的render props模式以及結合React的context API來作為例子。如果你想使用類似於render這樣子的render props,那也只需要把下面例子的children作為你要渲染的props即可。


為了展示render props,我們將要重寫之前文章的makeCounter HOC。這裡先展示HOC的版本:

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向元件注入了value和兩個回撥函式(onIncrement 和 onDecrement),此外還在HOC內部使用minValue和maxValue兩個props而沒有傳遞給元件。我們討論瞭如果元件需要知道這些值,如何不傳遞props可能會出現問題,並且如果使用多個HOC包裝元件,注入的props的命名也可能與其他HOC注入的props衝突。

makeCounter HOC將會被像下面這樣重寫:

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,
    });
  }
}
複製程式碼

這裡有一些需要注意的變化。首先,injectedCounterProps被保留,因為我們需要定義一個props的interface在render props函式呼叫上而不是傳遞給元件的props(和HOC一樣)。MakeCounter(MakeCounterProps)的props已經改變,加上以下內容:

children(props: InjectedCounterProps): JSX.Element;
複製程式碼

這是render prop,然後元件內需要一個函式帶上注入的props並返回JSX element。下面是它用來突出顯示這一點的示例:

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自己的元件宣告變得簡單多了;它不再被包裝在函式中,因為它不再是臨時的,輸入也更加簡單,不需要泛型、做差值和型別的交集。它只有簡單的MakeCounterProps和MakeCounterState,就像其他任何組成部分一樣:

class MakeCounter extends React.Component<
  MakeCounterProps, 
  MakeCounterState
>
複製程式碼

最後,render()的工作也變少了;它只是一個函式呼叫並帶上注入的props-不需要破壞和物件的props擴充套件運算子展開了!

return this.props.children({
  value: this.state.value,
  onIncrement: this.increment,
  onDecrement: this.decrement,
});
複製程式碼

然後,render prop元件允許對props的命名和在使用的靈活性上進行更多的控制,這是和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>
);
複製程式碼

有了所有這些好處,特別是更簡單的輸入,那麼為什麼不一直使用render props呢?當然可以,這樣做不會有任何問題,但要注意render props元件的一些問題。

首先,這裡有一個關注點以外的問題;MakeCounter元件現在被放在了Counter元件內而不是包裝了它,這使得隔離測試這兩個元件更加困難。其次,由於props被注入到元件的渲染函式中,因此不能在生命週期方法中使用它們(前提是計數器被更改為類元件)。

這兩個問題都很容易解決,因為您可以使用render props元件簡單地生成一個新元件:

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>
);
複製程式碼

另一個問題是,一般來說,它不太方便,現在使用者需要編寫很多樣板檔案,特別是如果他們只想將元件包裝在一個單獨的臨時檔案中並按原樣使用props。這可以通過從render props元件生成HOC來補救:

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>
);
複製程式碼

在這裡,上一篇文章的技術,以及render props元件的現有型別,被用來生成HOC。這裡唯一需要注意的是,我們必須從HOC的props中移除render prop(children),以便在使用時不暴露它:

type MakeCounterHocProps = Omit<MakeCounterProps, 'children'>;
複製程式碼

最後,HOC和render props元件之間的權衡歸結為靈活性和便利性。這可以通過首先編寫render props元件,然後從中生成HOC來解決,這使使用者能夠在兩者之間進行選擇。這種方法在可重用元件庫中越來越常見,例如優秀的render-fns庫。

就TypeScript而言,毫無疑問,hocs的型別定義要困難得多;儘管通過這兩篇文章中的示例,它表明這種負擔是由HOC的提供者而不是使用者承擔的。在使用方面,可以認為使用HOC比使用render props元件更容易。

在react v16.8.0之前,我建議使用render props元件以提高鍵入的靈活性和簡單性,如果需要,例如構建可重用的元件庫,或者對於簡單在專案中使用的render props元件,我將僅從中生成HOC。在react v16.8.0中釋放react hook之後,我強烈建議在可能的情況下對兩個高階元件或render props使用它們,因為它們的型別更簡單。

相關文章