render prop vs HOC vs Hooks

重复是不可能的,这辈子都不可能写重复的代码。

当然,这句话分分钟都要被打脸。我们烦恼于频繁的增加需求。

虽然我们不能改变别人,但我们却可以尝试去做的更好,我们需要抽象,封装重复的功能或者逻辑,而不是老旧的重复着机械的复制粘贴修改。

那么我们如何去封装 React 中的组件以及逻辑呢?

在本文中,我们将深入探讨三种模式,以便了解我们为什么需要它们,以及如何正确地使用它们来构建更好的 React 应用。

引入

组件是 React 代码复用的主要单元,但如何分享一个组件封装到其他需要相同 state 组件的状态或行为并不总是很容易。

需求

引入React官网中的例子,我们需要一个 商品 List 组件,它订阅外部数据源,用以渲染商品列表:

class ItemList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假设 "DataSource" 是个全局范围内的数据源变量
      items: DataSource.getItems()
    };
  }
  componentDidMount() {
    // 订阅更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      items: DataSource.getItems()
    });
  }

  render() {
    return (
      <div>
        {this.state.items.map((item) => (
          <Item item={item} key={item.id} />
        ))}
      </div>
    );
  }
}

新需求

现在又来了一个需求,我们需要一个 订单 List 组件,它订阅外部数据源,用以渲染订单列表:

class OrderList extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      // 假设 "DataSource" 是个全局范围内的数据源变量
      orders: DataSource.getOrders()
    };
  }
  componentDidMount() {
    // 订阅更改
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      orders: DataSource.getOrders()
    });
  }

  render() {
    return (
      <div>
        {this.state.order.map((order) => (
          <Order order={order} key={order.id} />
        ))}
      </div>
    );
  }
}

ItemListOrderList 不同 - 它们在 DataSource 上调用不同的方法,且渲染不同的结果。但它们的大部分实现都是一样的:

  • 在挂载时,向 DataSource 添加一个更改侦听器。
  • 在侦听器内部,当数据源发生变化时,调用 setState
  • 在卸载时,删除侦听器。

可以想象,在一个大型应用程序中,这种订阅 DataSource 和调用 setState 的模式将一次又一次地发生。

我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。

HOC

高阶组件是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

它类似于 Mobx 中广泛使用的装饰器模式。像 Python 这样的许多语言都内置了装饰器,JavaScript也很快就会支持装饰器。HOCs 很像装饰器。

const EnhancedComponent = higherOrderComponent(WrappedComponent);

如何使用高阶组件解决上述问题?

抽离公共逻辑

我们可以编写一个创建组件的函数,比如 ItemList 和 OrderList,订阅 DataSource。

该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。

让我们编写函数 withDataSource:

// 此函数接受一个组件以及获取数据的方法
function withDataSource(WrappedComponent, getData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: getData()
      };
    }

    componentDidMount() {
       // 假设 "DataSource" 是个全局范围内的数据源变量
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: getData()
      });
    }

    render() {
      // ... 使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

第一个参数是被包装组件。第二个参数是订阅数据的方法。

组件复用

商品列表

class List extends React.Component {
  constructor(props){
    super(props);
  }
  //...
  render() {
    return (
        <div>
        {this.props.data.map(item=>{
          <Item item={item} key={item.id} />
        })}
        </div>
    );
  }
}
const ItemList = withDataSource(ItemList,DataSource.getItems);

这里用定义好的高阶组件包装List组件,他会将items列表作为props注入List

当渲染 ItemList 时, withDataSource 将传递一个 data prop,其中包含从 DataSource.getItems 检索到的最新商品

订单列表

class List extends React.Component {
  constructor(props){
    super(props);
  }
  //...
  render() {
    return (
        <div>
        {this.props.data.map(item=>{
          <Order order={order} key={order.id} />
        })}
        </div>
    );
  }
}
const OrderList = withDataSource(List,DataSource.getOrders);

这里用定义好的高阶组件包装List组件,他会将items列表作为props注入List

当渲染 OrderList 时, withDataSource 将传递一个 data prop,其中包含从 DataSource.getOrders 检索到的最新订单

小结

HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。

render prop

术语 render prop 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。

具有 render prop 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现自己的渲染逻辑。

更具体地说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

如何使用render prop组件解决上述问题?

抽离公共逻辑

class List extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      list: props.getList()
    };
  }
  componentDidMount() {
    // 订阅更改   假设 "DataSource" 是个全局范围内的数据源变量
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    // 清除订阅
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      list: props.getList()
    });
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)} // props.render为一个方法,该方法接受一个list对象
      </div>
    );
  }
}

组件复用

商品列表

class Page extends React.Component {
  //...
  render() {
    return (
        <List getList={DataSource.getItems}
              render={list=>(
                <div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
             )} /> 
    );
  }
}

订单列表

class Page extends React.Component {
  //...
  render() {
    return (
      <List getList={DataSource.getOrders}
                    render={list=>(
                      <div>{list.map(item=>(<Order order={order} key={order.id} />))} </div>
                   )} /> 
    );
  }
}

::: tip
render prop 是因为模式才被称为 render prop ,
你不一定要用名为 render 的 prop 来使用这种模式。
事实上, 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 render prop
:::

另一种方式

尽管之前的例子使用了 render,我们也可以简单地使用 children prop!

class List extends React.Component {
  render() {
      return (
        <div>
          {this.props.children(this.state)} // props.render为一个方法,该方法接受一个list对象
        </div>
      );
    }
}
<List getList={DataSource.getItems}
      children={list=>(
      <div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
 )} /> 

::: tip
children prop 并不真正需要添加到 JSX 元素的attributes列表中。相反,你可以直接放置到元素的内部
:::

<List getList={DataSource.getItems}>
      {list=>(
        <div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
      )}
 <List/> 

注意事项

将 Render Props 与 React.PureComponent 一起使用时要小心。

如果你在 render 方法里创建函数,
那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。
因为这种情况下父组件每次渲染对于 render prop 将会生成一个新的值,这会导致浅比较 props 的时候总会得到 false。

为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:

class Page extends React.Component {
  // 定义为实例方法,`this.renderItem`始终
  // 当我们在渲染中使用它时,它指的是相同的函数
  renderItem(list) {
    return (
     <div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
    )
  }
    return <Item Item={Item} />;
  }

  render(){
    return (
      <List getList={DataSource.getItems}>
          {this.renderItem()}
       <List/> 
    );
  }
}

Hook

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

目前为止,在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props高阶组件,现在让我们来看看 Hook 是如何在让你不增加组件的情况下解决相同问题的

如何使用自定义 Hook解决上述问题?

抽离公共逻辑

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 Hook 都是函数,所以也同样适用这种方式。

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

import React, { useState, useEffect } from 'react';

function useData(getData) {
  const [list, setList] = useState([]);
  useEffect(() => {
    function handleChange() {
      setList(getData());
    }

    DataSource.addChangeListener(this.handleChange);
    return () => {
      DataSource.removeChangeListener(this.handleChange);
    };
  });
  }
  return list;

此处 useData 的 Hook 目的是订阅某个列表。这就是我们需要将 获取某列表的方法 getData 作为参数并且返回list列表的原因。

组件复用

现在让我们看看应该如何使用自定义 Hook。

我们一开始的目标是在 ItemList 和 OrderList 组件中去除重复的逻辑,即:这两个组件都想订阅DataSource的变化。

现在我们已经把这个逻辑提取到 useData 的自定义 Hook 中,然后就可以使用它了:

商品列表

function ItemList(props) {
  const list = useData(DataSource.getItems);
  return (
    <div>
    {list.map(item=>(<Item item={item} key={item.id}/>))}
    </div>
  )
}

订单列表

function OrderList(props) {
  const list = useData(DataSource.getOrders);
  return (
    <div>
    {list.map(item=>(<Order order={order} key={order.id}/>))}
    </div>
  )
}

这段代码运行结果等价于render props 和 高阶组件吗?等价,它的工作方式完全一样。

如果仔细观察,会发现我们没有对其行为做任何的改变,我们只是将两个函数之间一些共同的代码提取到单独的函数中。

自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

对比

  • render prop
    render-prop.jpg

render prop看起来像是一把精准的手术刀。它对外提供一个渲染接口,方便对其进行局部定制。它的粒度更细。

  • HOC
    hoc.jpg

HOC看起来则是完整的手术台,它可以对WrappedComponent进行宏观上的控制,并且WrappedComponent也具有完整的组件生命周期。

它可以传入多个参数,适用范围广,倾向于更好地执行更复杂的操作。

  • Hooks

Hooks使我们可以更优雅,更简单的复用逻辑。Hooks出来之后,前面的两个看似强大的模式都成了纸老虎。其他不说,首先从代码量上,Hooks就已经完胜了。
并且随着Hooks的推广,更多Hooks的潜能也会被逐渐发掘出来。

总结

  • 一般情况下,使用 Hooks 就可以了
  • 如果希望将特性仅应用于组件树的一部分,使用 Render Props
  • 如果希望对组件进行无侵入式的全方位增强,使用 HOCs

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335