【译文】TypeScript笔记2/17:render props

676 阅读4分钟

原文地址:Notes on TypeScript: Render Props 本系列文章共17篇,此为第2篇

关于TypeScript的其他笔记

【译文】TypeScript笔记1/17:Pick,Exclude与高阶组件

引言

这些笔记有助于更好的理解TypeScript,并可以用来查询特殊情况下的TypeScript使用。例子基于TypeScript 3.2。

render props

(译者注:可参考react官网关于render props的介绍:render props

render props 是一种受欢迎的、增强React组件功能的模式,可以和高阶组件hoc互换使用,至于是使用render props模式还是hoc,完全取决于个人习惯和具体的使用案例。

为了更好的理解这个主题,我们将用render props写一个组件。在前一篇TypeScript笔记中,我们写了一个接收 onChangevalue 属性的 Input 组件,在这里我们将用render props的方式来重写这个hoc。

前一篇文章的hoc实现:(译者添加)

function withOnChange(WrappedComponent) {
    return class OnChange extends React.Component {
        state = {
            value: ""
        };
        onChange = e => {
            const target = e.target;
            const value = target.checked ? target.checked : target.value;
            this.setState({ value });
        };
        render() {
            return (
              <WrappedComponent
                {...this.props}
                onChange={this.onChange}
                value={this.state.value}
              />
            );
        }
    };
}

const Input = ({ value, onChange, className }) => (
  <input className={className} value={value} onChange={onChange} />
);

render props 模式实现:

class OnChange extends React.Component {
  state = {
    value: this.props.initialValue
  };
  onChange = event => {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    this.setState({ value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}

在React应用里面使用重构之后的 onChange

<OnChange
  initialValue="hello"
  render={onChangeProps => <Input {...props} {...onChangeProps} />}
/>

重用前一篇文章定义的大部分类型:

type InputProps = {
  name: string,
  type: string
};

type OnChangeProps = {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
  value: string
};

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  // (译者注:参数onChangeProps的类型应该是OnChangeProps吧?即render: (onChangeProps: OnChangeProps) => JSX.Element)
  render: (onChangeProps: onChangeProps) => JSX.Element 
};

type OnChangeState = {
  value: string
};

Input 组件没有发生变化,这里的例子可以复用那个组件

const Input = ({ value, onChange, type, name }: InputProps & OnChangeProps) => (
  <input type={type} name={name} value={value} onChange={onChange} />
);

到目前为止,一切准备就绪,让我们看看怎样编写OnChange。有意思的是,为了写OnChange组件,我们需要做的并不多。

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    return this.props.render({
      value: this.state.value,
      onChange: this.onChange
    });
  }
}

前一篇文章的hoc实现:(译者添加):

type WithOnChangeState = {
    value: string | boolean;
}

function withOnChange<Props>(WrappedComponent: React.ComponentType<Props>) {
  return class OnChange extends React.Component<Diff<Props, WithOnChangeProps>, WithOnChangeState> {
    state = {
      value: ""
    };
    onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      const target = event.target;
      const value = target.type === 'checkbox' ? target.checked : target.value;
      this.setState({ value });
    };
    render() {
      return (
        <WrappedComponent
          {...this.props as Props}
          onChange={this.onChange}
          value={this.state.value}
        />
      );
    }
  };
}

和hoc实现相比,render props只需要为 onChange 定义props和state,在这个例子中,只需要向下面这样使用已经存在的 ExpandedOnChangePropsOnChangeState 来定义类组件

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState>.

如果想要在应用的多个地方复用这个功能,可以通过定义一个新的组件,例如ControlledInput,将OnChangeInput结合在一起,从而允许开发者可以像定义initialValue一样定义nametype

type ControlledInputProps = InputProps & { initialValue: string };

const ControlledInput = ({ initialValue, ...props }: ControlledInputProps) => (
  <OnChange
    initialValue={initialValue}
    render={onChangeProps => <Input {...props} {...onChangeProps} />}
  />
);

现在ControlledInput就可以在其他的组件里使用了,并且如果传入nametypeinitialValue的话,TypeScript将报错。

提高

如果想要传入render回调函数或者是children属性实现的话,我们可以对OnChange组件做出一些改变。之前的ExpandedOnChangeProps是这样的:

type ExpandedOnChangeProps = {
  initialValue: string | boolean,
  render: (onChangeProps: onChangeProps) => JSX.Element 
};

一种方法是,把传入的回调函数作为children属性:

type ExpandedOnChangeProps = {
  initialValue: string,
  render?: (onChangeProps: onChangeProps) => JSX.Element,
  children?: (onChangeProps: onChangeProps) => JSX.Element
};

但是上面的定义存在一个问题,render和children两个属性可以都传,也可以都不传。而我们想要的是确保只传入这两个属性之一,于是可以显示定义下面的RenderProps类型: (译者注:为什么作者在第一行行首都会加个 | 呢?这是什么用法?还是一个小的error?对于ts才疏学浅了,后续探究一下)

type RenderProp =
  | { render: (onChangeProps: OnChangeProps) => JSX.Element }
  | { children: (onChangeProps: OnChangeProps) => JSX.Element };

于是可以重写成下面的ExpandedOnChangeProps定义:

type ExpandedOnChangeProps = {
  initialValue: string
} & RenderProp;

最后更新render函数来处理这两种可能的情况:

class OnChange extends React.Component<ExpandedOnChangeProps, OnChangeState> {
  state = {
    value: this.props.initialValue
  };
  onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const target = event.target;
    this.setState({ value: target.value });
  };
  render() {
    if ("render" in this.props) {
        return this.props.render({
          value: this.state.value,
          onChange: this.onChange
        });
    }
    
    if ("children" in this.props) {
        return this.props.children({
          value: this.state.value,
          onChange: this.onChange
        });
    }
    
    throw new Error("A children or render prop has to be defined");
  }
}

通过调用"render" in this.props,可以检查是否定义了render。同样也可以检查是否定义了children。如果这两个属性都没有定义,那么将抛出错误。 前面的ControlledInput例子可以重写:

const ControlledInput = ({
  initialValue,
  ...props
}: InputProps & { initialValue: string }) => (
  <OnChange initialValue={initialValue}>
    {onChangeProps => <Input {...props} {...onChangeProps} />}
  </OnChange>
);

现在,我们对于如何用TypeScript书写render props应该有了基本的了解。