柚木

精读《我不再使用高阶组件》

本期精读的文章是:我不再使用高阶组件

懒得看文章?没关系,稍后会附上文章内容概述,同时,更希望能通过阅读这一期的精读,穿插着深入阅读原文。

1 引言

React 与 Vue 相比,组件为一等公民是最大特色之一。

由于组件可以作为一个 props 向下传递,因此 React 具备了高度抽象化的能力,Vue 虽然更易上手,但因 template 特点,没有所谓 props 传递组件这种概念,但这样导致在抽象能力上落后于 React。可能这是 JSX 与 template 之间的差异吧,也是变量与字符串之间的差异,变量同名但含义不同,所以可抽象,而模版靠规则和名称确定含义。

当然 Vue 也有 babel-plugin-transform-vue-jsx 这里不做展开。

强大的组件能力,导致了实践的多样性,高阶组件就是其一。高阶组件的特点是,JSX 描述的子元素,会注入到父级组件的 this.props.children 中,因此可以无入侵增强组件能力,常用比如权限、跳转、埋点、异常、描述、注入等等。

高阶组件也带来了使用中的困扰,作者这篇文章阐述了高阶组件存在的问题,值得我们了解。

2 内容概要

高阶组件由于可嵌套,如果有一环高阶组件没有将内部 wrappedComponent 暴露出来,会导致后续叠加的高阶组件都无法获取、注入到原始组件。

另外就算所有高阶组件都遵循了规范,组件也难以察觉被注入的数据是由哪些高阶组件提供的,而且高阶组件之间互相隔离,导致可能存在覆盖 props 的危险情况,这些问题高阶组件都束手无策。这体现出约定比约束更加效率,但约定的可维护性低于约束。

因此更好的解决思路可能是叫做 render props render callback function as child 这些名字的方法,组件定义如下:

// Contrived example for simplicity
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Caffeinate extends Component {
  propTypes = { children: PropTypes.func.isRequired };
  state = { coffee: "Americano" };
  render() {
    return this.props.children(this.state.coffee);
  }
}

直接将函数作为子元素,可以认为是一个匿名组件:

render(
  <Caffeinate>
    {(beverage) => <div>Drinking an {beverage}.</div>}
  </Caffeinate>,
  document.querySelector("#root")
);
//=> Drinking an Americano.

这种用法在 React Motion React Router 里都有采用。

3 精读

本质是将组件作为参数

我们看另一种写法:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Caffeinate extends Component {
  propTypes = { children: PropTypes.func.isRequired };
  state = { coffee: "Americano" };
  render() {
    return this.props.child(this.state.coffee);
  }
}

// usage
render(
  <Caffeinate child={
    (beverage) => <div>Drinking an {beverage}.</div>
  } />,
  document.querySelector("#root")
);

render props 本质上与上面这种很常规的写法没什么不同,差异在于利用了 props.children,将参数写在 JSX 的子元素中。相比高阶组件用法,这样嵌套下来,看得清楚数据流动,解决了高阶组件反复嵌套导致的各类问题:

render(
  <RenderProps1>
    {(title) => <div>
      <h1>{title}</h1>
      <RenderProps2>
        {(name) =>
          <RenderProps3>
            {(age) => {
              <div>{name}, {age}</div>
            }}
          </RenderProps3>
        }
      </RenderProps2>
    </div>}
  </RenderProps1>
)

与高阶组件对比

与 HOC 相比,render props 开放性提升明显,原本 HOC 所做的功能抽象可通过 render Props 获取,而 render 也可以访问到父级的一切:

Render Props 存在的问题

  1. this.props.children 不该作为函数调用。
  2. 渲染粒度变大,表格等需要性能优化的场景不适合。
  3. renderProps 渲染的并不是 React 组件,无法为其单独使用 reduxmobx dob 等依赖收集粒度也放不下去。

renderProps 为了解耦,让控制权从上到下传递,而底层实现不需要了解上层实现,这是解决 JSX 修改组件模版问题的方法之一,作为优化点之一,可以考虑让传入的 props 自身作为一个组件:

const View = ({title}) => <div>{title}</div>

// ...
render() {
  return (
    <Component view={View} />
  )
}

简约即美

与其绕那么大一圈,还不如回归到最普通的 props 传参,这说明 renderProps 作为其中一种特例,可能观赏价值大于其实用价值。其控制放权的思想也是为了解决组件 dom 结构定制化的问题。

但是这也涉及到限度的问题,以下就是两种极端:

render() {
  return this.props.children
}
render() {
  return (
    <div>
      <Header />
      <Sidebar>
        <Toolbox>
          <ul>
            <li>..</li>
            <li>{this.props.secondLi}</li>
            <li>..</li>
          </ul>
        </Toolbox>
      </Sidebar>
      <Footer />
    </div>
  )
}

可以看出,写出这两种代码的目的,都为了从外部控制组件结构,以至于最大限度提高组件的复用能力。其实很难在不了解组件自身含义时,妄下一个通用的结论,说 “你只要这么写,就能保证任何组件都非常通用”。

比如 Card 组件可以将 title extra 设定为 ReactNode,加上 children,其实用性已经足够了:

render() {
  return (
    <Container>
      <Title>
        {this.props.title}
        {this.props.extra}
      </Title>
      <Body>
        {this.props.children}
      </Body>
    </Container>
  )
}

再比如 Modal 也只需要对 Header Footer children 支持 ReactNode 就可以保证足够的通用性。

在业务场景,由于代码修改频率较高,复用性重要程度就没那么高。

4. 总结

作者也提到了,高阶组件在某些场景很有用,所以不会完全拒绝使用。

在不为组件做注入的场景下是高阶组件的好场景,利用其生命周期实现权限、埋点,在层级少的时候用作依赖注入也非常方便。

其实程序员在思考这些最佳实践时,与艺术家的思考方式很类似,况且这些最佳实践在不同场景、不同团队,不同项目下都有所侧重,所以不用逮着所谓最完美的实践把代码全部重构,以后也全部用一种风格写代码。就像陶瓷艺术家也不会说:我再也不做彩瓷了,因为白瓷这种颜色非常简约,在我心中是完美的,因此我宁愿一辈子只做白瓷。

这一期也想表达一个积极含义,精读周刊是不会 give up 的!

如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。