【譯】TypeScript中的React高階元件

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

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

高階元件(HOCs)在React中是元件複用的一個強大工具。但是,經常有開發者在結合TypeScript使用中抱怨道很難去為其設定types。

這邊文章將會假設你已經具備了HOCs的基本知識,並會根據由淺入深的例子來向你展示如何去為其設定types。在本文中,高階元件將會被分為兩種基本模式,我們將其命名為enhancersinjectors

  • enhancers:用附加的功能/props來包裹元件。
  • injectors:向元件注入props。

請注意,本文中的示例並不是最佳實踐,本文主要只是展示如何在HOCs中設定types。


Enhancers

我們將從enhancers開始,因為它更容易去設定types。此模式的一個基本示例是一個向元件新增loading props的HOC,並且將其設定為true的時候展示loading圖。下面是一個沒有types的示例:

const withLoading = Component =>
  class WithLoading extends React.Component {
    render() {
      const { loading, ...props } = this.props;
      return loading ? <LoadingSpinner /> : <Component {...props} />;
    }
  };
複製程式碼

然後是加上types

interface WithLoadingProps {
  loading: boolean;
}

const withLoading = <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} />;
    }
  };
複製程式碼

這裡發生了一些事情,所以我們將把它分解:

interface WithLoadingProps {
  loading: boolean;
}
複製程式碼

在這裡,宣告一個props的interface,將會被新增到被包裹的元件上。

<P extends object>(Component: React.ComponentType<P>)
複製程式碼

這裡我們使用泛型:P表示傳遞到HOC的元件的props。React.ComponentType<P>React.FunctionComponent<P> | React.ClassComponent<P>的別名,表示傳遞到HOC的元件可以是類元件或者是函式元件。

class WithLoading extends React.Component<P & WithLoadingProps>
複製程式碼

在這裡,我們定義從HOC返回的元件,並指定該元件將包括傳入元件的props(P)和HOC的props(WithLoadingProps)。它們通過 & 組合在一起。

const { loading, ...props } = this.props;
複製程式碼

最後,我們使用loading props有條件地顯示加loading圖或傳遞了自己props的元件:

return loading ? <LoadingSpinner /> : <Component {...props as P} />;
複製程式碼

注意:由於typescript中可能存在的bug,因此從typescript v3.2開始,這裡需要型別轉換(props as p)。

我們的withloading HOC也可以重寫以返回函式元件而不是類:

const withLoading = <P extends object>(
  Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> => ({
  loading,
  ...props
}: WithLoadingProps) =>
  loading ? <LoadingSpinner /> : <Component {...props as P} />;
複製程式碼

這裡,我們對物件rest/spread也有同樣的問題,因此通過設定顯式的返回型別React.FC<P & WithLoadingProps>來解決這個問題,但只能在無狀態功能元件中使用WithLoadingProps。

注意:React.FC是React.FunctionComponent的縮寫。在早期版本的@types/react中,是React.SFC或React.StatelessFunctionalComponent。

Injectors

injectors是更常見的HOC形式,但更難為其設定型別。除了向元件中注入props外,在大多數情況下,當包裹好後,它們也會移除注入的props,這樣它們就不能再從外部設定了。react redux的connect就是是injector HOC的一個例子,但是在本文中,我們將使用一個更簡單的例子,它注入一個計數器值並回撥以增加和減少該值:

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

這裡有幾個關鍵區別:

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

複製程式碼

我們給將要注入到元件的props宣告一個interface,該介面將被匯出,以便這些props可由被HOC包裹的元件使用:

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);
複製程式碼
<P extends InjectedCounterProps>(Component: React.ComponentType<P>)
複製程式碼

我們再次使用泛型,但是這次,你要確保傳入到HOC的元件包含注入到其中的props,否則,你將收到一個編譯錯誤。

class MakeCounter extends React.Component<
  Subtract<P, InjectedCounterProps>,    
  MakeCounterState  
>
複製程式碼

HOC返回的元件使用Piotrek Witek’s的utility-types包中的subtract,它將從傳入元件的props中減去注入的props,這意味著如果它們設定在生成的包裹元件上,則會收到編譯錯誤:

TypeScript compilation error when attempting to set value on the wrapped component

Enhance + Inject

結合這兩種模式,我們將在計數器示例的基礎上,允許將最小和最大計數器值傳遞給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}
        />
      );
    }
  };
複製程式碼

這裡,Subtract與types交集相結合,將元件自身的props與HOCs自身的props相結合,減去注入元件的props:

Subtract<P, InjectedCounterProps> & MakeCounterProps
複製程式碼

除此之外,與其他兩種模式相比,沒有真正的差異需要強調,但是這個示例確實帶來了一些高階元件的問題。這些並不是真正特定於typescript的,但值得詳細說明,以便我們可以討論如何使用typescript來解決這些問題。

首先,MinValue和MaxValue被HOC攔截,而不是傳遞給元件。但是,你也許希望它們是這樣的,這樣你就可以基於這些值禁用遞增/遞減按鈕,或者向使用者顯示一條訊息。如果用HOC,你也可以簡單地修改它來注入這些值,但是如果你沒有(例如,它來自一個NPM包),這就將會是一個問題。

其次,由HOC注入的prop有一個非常通用的名稱;如果要將其用於其他目的,或者如果要從多個HOC注入prop,則此名稱可能與其他注入的prop衝突。您可以將名稱更改為不太通用的解決方案,但就解決方案而言,這不是一個很好的解決方案!

相關文章