注:这种模式也叫 渲染回调(Render Callback) , 现在最新的叫法为:渲染属性(Render Props),请参阅。这是一篇老文章,主要讲解该模式简洁和灵活,对于我们理解这种模式非常有帮助。
我最近在推特上就 高阶组件 和 函数作为子组件 进行了调查,结果让我感到惊讶。
虽然我非常感谢我的朋友 Ryan Florence 的回复:每次都使用函数作为子组件。 我想不出 高阶组件(HOC) 更强大的场景。
每个人都应该 “重新思考最佳实践”?如果你不知道 “函数作为子组件” 模式是什么,这篇文章我要阐述的内容:
- 告诉你什么是函数作为子组件。
- 说服你为什么它很有用。
什么是函数作为子组件?
“函数作为子组件” 是一个组件,这个组件接收一个函数作为其子组件。由于 React 的属性(property) 类型,该模式可以简单地实现并且值得推广。
class MyComponent extends React.Component { render() { return ( <div> {this.props.children('Scuba Steve')} </div> ); } } MyComponent.propTypes = { children: React.PropTypes.func.isRequired, };
注: 从 React v15.5 开始 ,React.PropTypes 助手函数已被弃用,我们建议使用 prop-types 库 来定义contextTypes。 即你需要手动引入 import PropTypes from 'prop-types';
这就函数作为子组件!通过使用函数作为子组件,我们将父组件和子组件分离,让使用者决定如何将参数应用于子组件。例如:
<MyComponent> {(name) => ( <div>{name}</div> )} </MyComponent>
而使用相同组件的其他人可能决定以不同的方式应用 name
,也许是用于属性:
<MyComponent> {(name) => ( <img src="/scuba-steves-picture.jpg" alt={name} /> )} </MyComponent>
这里真正干净的是 MyComponent
,作为子组件的函数可以代表它所组成的组件管理状态,函数作为子组件可以代表它组成的组件管理状态,而无需知道子组件如何利用 state(状态)。让我们继续讨论一个更现实的例子。
Ratio Component
Ratio 组件将使用当前设备宽度,监听 resize 事件并子组件中调用宽度,高度和一些关于它是否已经计算出尺寸的信息。
首先,我们从函数作为子组件的片段开始,这在所有函数作为子组件的模式中很常见,它只是让使用者知道我们期望一个函数作为我们的子组件,而不是React节点。
class Ratio extends React.Component { render() { return ( {this.props.children()} ); } } Ratio.propTypes = { children: React.PropTypes.func.isRequired, };
接下来让我们设计我们的API,我们想要按 X 和 Y 轴提供的一个比率,然后我们将使用当前宽度和高度来计算,让我们设置一些内部 state(状态) 来管理宽度和高度,我们是否还计算过,以及一些 propTypes 和 defaultProps 。这些对于使用我们组件的人来说是好东西。
class Ratio extends React.Component { constructor() { super(...arguments); this.state = { hasComputed: false, width: 0, height: 0, }; } render() { return ( {this.props.children()} ); } } Ratio.propTypes = { x: React.PropTypes.number.isRequired, y: React.PropTypes.number.isRequired, children: React.PropTypes.func.isRequired, }; Ratio.defaultProps = { x: 3, y: 4 };
我们还没有做任何有趣的事情,让我们添加一些事件监听器并实际计算宽度(适应我们的比率变化时):
class Ratio extends React.Component { constructor() { super(...arguments); this.handleResize = this.handleResize.bind(this); this.state = { hasComputed: false, width: 0, height: 0, }; } getComputedDimensions({x, y}) { const {width} = this.container.getBoundingClientRect(); return { width, height: width * (y / x), }; } componentWillReceiveProps(next) { this.setState(this.getComputedDimensions(next)); } componentDidMount() { this.setState({ ...this.getComputedDimensions(this.props), hasComputed: true, }); window.addEventListener('resize', this.handleResize, false); } componentWillUnmount() { window.removeEventListener('resize', this.handleResize, false); } handleResize() { this.setState({ hasComputed: false, }, () => { this.setState({ hasComputed: true, ...this.getComputedDimensions(this.props), }); }); } render() { return ( <div ref={(ref) => this.container = ref}> {this.props.children(this.state.width, this.state.height, this.state.hasComputed)} </div> ); } } Ratio.propTypes = { x: React.PropTypes.number.isRequired, y: React.PropTypes.number.isRequired, children: React.PropTypes.func.isRequired, }; Ratio.defaultProps = { x: 3, y: 4 };
我在这里做了很多事情。我们添加了一些事件监听器来监听 resize 事件以及使用提供的比率实际计算宽度和高度。所以我们有一个宽度和高度在我们的内部状态,我们如何将他共享给其他组件呢?
这是难以理解的事情之一,因为它很简单,当你看到它时,你会想到,“这不可能就是全部。”但这就是它的全部。
子组件实际上只是一个 JavaScript 函数。
这意味着为了将计算出的宽度和高度向下传递,我们只需将它们作为参数提供:
render() { return ( <div ref='container'> {this.props.children(this.state.width, this.state.height, this.state.hasComputed)} </div> ); }
现在任何人都可以使用 Ratio
组件以他们想要的任何方式提供全屏的宽度和正确计算的高度!例如,有人可以使用 Ratio
组件在 img
上设置宽高比:
<Ratio> {(width, height, hasComputed) => ( hasComputed ? <img src='/scuba-steve-image.png' width={width} height={height} /> : null )} </Ratio>
同时,在另一个文件中,有人决定使用它来设置CSS属性。
<Ratio> {(width, height, hasComputed) => ( <div style={{width, height}}>Hello world!</div> )} </Ratio>
在另一个应用程序中,有人使用基于计算高度有条件地渲染不同的子组件:
<Ratio> {(width, height, hasComputed) => ( hasComputed && height > TOO_TALL ? <TallThing /> : <NotSoTallThing /> )} </Ratio>
优势
- 组合组件的开发人员可以快速传递和使用这些属性。
- 函数作为子组件的模式不强制使用者如何利用其值,可以非常灵活的使用。
- 使用者不需要创建另一个组件来决定如何应用从 “高阶组件” 传入的属性。高阶组件通常在它们所组成的组件上强制要求属性名。为了解决这个问题,许多“高阶组件”提供者提供了一个选择器函数,允许使用者选择需要的属性名称(想想 redux-connects 选择函数)。在函数作为子组件的模式不存在这个问题。
- 不会污染 “props” 的命名空间,这允许您使用 “Ratio” 组件结合 “Pinch to Zoom” 组件使用,无论它们是否都有计算宽度。高阶组件带有他们对组成的组件施加了隐式约定,不幸的是,这可能意味着 prop 名称冲突。
- 高阶组件在您的开发工具和组件本身中创建一个间接层,例如,一旦包含在高阶组件中,高阶组件中的设置常量将无法访问。例如:
MyComponent.SomeContant = ‘SCUBA’;
,然后由高阶组件包裹,export default connect(...., MyComponent);
。你的常数就好像死了。如果没有高阶组件提供访问底层组件类的函数,则再也访问不到它。伤心。
概要
大多数时候,您会认为“我需要一个更高阶的组件来实现这个共享功能!”我希望我已经说服你,函数作为子组件是抽象UI问题的更好选择,根据我的经验,这钟模式总是适用。除非你的子组件真正耦合到由它组成的高阶组件。
关于高阶组件的一个不幸的真相
作为一个辅助点,我认为高阶组件的名称不正确,尽管尝试更改其名称可能比较遥远。高阶函数是至少符合以下操作之一的函数:
- 将n个函数作为参数。
- 返回一个函数作为结果。
事实上,高阶组件做了与此类似的事情,即将一个组件作为参数并返回另一个组件,但我认为将高阶组件视为工厂函数更容易,它是一个动态创建允许组件的函数,用于组件的运行时组合。 但是,他们在组合时不知道你的 React state(状态) 和 props(属性) !
函数作为子组件的模式允许组件类似组合,以便在进行组合决策时可以访问 state(状态) , props(属性)和上下文。 因为函数作为子组件:
- 把函数作为一个参数。
- 渲染函数的返回结果。
我不禁觉得函数作为子组件的模式应该叫做“高阶组件”才对,因为它很像高阶函数,只使用组件组合技术而不是函数组合。
示例
- 这个项目经过很长一段时间,将高阶组件转换为我介绍了这个概念。
缺点
由于 函数作为子组件 在渲染时为您提供是函数,因此通常无法使用 shouldComponentUpdate 优化它们。
原文链接:
感谢分享,收藏了
写的蛮好的,学习到了,
没有楼主这么牛,但业余时间也整理了些东西,有兴趣的可以看下:https://github.com/meibin08