【譯】TypeScript中的React Render Props

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

和以前的文章同樣,本文也要求你對render props有一些知識背景,若是沒有官方文檔可能會對你有很大的幫助。本文將會使用函數做爲children的render props模式以及結合React的context API來做爲例子。若是你想使用相似於render這樣子的render props,那也只須要把下面例子的children做爲你要渲染的props便可。html


爲了展現render props,咱們將要重寫以前文章的makeCounter HOC。這裏先展現HOC的版本:java

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衝突。react

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

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已經改變,加上如下內容:typescript

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

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

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擴展運算符展開了!post

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使用它們,由於它們的類型更簡單。

相關文章
相關標籤/搜索