原文链接:https://medium.com/@jrwebdev/…
和之前的文章一样,本文也要求你对 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 使用它们,因为它们的类型更简单。