Beace Lee

Beace Blog

Written by Beace Lee who lives and works in China building useful things. You should follow him on Twitter

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 />