React Hooks | 「初出茅庐」的 Hooks 何以如此快速的被我采用

940 阅读9分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

文章阅读小提示,本文字数:3000左右,阅读完需:约 10 分钟。

前言

本来标题我想写「我与 React 的缘分又延续到了 Hooks 上」,但是转念想到文章标题尽量能够简单明了的概括整篇文章要讲什么。

回归到缘分这件事上,我还在用 Vue 的时候,感觉JSX的写法很新奇,那会就开始着手学习 React 。没多久就切换到了 React 上,所以我总结了一条经验,我个人感觉这条经验也适合日常兴趣培养上:

当我犹豫要不要学习某个技术(或兴趣)的时候,大多数情况下,我会先上手。因为实践出真知,不上手我没办法确定我不需要或者不想要它。上手实践之后,不喜欢我也没有损失。漫漫黑夜,我不缺少可以消磨的时间。

后来,Hook 出现了,灵活的写法又让我沉迷其中。

但是,冷静下来,我问了自己几个问题:

  • 新特性出来就一定要用吗?
  • 除了已知的优点,Hooks 还有哪些核心优点?
  • Hooks 就一点缺点都没有吗?
  • 每次追在巨人的后面,有没有对 React 的未来做过展望?
  • 用了这么久,有没有总结一些技巧做产出?

以往我用正向的知识点列表,更多的是已知的问题,其实缺少对未知问题的思考。

后来阅读一些大佬的文章,改用自问自答的方式,先进行反向知识点罗列,加深对自己知识掌握的认知,再通过问题解答检验自己最终的学习成果。

有了就一定要用吗?

先说一个我之前的拙见,也是对技术保持一定的探索精神的支撑点:

一个技术新特性,是为了解决某些问题而出现的。

我未必需要它,这一句是肯定的。但是怎么验证这种肯定的存在呢。我一般会去尝试它、了解它。对,没错,现有尝试,再尝试中去了解。

我为什么会有这种看上去有点奇怪的顺序。是因为,一旦我只是去了解,很难对它有明确的判断。是我个人的学习习惯,有点像我对待美食,我要是没有品尝过,我说服不了自己它不好吃。

React 有个很突出的特点,就是用组件去描述和组成 UI(界面)。

Class 的一个特点是继承。但是 React 的组件之间一般不会相互继承。

所以 Class 在 React 可以发挥的才能有限。所以 React 团队为了尽善尽美,就有了本篇中的主角— Hooks

给这段文字和这个问题做个总结:

没错,有些新出现的技术特性不一定适合我当前的开发,不适合的情况下我很少会用到它。但是当它比我现在使用到的更合适的时候,我会考虑或者直接采用新特性。

Hooks 从0到1的故事

什么是 Hooks?

Hook直译是钩子。在 React 中,Hooks 是一些可以让开发者在函数组件里“钩入” state 及生命周期等特性的函数。当函数的依赖项发生变化时,函数中的目标结果会发生变化得到新的结果,这个目标结果一般是页面的 DOM 数。

注: Hook 不能在 class 组件中使用。

Hooks 诞生的锲机

React官网提到了为什么要在React中引入Hooks的原因(也可以称之为动机):

Hook解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。

正如我前面提到某些新特性的出现是为了解决一些问题的。Hooks出现之前,在React中,复杂组件可读性较低,且组件之间难易复用状态逻辑。导致的结果就是组件维护成本变高,为了解决这些问题,Recat引入了Hooks。

我为什么这么快就接受了 Hooks ?

大抵总是受到「多情却被无情恼」的困惑,我想要的Class给不了,Class给到的我用不上。我和Class背道而驰是迟早的事情。

当我了解了 Hooks 用处之后,我非常愉快的做出了选择。

对代码可复用的需要

Hooks 说

你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

我的需要

以我实际遇到的一个需求为例,我们的后台页面的操作按钮需要做权限管理。具体来说就是:

即不同角色用户可进行的页面操作可通过数据配置进行控制。超级管理员可以操作所有按钮。

如图所示:

使用 Class 组件的场景

这个需求如果使用 Class 组件实现。首先要定义一个高阶组件,获取用户权限信息,并将经过处理之后的权限数据作为 props 传给下一个组件。

高阶组件

包含处理用户权限的逻辑

/**
 * @description 高阶组件 包含处理用户权限的逻辑
 */

import React from 'react';

const withPowerButtonByUserInfo = ButtonComponent => {
  class WrappedComponent extends React.PureComponent {
    constructor(props) {
      super(props);
      this.state = {
        powerType: null, // super-超级管理员 userA-A类用户 userB-B类用户
      };
    }
    componentDidMount() {
      this.getPowerType();
    }
    /**
     * 通过接口获取当前用户权限信息
     */
    getPowerType() {
      let userInfo = {};
      this.setState({
        powerType: userInfo.powerType ? userInfo.powerType : 'super',
      });
    }

    render() {
      return <ButtonComponent {...this.state} />;
    }
  }
  return WrappedComponent;
};
export default withPowerButtonByUserInfo;

操作页面

操作页面调用 withPowerButtonByUserInfo 组件,根据 props 传入的值区分不同的 UI 展示。

/**
 * @description 展示页面
 */
import React from 'react';
import withPowerButtonByUserInfo from './components/withPowerButtonByUserInfo';

class Page extends React.Component {
  constructor(props) {
    super(props);

    this.state = {};
  }

  render() {
    const { powerType } = this.props;
    // 超级管理员或者A类用户展示操作按钮组A
    if (powerType === 'super' || powerType === 'userA') return <ButtonComponentA />;
    // 超级管理员或者B类用户展示操作按钮组B
    if (powerType === 'super' || powerType === 'userB') return <ButtonComponentB />;
  }
}

export default withPowerButtonByUserInfo(Page);

使用 Hooks 组件的场景

这个需求如果使用 Hooks 组件实现。定义一个 Hooks 组件,处理用户权限的逻辑,并将最终的数据值返回。

Hooks 组件

同样是用户权限的逻辑处理的功能实现。

/**
 * @description 用户权限的逻辑处理
 */
import React, { useEffect, useState } from 'react';

const getPowerButtonByUserInfo = () => {
  const [powerType, setPowerType] = useState('');

  /**
   * 通过接口获取当前用户权限信息
   */
  const getPowerType = () => {
    let userInfo = {};
    let type = userInfo.powerType ? userInfo.powerType : 'super';
    setPowerType(type);
  };

  useEffect(() => {
    getPowerType();
  }, []);

  return powerType;
};
export default getPowerButtonByUserInfo;

操作页面

对比 Class 组件,使用 Hooks 组件获取数据变得更加灵活。

/**
 * @description 展示页面
 */
import React from 'react';
import getPowerButtonByUserInfo from './components/getPowerButtonByUserInfo';

const Page = () => {
  const powerType = getPowerButtonByUserInfo();
  if (powerType === 'super' || powerType === 'userA') return <ButtonComponentA />;
  if (powerType === 'super' || powerType === 'userB') return <ButtonComponentB />;
};

export default Page;

小结

相较 Class 组件,使用 Hooks 组件让代码复用变得简单又灵活,且不会产生额外的组件节点。

且举例中仅仅是对权限的单一需要,如果还要加上不同页面的需要,使用 Class 组件,需要再加一层组件嵌套。而 Hooks 组件依旧是灵活又简便的。

对代码可读性的需要

Hooks 说

Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

我的需要

我要做一个滚动监听的功能,但是想把逻辑处理整合到一处,而非强制按照生命周期区分使用。

使用 Class 组件的场景

在 componentDidMount 中添加滚动监听,在 componentWillUnmount 中清除监听事件。

/**
 * @description 展示页面
 */
import React from 'react';

export default class Page extends Component {
  constructor(props) {
    super(props);

    this.state = {
      navIndex: 1,
    };
  }
  componentDidMount() {
    // 滚动监听
    window.removeEventListener('scroll', this.scrollHandle, false);
  }

  componentWillUnmount() {
    // 清除滚动
    window.removeEventListener('scroll', this.scrollHandle, false);
  }

  /**
   * 滚动监听
   */
  scrollHandle = () => {
    let SY = window.scrollY + 60;
    let content = document.getElementById('content');
    let { navIndex } = this.state;
    if (SY >= 0 && SY < content.offsetTop) {
      navIndex = 2;
    } else {
      navIndex = 1;
    }
    this.setState({
      navIndex,
    });
  };

  render() {
    let { tabList, navIndex } = this.state;

    return (
      <div className="tab">
        {tabList.map(item => {
          return (
            <div key={item.index} className="item">
              <span className={classnames({ active: navIndex === item.index })}>{item.name}</span>
            </div>
          );
        })}
      </div>
    );
  }
}

使用 Hooks 组件的场景

在 useEffect 中添加滚动监听和清除监听事件。在 Effect 返回一个函数,React 将会在执行清除操作时调用它。

/**
 * @description 展示页面
 */
import React, { useEffect, useState } from 'react';

const Page = () => {
  let [navIndex, setNavIndex] = useState(1);

  /**
   * 滚动监听
   */
  const scrollHandle = () => {
    let SY = window.scrollY + 60;
    let content = document.getElementById('content');

    if (SY >= 0 && SY < content.offsetTop) {
      setNavIndex(2);
    } else {
      setNavIndex(1);
    }
  };

  useEffect(() => {
    // 滚动监听
    window.addEventListener('scroll', scrollHandle, false);

    return () => {
      // 清除滚动
      window.removeEventListener('scroll', scrollHandle, false);
    };
  }, []);

  return (
    <div className="tab">
      {tabList.map(item => {
        return (
          <div key={item.index} className="item">
            <span className={classnames({ active: navIndex === item.index })}>{item.name}</span>
          </div>
        );
      })}
    </div>
  );
};

export default Page;

小结

在 Class 组件中,设置事件监听的代码一般被放在 componentDidMount 中,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被拆分到不同的生命周期中,而完全不相关的代码却在同一个方法中组合在一起。这样并利于开发者对代码的理解。

在 Hooks 中,可以把相互关联的方法放在一起,没有联系的方法分开。因为允许多个 Effect 的存在,来帮助关注点分离,进而提升代码的可阅读性。

今日总结

总结一下本篇文章所讲的内容

  • 新特性不一定适合当前的开发,不适合的情况下可以不去采用。当它比现在使用到的更合适的时候,可以考虑或者直接采用新特性。
  • Hooks 与 React 的搭配比 Class 更优。这种更优体现在,继承的优点用不上,灵活的钩子可以便捷的帮助 React 的使用者更改目标结果。
  • Hooks 让我心动并快速采用的点在于,它可以帮助实现复杂逻辑的复用和提升代码的可读性。

未完待续

问:为什么总结下面还会有这样一段文字呢?

答:今日总结是对本篇中心思想的总结,帮助自己重新思路,帮助读者快速记忆。未完待续是我的经验的输出,在职业生涯中,大浪淘沙之后,有一些我「撞南墙」之后的经验。我是一个相对话痨的人,所以会「知无不言言无不尽」。

最后一句话作为本篇的收尾(这句话是跟小伙伴聊天的时候想到然后略作了修改):

如果我看起来挺有经验的,是因为我拼的是时光,时光没有负我。

而你们有的是才华,短暂的时光不足以判定才华,才华是永不会被埋没的。