什么是 Render Props?
新的 context api 使用了 render props:
- <ThemeContext.Consumer>
- {theme => (
- <button
- {...props}
- style={{backgroundColor: theme.background}}
- />
- )}
- </ThemeContext.Consumer>
第一次见到这个语法时, 大多会很惊讶, 因为日常代码里 props.children 必然是字符串或者元素. 但事实上 props.children 可以是函数, 只要最终生成的 render 的返回值是 dom 元素就行. 例如:
- // chilren props
- const Test = props => props.children('hello world')
- const App = () => (
- <Test>
- {text => <div>{text}</div>}
- </Test>
- )
- ReactDOM.render((<App />, root) // 返回 < div>hello world</div>
虽然没有实际意义, 但这即是一个 render props. 当然 render props 最初的意思是: 组件不自己定义 render 函数, 而是通过一个名为 render 的 props 将外部定义的 render 函数传入使用. 以上例来说, 会是这样:
- // render props
- const Test = props => props.render('hello world')
- const App = () => (
- <Test
- render={text => <div>{text}</div>}
- />
- )
- ReactDOM.render((<App />, root) // 返回 < div>hello world</div>
因为现实中 render 函数很庞大, 为了代码整洁多半会使用 children 而不是自定义的 render 来接收外部的 render 函数. 所以这一技巧也可以称为 children props(相对于 render props 更加不知所云的名称), 但一般统称 render props.
为何要使用如此怪异的语法呢?
为了重用性. React 的组件化就是为了方便重用. 大多数场景我们需要重用的是 UI(例如文章列表, 侧栏), 但也有少数情况需要重用的是功能和状态 (例如 context).
如果说 React 的核心是 State => UI, 普通的组件是 UI 重用, 那么 render props 就是为了 State 重用而应运而生的.
Render Props 小史
在 Demo 展开前插播一段 render props 的趣史.
最早引人关注是从 Facebook 的 Cheng Lou 写的 React Motion https://github.com/chenglou/react-motion 动画库.
- import { Motion, spring } from 'react-motion';
- <Motion defaultStyle={{x: 0}} style={{x: spring(10)}}>
- {value => <div>{value.x}</div>}
- </Motion>
之后这一写法被各位大牛广泛接受, 写过很多非常赞的前端教程的 Kent C. Dodds 就非常喜欢 render props, 他所任职的 PayPal 的输入框组件 https://github.com/paypal/downshift 也使用了 render props
大家熟知的 https://github.com/ReactTraining/react-router 的作者 Michael Jackson 也是 render props 的极力推崇者. 他 twitter 过一句很有争议的话:
Next time you think you need a HOC (higher-order component) in @reactjs, you probably don't.
翻译过来就是: 下次你想使用 HOC 解决问题时, 其实大半不需要. 在回复中他补充说明到,
I can do anything you're doing with your HOC using a regular component with a render prop. Come fight me.
即是说, 所有用 HOC 完成的事, render props 都能搞定. 值得一提的是 react-router 4 里唯一的一个 HOC 是 withRouter, 而它是用 render props 实现的, 有兴趣的可以去看一下源代码 https://github.com/ReactTraining/react-router/blob/master/packages/react-router/modules/withRouter.js .
HOC 虽然好用, 但写一个 "真正好用" 的 HOC 却要经过一道道繁琐的工序 (React 的新 api fowardRef 就几乎是为此而生的), 是个用着舒服写着烦的存在. 所以感觉最近大有 "少写 HOC 推崇 render props" 的思潮. 至于新的 Context api 虽然思路上和 react-redux 如出一辙, 却选择了 render props 的写法, 在我看来也是顺理成章.
Render Props 使用场景
实例 1: 一个日常的使用场景是弹窗. App 的弹窗 UI 可能千奇百怪, 但它们的功能却是类似的: 无非有个显示隐藏的状态, 和一个控制显隐的方法, 以 antd 为例:
- import { Modal, Button } from 'antd';
- class App extends React.Component {
- state = { visible: false }
- showModal = () => {
- this.setState({
- visible: true,
- });
- }
- handleOk = (e) => {
- // 做点什么
- this.setState({
- visible: false,
- });
- }
- handleCancel = (e) => {
- this.setState({
- visible: false,
- });
- }
- render() {
- return (
- <div>
- <Button onClick={this.showModal}>Open</Button>
- <Modal
- title="Basic Modal"
- visible={this.state.visible}
- onOk={this.handleOk}
- onCancel={this.handleCancel}
- >
- <p>Some contents...</p>
- <p>Some contents...</p>
- <p>Some contents...</p>
- </Modal>
- </div>
- );
- }
- }
上面是最简单的 Modal 使用实例, 但大家心中理想的使用方式是如下的:
- <div>
- <Button>Open</Button>
- <Modal
- title="Basic Modal"
- onOk={this.handleOk // 做点什么}
- >
- <p>Some contents...</p>
- <p>Some contents...</p>
- <p>Some contents...</p>
- </Modal>
- </div>
我只想写业务逻辑的 onOK, 其他部分不都是弹窗的实现细节吗, 为啥不能封装起来?
答案是可以的. 下面就使用 render props 来写一个 Pop 组件, 封装所有逻辑. 希望的最终使用方式是:
- <Pop>
- {({ Button, Modal }) => (
- <div>
- <Button>Open</Button>
- <Modal
- title="Simple"
- onOK={() => alert("everything is OK")}
- >
- <p>Some contents...</p>
- <p>Some contents...</p>
- <p>Some contents...</p>
- </Modal>
- </div>
- )}
- </Pop>
大家可以先尝试自己写一下. 我写的如下:
- import { Modal, Button } from 'antd';
- class Pop extends React.Component {
- state = { on: false };
- toggle = () => this.setState({ on: !this.state.on });
- // 将 antd 组件包裹上状态和方法
- MyButton = props => <Button {...props} onClick={this.toggle} />;
- MyModal = ({ onOK, ...rest }) => (
- <Modal
- {...rest}
- visible={this.state.on}
- onOk={() => {
- onOK && onOK();
- this.toggle();
- }}
- onCancel={this.toggle}
- />
- );
- render() {
- return this.props.children({
- on: this.state.on,
- toggle: this.toggle,
- Button: this.MyButton,
- Modal: this.MyModal
- });
- }
- }
完整的 Demo https://codesandbox.io/s/7o2y6yq0n6
简单的说, render props 将如何 render 组件的事代理给了使用它的组件, 但同时以参数的形式提供了需要重用的状态和方法给外部. 实现 UI 的自定义和功能的重用. 不过这个例子有点激进, 不仅提供了状态和方法, 还提供了带状态的组件作为参数. 如果大家有不同意见, 请务必留言, 互相学习.
实例 2: 一般的 render props 只封装 "state".React 官方文档上 Dan Abromov 给出了一个很好的例子: 鼠标跟踪的功能. 这个功能有很多应用场景, 也很好实现:
- class Mouse extends React.Component {
- state = { x: 0, y: 0 }
- handleMouseMove = e =>
- this.setState({ x: e.clientX, y: e.clientY })
- render() {
- return (
- <div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
- <p > 鼠标位于 ({this.state.x}, {this.state.y})</p>
- </div>
- )
- }
- }
但如何封装和重用这个功能呢? 比如要写一个猫的图案跟着鼠标走. 大家可以先试试. 答案如下:
- // 封装
- class Mouse extends React.Component {
- state = { x: 0, y: 0 }
- handleMouseMove = (e) =>
- this.setState({ x: e.clientX, y: e.clientY })
- render() {
- return (
- <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
- {this.props.children(this.state)}
- </div>
- )
- }
- }
- // 重用
- const Cat = () =>
- <Mouse>
- {({x,y}) =>
- <img src="/cat.jpg"
- style={{ position: 'absolute', left: x, top: y }}
- />
- }
- <Mouse>
结语
如果太长没看的话, 只有一句是我最想分享的: 当你写项目时碰到需要重用的是功能不是 UI 时, 试着用 render props 封装一个组件吧. 当然 HOC 也是解决方法, 不过关于 HOC vs render props 的讨论, 下篇再写.
来源: https://juejin.im/post/5b2f99ea6fb9a00e3a5aa511