HOC VS Children VS Render Props

November 25, 2018

简介

  • HOC(Higher-Order Components) 是react中通过高阶组件来抽象逻辑的用法。它并不是React提供的API,而是一种通过在JavaScript函数传参的形式来减少代码逻辑。
  • Children 是 React 通过 props 向下暴露的 API,可以简单的理解为在任何被JSX标签包裹的组件都是其外层组件的孩子(children)。
  • Render Props 是通过函数的形式来传递 props 达到组件共享数据的一种方式。

共同的作用

其实,这三种在 React 中的组件构建方式最核心的目的只有两个,要么是组件间相互通信,要么是将公共的逻辑抽象。

我们都知道 React 通过组件作为单位来整合最基础的Page。除了直接书写html元素之外,组件(Component)可以理解为 React 应用中的最小单位了。有时候为了更加友好的书写组件的测试,拆分业务逻辑,最大可能得复用组件,就不得不需要将组件间拆分的很细,与此同时,由于 React 中单向数据流的特性,以上的设计未免带来许多困扰。

这篇文章,用来梳理一下在不同场景中,通过 props 进行通信的三种选择。会提到一些优劣势的问题和取舍关系。

Children

在 React 中,可以将除根节点以外的任何JSX元素理解为是某个元素的children,同样,只要不是叶子节点,任何元素也可以理解为某个元素的parent。

可以通过this.props.children拿到当前元素都孩子节点,children 的存在就像一个插槽,你可以在父元素的某个具体位置,插入任何 React Element。例如,在web页面经典的三栏布局中,需要有PageHeader,Sidebar始终保持在页面固定位置,而Content需要动态的变化,因此产生了类似如下的代码。

// LayoutComponent
<Layout>
<Header></Header>
<div>
<Sidebar></Sidebar>
<Content>{this.props.children}</Content>
</div>
</Layout>

通过引用LayoutComponent,具体的内容便显示在Content组件中间

import LayoutComponent from 'LayoutComponent';
// in render return
<LayoutComponent>
hello, I'm content in LayoutComponent!
</LayoutComponent>

通过以上的方式动态的插入 children ,在任何页面只需被LayoutComponent 包裹,就可以使用该布局,达到了复用Layout的目的。

这其实是一个很简单的操作,类似一些模板引擎也可以做到类似的效果。更重要的是,假如 Layout 需要向 children 动态传入一些数据,类似的,Layout 可能有一些全局配置,是通过路由来改变,而 content 中如何接收到路由的信息呢?可能没有太好的办法,this.props.children 毕竟不是一个可以传递参数的函数。

还好,React 中的静态方法可以帮助我们。 Class Component 的写法,本质上是通过React.CreateElement 来实现的,React 还提供了CloneElement的方法,来克隆一个组件。对于次场景,可以通过CloneElement的第二个参数来实现对 children 的传参。

React.cloneElement(this.props.children, { routes: routes })

上面我们通过cloneElement 将 routes 信息传递给了 children,在children中可以通过 this.props.routes 的方式获取到路由信息。

Render Props

顾名思义,render props的实现原理就是根据名为render的props,通过改变render,来改变自身的渲染逻辑。

Render中接受一个函数,通过函数方式返回一个新的组件,只要有函数就可以通过传递参数的方式动态传递变量。例如如下代码通过传递count props来传递初始化的值。

import RenderProps from './components/RenderProps';
<RenderProps render={count => <Hello onClick={this.onClick} />}/>
import React from 'react';
class RenderProps extends React.Component {
state = {}
render() {
return (
<div>{this.props.render(10)}</div>
)
}
}
export default RenderProps;

可以看到render props的方式每次都创建一个function,若参数中的值频繁变化,可以采用这种方式,类似于react-motionreact-router的库都会采用这种方式。

其实在children props也支持这种方式,类似如下写法。

import RenderProps from './components/RenderProps';
<RenderProps>
{count => <Hello onClick={this.onClick} />}
</RenderProps>
import React from 'react';
class RenderProps extends React.Component {
state = {}
render() {
return (
<div>{this.props.children(10)}</div>
)
}
}
export default RenderProps;

在 react 中也提提到了这一特点。

It’s important to remember that just because the pattern is called “render props” you don’t have to use a prop named render to use this pattern. In fact, any prop that is a function that a component uses to know what to render is technically a “render prop”.

也就是说任何的props影响到渲染的props都可以这样去实现,并不局限于render。

总之,render props 通过指定了children的props,来进行通信,所以,对于改变的props,父组件是有绝对知情权的,也就是说父组件需要知道子组件用 props 来具体做什么。因此,render props 通常使用在对 props 经常变化,并且对子元素严格把控的场景下。

HOC

HOC其实来源于HOF,high-order function。高阶函数的本质是将其他函数作为自己的参数或者返回值为函数。

在React中,其他函数指的就是component。HOC有点像mixin,react 通过这种方式让 mixin 支持了class。作为一种抽象方式,HOC方式不仅仅可以传递children,还可以传递事件、数据等等。

例如,假设有一组类似的计数组件都包含onClick这样的逻辑,但是并不确定组件如何构成,但是知道组件肯定需要这样一种业务逻辑。这就是HOC的关键。

你是知道组件需要做什么事的,表单提交还是公共逻辑的提取。但是具体长什么样子,由谁来做并不是你的重点。重点只需要拥有该方法。

import React from 'react';
function Wrapper(WrapperedComponent) {
return class extends React.Component {
onClick = count => {
console.log('hoc count', count);
}
render() {
return (
<div>
<WrapperedComponent onClick={this.onClick} />
</div>
)
}
}
}
export default Wrapper;
import HOCComponent from './components/HOC';
const Hoc = HOCComponent(Hello);
<Hoc />