在React Hooks时代,高阶组件只能感叹:既生瑜何生亮?

3,912 阅读9分钟

背景介绍

这是设计模式系列的第十节,学习的是patterns.dev里设计模式中高阶组件模式内容,由于是资料是英文版,所以我的学习笔记就带有翻译的性质,但并不是翻译,记录的是自己的学习过程和理解

关于设计模式前九节的内容,在文末会有直达链接。

写在前面

在项目开发中,我们经常会遇到几个组件共用一些逻辑,比如说多个组件共用样式授权认证全局状态,这个时候把逻辑封装成高阶组件将是一种不错的实现方法。即便现在是Hooks时代了,高阶组件的使用场景会缩小,但是部分场景还是适合用高阶组件

极简释义

一个组件接收另一个组件作为参数返回一个组件,同时将可复用的逻辑作为props传递给返回的组件

正文

高阶组件也常用英文首字母缩写HOC(Higher Order Component),这是一种设计模式,允许全局复用某些逻辑。

高阶组件是一个组件接受另一个组件作为参数,高阶组件会向被传入的组件中添加特定的逻辑参数props,然后返回附加特定逻辑的组件。

比如说我们要给一些基础组件添加通用的样式,就可以使用高阶组件:

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = (props) = <button {...props}>Click me!</button>
const Text = (props) => <p  {...props}>Hello World!</p>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

上面的withStyle是一种简单的高阶组件用法,相信大家一看就懂。

为了加深大家的理解,再举一个异步请求添加loading状态的例子,比如前面在视图和逻辑分离模式中使用到一个从API中请求并展示一系列Dog图片的DogImages示例,在那个示例中我们先使用容器组件异步请求数据,传递参数给视图组件进行展示,最后又推荐了一种Hooks的写法,代码如下:

import React from "react";
import useDogImages from "./useDogImages";

export default function DogImages() {

    const dogs = useDogImages();
    return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />);

}

// ./useDogImages
import { useState, useEffect } from "react";

export default function useDogImages() {

    const [dogs, setDogs] = useState([]);

    useEffect(() => {
        async function fetchDogs() {
            const res = await fetch(
            "https://dog.ceo/api/breed/labrador/images/random/6"
            );
            const { message } = await res.json();
            setDogs(message);
        }
        fetchDogs();
    }, []);

    return dogs;
}

在线示例

在网络环境不好时,异步请求可能需要等待好几秒,这几秒的空白内容,可能会导致用户的流失。接下来让我们稍微提升一下用户体验,在fetchDogs接口请求的过程中,在屏幕上加一个loading

通常我们会直接在DogImages这个组件里添加loading逻辑,而在项目中会有很多异步请求,给每次异步请求单独添加loading难免麻烦,接下来来看下怎么使用高级组件封装loading逻辑,然后可以用在其他异步组件中:

先来个实现一个简单的高阶组件withLoader:

function withLoader(Element) {
  return props => <Element />;
}

当然这个高阶组件直接返回了接收到的组件,还不能满足需求,我们需要得到异步请求的状态,还需要添加异步接口是否在请求中的loading状态。

为了让withLoader这个高阶组件具有更强的复用性,我们把接口API作为参数传入,从而让其他接口也能复用这个高阶组件。

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


export default function withLoader(Element, url) {
  return (props) => {
    const [data, setData] = useState(null);


    useEffect(() => {
      async function getData() {
        const res = await fetch(url);
        const data = await res.json();
        setData(data);
      }
      getData();
    }, []);


    if (!data) {
      return <div>Loading...</div>;
    }

    return <Element {...props} data={data} />;
  };
}

通过判断data是否为空,我们可以知道异步请求是否完成,并把请求到的数据传递给接收到的组件,这样就完成了高阶组件最核心的功能。接下来我们来看看怎么使用这个高阶组件?

import React from "react";
import withLoader from "./withLoader";


function DogImages(props) {
  return props.data.message.map((dog, index) => (
    <img src={dog} alt="Dog" key={index} />
  ));
}


export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

相信大家一看就懂了,这里还要细细去体会,原来hook的写法是在DogImages里调用hook异步请求拿到数据,然后展示,现在把hook的异步请求封装进withLoader高阶组件中,

那么如果在高阶组件中不封装异步请求,而是直接调用已有的useDogImages异步hooks是否可行呢?

快来在线试试吧:在线示例

这样我们创建了一个扩展性的可以接收任何组件和接口的高阶组件

  1. 当调用withLoader高阶函数,当useEffect发起请求时还没完成并调用setState时,data为空,我们返回了loading;
  2. 当请求完成并调用setState给data赋值时,我们返回了传入的组件,并把请求到的数据data传递给传入的组件,从而完成组件渲染
  3. 在DogImages导出时,通过导出withLoader包裹后的组件,从而实现相关内容区域请求时展示loading,有数据时展示数据;

组合使用

我们可以组合多个高阶组件使用。比如说上面DogImages示例,再添加一个鼠标hover整个list时,toast提示一下。

这时我们就可以创建一个高阶组件,把hover状态作为props传给DogImages。有个这个props,我们就可以在鼠标移入DogImages时Toast提示了,代码如下:

import withLoader from "./withLoader";
import withHover from "./withHover";


function DogImages(props) {
  return (
    <div {...props}>
      {props.hovering && <div id="hover">Hovering!</div>}
      <div id="list">
        {props.data.message.map((dog, index) => (
          <img src={dog} alt="Dog" key={index} />
        ))}
      </div>
    </div>
  );
}


export default withHover(
  withLoader(DogImages, "https://dog.ceo/api/breed/labrador/images/random/6")
);

// ./withHover.js
import React, { useState } from "react";


export default function withHover(Element) {
  return props => {
    const [hovering, setHover] = useState(false);


    return (
      <Element
        {...props}
        hovering={hovering}
        onMouseEnter={() => setHover(true)}
        onMouseLeave={() => setHover(false)}
      />
    );
  };
}

在线示例

通过组合withHoverwithLoader,我们同时给DogImages添加了异步请求loding和监听hover操作。

一个众所周知的高阶组件库recompose,因为hooks的冲击,现在已经停止维护了。因为大部分的高阶组件都可以被Hook取代。

Hooks

在高阶组件模式日渐式微的今天,我们也有必要来对比一下,高阶组件模式为什么会走下坡路Hooks到底强在哪里?

在许多场景下,Hook是可以代替高阶组件的。接下来让我们用hook代替withHover这个高阶函数,来写一个useHover Hook吧!

import { useState, useRef, useEffect } from "react";

export default function useHover() {
  const [hovering, setHover] = useState(false);
  const ref = useRef(null);


  const handleMouseOver = () => setHover(true);
  const handleMouseOut = () => setHover(false);


  useEffect(() => {
    const node = ref.current;
    if (node) {
      node.addEventListener("mouseover", handleMouseOver);
      node.addEventListener("mouseout", handleMouseOut);


      return () => {
        node.removeEventListener("mouseover", handleMouseOver);
        node.removeEventListener("mouseout", handleMouseOut);
      };
    }
  }, [ref.current]);


  return [ref, hovering];
}

通过导出ref,再在使用时给ref赋值,从而实现给ref绑定组件添加鼠标hover事件,在useEffect中绑定事件,在useEffect的返回方法中移除事件

这种添加之后需要移除的事件,特别使用用hook来实现

接下来就是使用这个hook了:

import React from "react";
import withLoader from "./withLoader";
import useHover from "./useHover";


function DogImages(props) {
  const [hoverRef, hovering] = useHover();


  return (
    <div ref={hoverRef} {...props}>
      {hovering && <div id="hover">Hovering!</div>}
      <div id="list">
        {props.data.message.map((dog, index) => (
          <img src={dog} alt="Dog" key={index} />
        ))}
      </div>
    </div>
  );
}


export default withLoader(
  DogImages,
  "https://dog.ceo/api/breed/labrador/images/random/6"
);

在线示例

不过通常来说,React Hooks不会完全取代高阶组件。

React Hooks是有效的,可以减少Dom树嵌套的层数。———— React文档

正如官方文档中所说,Hooks可以减少组件嵌套的层数,而高阶组件(HOC)则容易导致Dom树嵌套很深

<withAuth>
  <withLayout>
    <withLogging>
      <Component />
    </withLogging>
  </withLayout>
</withAuth>

高阶组件模式可以为很多组件添加相同的逻辑,同时把逻辑集中在一个地方。Hooks则让我们在组件内部复用相同逻辑,但在很多组件复用同一逻辑的情况下,相较于高阶组件,Hooks可能会有更高导致Bug的风险。

所以高阶组件和Hook在很多场景下可以相互替代,但他们各自也有自己的最佳使用场景

高阶组件最佳使用场景:

  1. 整个应用程序中很多组件复用相同的非定制功能时;
  2. 该组件本身在不添加自定义行为时也可以独立使用

React Hooks最佳使用场景:

  1. 每个组件需要单独定制相关逻辑;
  2. 目标行为只在局部有限的几个组件使用,而不是全局很多组件在使用;
  3. 目标行为为组件添加很多属性

案例分析

高阶函数一个比较好的用例库是Apollo Client,下面是他提供的高阶函数Hooks的两种用法:

高阶组件用法

import React from "react";
import "./styles.css";


import { graphql } from "react-apollo";
import { ADD_MESSAGE } from "./resolvers";


class Input extends React.Component {
  constructor() {
    super();
    this.state = { message: "" };
  }


  handleChange = (e) => {
    this.setState({ message: e.target.value });
  };


  handleClick = () => {
    this.props.mutate({ variables: { message: this.state.message } });
  };


  render() {
    return (
      <div className="input-row">
        <input
          onChange={this.handleChange}
          type="text"
          placeholder="Type something..."
        />
        <button onClick={this.handleClick}>Add</button>
      </div>
    );
  }
}


export default graphql(ADD_MESSAGE)(Input);

Hook的用法

import React, { useState } from "react";
import "./styles.css";


import { useMutation } from "@apollo/react-hooks";
import { ADD_MESSAGE } from "./resolvers";


export default function Input() {
  const [message, setMessage] = useState("");
  const [addMessage] = useMutation(ADD_MESSAGE, {
    variables: { message }
  });


  return (
    <div className="input-row">
      <input
        onChange={(e) => setMessage(e.target.value)}
        type="text"
        placeholder="Type something..."
      />
      <button onClick={addMessage}>Add</button>
    </div>
  );
}

在线示例

如上面的例子,使用Hooks会有更清晰数据流传递路径,使重构或者拆解成微应用时有更好的开发体验,提高可维护性

总结

高阶组件可以让很多组件复用同一套逻辑,会减少复制代码和维护的风险,同时可能会增加其他风险。通过将相同的代码写在同一个地方,实现了关注点分离和代码的DRY.

高阶组件传递的props属性可能会和组件原本的props命名冲突,从而发生属性值覆盖;当多个高阶组件嵌套时,并不容易识别逻辑源头,可能会让程序难于调试和扩展

相关推荐

第一节:高并发造成的数据统计困难?看我单例模式一招制敌

第二节:JS和迪丽热巴一样有专业替身?没听过的快来补补课...

第三节:还在层层传递props?来学学非常实用的供应商模式吧

第四节:都知道JavaScript原型,但设计模式里的原型模式你会用吗?

第五节:React Hooks时代,怎么实现视图与逻辑分离呢?

第六节:是时候拿出高级的技术了————观察者模式

第七节:前端性能优化进阶篇——动态加载模块基础补遗

第八节:在React Hook时代,Object.assign这种混合写法还要用吗?

第九节:如何使用中间件优化多对多通信?

相关活动

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情