趣谈 React Hooks

简介: React Hooks 的基本操作

大家好,我是小闲。是一名前端工程师,在我日常编码中,很大一部分时间都是在写组件。那怎么才能缩短写组件的时间?我会回答:复用。

你可能在想,不就是复用吗,谁都知道。

但,如何复用?通过什么方式复用?我们先从 React 组件讲起。

Class 组件

React 组件是什么?

我愿意称 React 组件为 UI 和 逻辑 的聚合体,当然,这二者在组件中的份额有时候会差的很多,有的组件只负责 UI 展示,有的组件只负责逻辑。前者是比较容易复用的,那组件中的逻辑怎么复用,为了解答这个问题,我要在 React 的世界建一个:

数字化养猪场!

初识 Class 组件:智能猪圈

养猪场里面肯定得有猪,我为每头猪配备了智能穿戴设备,可以通过调用该设备的接口获取每头猪的信息

import getDeviceInfo from 'getDeviceInfo'
const info = await getDeviceInfo();
console.log(info)
{
  name: '小猪',
  appetite: 5,
  weight: 200
  ...
}

接下来我们要建一个智能猪圈,它需要识别穿戴设备,从而知道哪些猪食欲不振 (当appetite < 5),然后将其放出猪圈玩耍,让我们来实现一下这个逻辑。

class PigBed extends React.Component<Iprops, IState> {
  listener: null | NodeJS.Timer = null;
  constructor(props: Iprops) {
    super(props);
    this.state = {
      pigList: [10521, 10522, 10523, 29, 390],
    };
  }
  componentDidMount() {
    this.listener = setInterval(() => {
      const moreAppetiteList = this.state.pigList.filter((id) => {
        // 获取猪的食欲,并保留大于 5 的猪猪
        const { appetite } = getDeviceInfo(id);
        return appetite >= 5;
      });
      this.setState({
        pigList: moreAppetiteList,
      });
    }, 1000);
  }
  componentWillUnmount() {
    // 销毁的时候去掉监听
    if (this.listener) {
      clearInterval(this.listener);
      this.listener = null;
    }
  }
  render() {
    return (
      <div>
        <h1>猪圈</h1>
        <h2>当前有猪:{this.state.pigList.join('、')}</h2>
      </div>
    );
  }
}

很容易实现了一个 Class 组件,并且建好了初代猪圈,我们的智能化猪圈让养殖的猪比市场上的猪 肉质更好,我们的养猪场也得以扩建!我准备增加 猪猪乐园!

高阶组件:猪猪乐园

猪猪乐园可以让郁闷的猪尽快恢复食欲,各种娱乐设备一应俱全。和智能猪圈不同的是,猪猪乐园里,只保留食欲不振 (appetite < 5)的猪。

逻辑和智能猪圈的逻辑很相似,作为机智的猪厂长,我肯定不会用复制粘贴来复用。

React 提供了“高阶组件”以解决在 class 组件中的逻辑复用问题。

高阶组件是参数为组件,返回值为新组件的函数

如果我们把相关业务逻辑放到高阶组件里,想获取小猪食欲度的组件作为参数传入这个组件,并包装 props 后再返回不就可以复用了。来看看代码。

// WrappedComponent:想要获取 Appetite 的组件
// selectData 获取哪些数据作为 data 传入 WrappedComponent中
function WithAppetite(
  WrappedComponent: React.ComponentClass<WrappedComponentProps>,
  selectData: (arr: ImoreAndLess) => number[],
) {
  return class extends React.Component<Iprops, IState> {
    listener: null | NodeJS.Timer = null;
    constructor(props: Iprops) {
      super(props);
      this.state = {
        pigList: [10521, 10522, 10523, 29, 390],
        data: [],
      };
    }
    componentDidMount() {
      this.listener = setInterval(() => {
        const moreAppetiteList: number[] = [];
        const lessAppetiteList: number[] = [];
        this.state.pigList.forEach((id) => {
          // 获取猪的食欲,并保留大于 5 的猪猪
          const { appetite } = getDeviceInfo(id);
          if (appetite >= 5) {
            moreAppetiteList.push(id);
          } else {
            lessAppetiteList.push(id);
          }
        });
        this.setState({
          data: selectData({
            moreAppetiteList,
            lessAppetiteList,
          }),
        });
      }, 1000);
    }
    componentWillUnmount() {
      if (this.listener) {
        clearInterval(this.listener);
        this.listener = null;
      }
    }
    render() {
      return <WrappedComponent data={this.state.data} {...this.props}/>;
    }
  };
}
export default WithAppetite;

我们所有的业务逻辑都挪到 WithAppetite中,并且它接受一个组件 以及 一个函数,该函数可以挑选需要的属性。使用时:

import React from 'react';
import Pig from '@/assets/pig.svg';
import WithAppetite from './WithAppetite';
interface Iprops {
  data: number[];
}
class PigPark extends React.Component<Iprops> {
  listener: null | NodeJS.Timer = null;
  render() {
    return (
      <div>
        <h1>猪猪乐园</h1>
        {this.props.data.map((id) => {
          return (
            <div key={id}>
              <img src={Pig} style={{ marginLeft: '10px', width: '60px' }} />
              id: {id}
            </div>
          );
        })}
      </div>
    );
  }
}
export default WithAppetite(PigPark, ({ lessAppetiteList }) => {
  return lessAppetiteList;
});

可以看到我们非常容易的实现了猪猪乐园组件,它只收留食欲不振的猪猪。有了高阶组件 WithAppetite我们就有快速建立更多设施的可能性,之后根据食欲参数再做细分,实行更精细差异化管理!

就在我做着养猪场上市的大梦时,突然我想到,为了猪猪的卫生,应该在猪圈里建造洗澡房。

嵌套问题之:猪猪洗澡房

在猪圈里建洗澡房,我需要用智能摄像头来识别猪的洁净程度,将邋遢的猪 ( clean < 5) 放到洗澡房,我用下面这个接口可以获取猪的清洁程度:

import getCameraInfo from 'getCameraInfo'
const info = await getCameraInfo(id);
console.log(info)
{
  name: '小猪',
  // 整洁度
  clean: 5
  ...
}

万一别的组件也需要知道哪些小猪比较邋遢怎么办,有了上次的经验,我直接将其抽成高阶组件 withClean,它的结构和 WithAppetite 差不多,就不贴实现了。

使用时:

export default WithAppetite(WithClean(PigBed), ({ lessAppetiteList }) => {
  return lessAppetiteList;
});

WithClean高阶组件包裹猪圈组件,再传递给 WithAppetite高阶组件(高阶组件嵌套)。

高阶组件的嵌套问题

功能实现了,但如果我还要在猪圈里加设施,继续使用高阶组件来复用逻辑,就存在几个问题:

  • 组件嵌套的越来越多会形成嵌套地狱,对于排查问题是个灾难
  • 高阶组件比较绕,多了层组件,数据可能在传递中丢失
  • 对于新手的理解成本也上升了

那么有没有更好的办法复用逻辑呢?

Class 组件的逻辑分散问题

对于 Class 组件而言,相同的业务逻辑被分散在各个钩子函数中,当我们读一个陌生组件的代码时,需要目光上下跳跃,这无疑增加了我们的阅读成本。

那么有没有更好的办法让业务逻辑更聚合呢?

这两个问题的答案可能都指向了 Hook

React Hooks

简介

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。(我们不推荐把你已有的组件全部重写,但是你可以在新组件里开始使用 Hook。)

对于官网这种概念类的描述我不是很在意,我现在最在意的是,Hooks 能给我的数字化猪厂带来什么?

我迫不及待的想再建一个使用 Hooks猪圈!

变量之 useState

建猪圈之前先介绍两个我们用到的 Hooks 函数

我们之前在class 组件中,变量是存在 this中的,现在 Hooks是在函数组件中,那变量应该存在哪儿呢?

这就是我们要学习的第一个 Hooks函数:

const [data, setData] = useState();

我们定义了一个名为 data的变量,并且可以通过 setData来为其赋值。

在模版中渲染的时候,也就可以直接使用 data,比如:

function PigName() {
  const [username, setUsername] = useState('五花肉');
  return (
    <div>
      <span>{ username }</span>
      <button onClick={() => {setUsername('红烧肉')}}>切换用户名</button>
    </div>
  )
}

这样我们就实现了展示猪猪的名字,并且在点击按钮的时候通过调用 setUsername来切换菜系......哦不,名字。

生命周期之 useEffect

你一定好奇,我们的生命周期跑哪儿去了?这就是我们要认识的第二个 Hook

useEffect(() => {
  // 挂载
  addEventListener('监测猪猪睡觉')
  // 卸载
  return () => {
    removeEventListener('监测猪猪睡觉')
  }
})

当组件挂载的时候,useEffect 的函数参数会被调用,就会监听逻辑,当组件卸载的时候,useEffect 的返回值函数会被调用,移除监听事件。

componentDidUpdate钩子要怎么替代呢,要知道我们经常用它来做性能优化,当变量变化时再去做某些操作,比如这样:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

有了 useEffect你可以这么做:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count])

第二个参数是一个数组,我们可以传入一个或多个依赖项,当 count变化时,才会执行该 Hook

使用 Hooks 的规则

只能在 React 函数中使用 && 不要在循环,条件或嵌套函数中调用 Hook

前一句话很容易理解,为什么不能在循环条件或嵌套函数中调用呢?

const [data, setData] = useState(-1);
const [list, setList] = useState([1,2,3]);

如果我们写了这两个变量,作为 React 怎么区分呢?要知道我们只传了初始值给 useState,按照以往的经验,React 以我们的传入参数是区分不了这两个变量的。

其实,它是通过 hooks 调用的顺序来区分的,所以就有了这条规则,因为循环,条件和嵌套函数可能会让hooks调用的顺序发生变化,因此我们要把 Hooks 的调用写在函数的最顶层。

"Hooks 类型" 的 猪圈

基于以上,我们就可以建造新猪圈了:

import React, { useEffect, useState } from 'react';
export default function (props) {
  const { pigList } = props;
  const [data, setData] = useState<number[]>();
  useEffect(() => {
    let listener: null | NodeJS.Timer = null;
    // 组件 mount
    listener = setInterval(() => {
      const moreAppetiteList: number[] = pigList.filter((id) => {
        return getDeviceInfo(id).appetite > 5;
      });
      setData(moreAppetiteList);
    }, 1000);
    // 组件 unMount
    return () => {
      if (listener) clearInterval(listener);
      listener = null;
    };
  });
  return (
    <div>
      <h1>猪圈</h1>
      {data?.map((id) => {
        return (
          <div key={id}>
            <img src={Pig} style={{ marginLeft: '10px', width: '60px' }} />
            id: {id}
          </div>
        );
      })}
    </div>
  );
}

花了几分钟,我就又建了一个猪圈,而且旧的 class 组件的猪圈也能共存。

hooks的代码比 class组件更简洁,而且不用将逻辑代码分散在各处钩子函数中。

像这样,不同的业务逻辑可以分开放,这样我们就解决了 Class 组件的逻辑分散问题。

那,怎么复用呢?这就要引出另外一个概念 自定义 Hooks

自定义 Hooks 是以 use开头,由我们自己封装的 hooks 函数,在其内部也可以使用原生hooks,比如上文的 useState useEffect等等。

让我们把上段代码改造一番:

// useAppetite.tsx
import { useEffect, useState } from 'react';
import getDeviceInfo from '@/service/getDeviceInfo';
export default function useAppetite(pigList: number[]) {
  const [data, setData] = useState<number[]>();
  useEffect(() => {
    let listener: null | NodeJS.Timer = null;
    listener = setInterval(() => {
      if (!pigList?.length) {
        return;
      }
      const moreAppetiteList: number[] = pigList.filter((id) => {
        return getDeviceInfo(id).appetite > 5;
      });
      setData(moreAppetiteList);
    }, 1000);
    return () => {
      if (listener) clearInterval(listener);
      listener = null;
    };
  }, [pigList]);
  return data;
}

在调用时:

import useAppetite from '@/useAppetite';
const data = useAppetite(list);

这样就可以在函数组件中直接拿到数据了。我们完成了获取小猪食欲值的逻辑复用,我们又解决了高阶组件的复用问题。

数字化猪圈有了 Hooks这个工具一定会继续做大做强,再创辉煌!

常用的 Hooks

在我的数字化猪场不断的迭代中,有几个 hooks 用的比较多。

跨层级传递 propsuseContext

如果你想在组件中跨层级传递数据,通过props能做到,但是需要在每个组件都要传递。useContext可以帮你更快捷的共享数据。

第一步,创建上下文:

const pigInfo = {
  list: [1,2,3,4]
}
const PigContext = React.createContext(pigInfo);

第二步,用 Provider 包装父组件:

function Parent() {
  return (
    <PigContext.Provider value={pigInfo}>
      <Child />
    </PigContext.Provider>
  );
}

第三步,就可以在子组件或孙子组件或重孙子组件......直接引用

function Child(props) {
  return (
    <div>
      <Grandson />
    </div>
  );
}
function Grandson() {
  const { pigInfo } = useContext(PigContext);
  return (
    <span>现在有猪猪:{ pigInfo }</span>
  );

状态管理:useReducer

如果你的 state比较复杂,就可以用 useReducer来管理(这是一个 Redux 替代方案),他的使用方法基本和 Redux类似:

const initialState = {count: 0};
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

更方便的是,如果你的子组件或者孙子组件想修改父组件 state的值,可以直接用 dispatch来修改。

性能优化:useMemo

如果你想当某一状态变化时再进行计算,比如,当 url 更新的时候再去拿到计算后的 query 值:

import qs from 'query-string'
function QueryMap() {
  const id = useMemo(() => {
    return qs.parse(location.search)?.id || null
  }, [location])  
  return <></>
}

useMemo 的第二个参数可以传入依赖数组,当依赖值更新是,第一个参数才会重新计算。这样可以避免每次渲染时都重新计算 id,也是我们日常项目中最常见的优化手段。

Ahooks

随着我的养猪场规模不断扩大,有一天我突然发现,我给猪猪身上安装的智能设备偶发的会出问题,有时候请求设备获取猪猪信息不会响应。

于是我想到,可以增加请求重试逻辑。正当我准备动手,隔壁的农场主过来遛弯,他告诉我他的数字化农场开的比较早,积攒了很多可以复用的自定义hooks,已经实现了通用的请求 hooks,而且已经支持了请求重试,我拿去直接用就行。

他还把这个 hooks 库开源了:https://ahooks.js.org/zh-CN/hooks/use-request/index

我迫不及待的试了试,果然方便:

const { data } = useRequest(() => {
  return getDeviceInfo(id)
}, {
  retryCount: 3,
});

这样就支持了请求重试,我就不用去关注怎么实现了,数字化猪场的成本再一次缩减!

他这个 ahooks库果然强大,单单是 useRequest就支持了请求轮训、依赖刷新、防抖、节流、缓存......

以下这些是我用的比较多的 hook

  • useClickAway 可以监听目标元素外的点击事件,比如你要做一个弹窗,点击弹窗外就关闭的需求就可以用它
  • useDebounce 和 useThrottle:防抖和节流
  • useUrlState 像操作 state 一样的操作 url 参数
  • useDocumentVisibility 浏览器页面是否切换到后台
  • useAsyncEffect 在 Effect 中使用 async await

更多的 hooks 你可以仔细阅读 ahooks 文档,在开始写代码之前,先看看文档中是否已有能直接使用的 hooks,这样我们的效率就会提升很多!

总结

对于数字化养猪场,我们刚开始使用 class 组件来建造设施,但也引来了组件逻辑复用问题,我们使用高阶组件来做复用,又发现高阶组件虽然能复用,但其带来了新的高阶组件嵌套问题,进一步提高了我们的维护成本。我们又引入 hooks使用函数组件来复用,不仅不用嵌套式写法,还让之前散落在各种钩子函数中的逻辑聚合起来,维护起来更简单了。最后,我们发现,开源的 ahooks封装了很多好用的 hooks 函数,我们在之后的迭代中可以使用它从而提高开发效率。

正当我坐在数字化养猪场门口的槐树下乘凉时,隔壁县的王厂长找了过来:“看新闻上说你的数字化养猪厂办的不错,你的先进经验能不能复用到我的传统养猪厂里。”

我想了想说:“可以......”

"我们要先做猪圈组件复用!" 欲知后事如何,请听下回分解。

相关文章
|
5月前
|
前端开发 JavaScript
React Hooks 入门基础(详细使用)
React Hooks 入门基础(详细使用)
26 0
|
1月前
|
前端开发
【边做边学】系统解读一下React Hooks
【边做边学】系统解读一下React Hooks
|
5月前
|
前端开发 JavaScript 算法
【React学习】—React简介(一)
【React学习】—React简介(一)
|
7月前
|
前端开发 JavaScript
React-Hooks开篇和React-Hooks-useState
React-Hooks开篇和React-Hooks-useState
29 0
React-Hooks开篇和React-Hooks-useState
|
8月前
|
存储 缓存 前端开发
react hooks 全攻略
react hooks 全攻略
|
11月前
|
设计模式 存储 缓存
|
前端开发
react实战笔记114:react中usecallback的使用
react实战笔记114:react中usecallback的使用
59 0
react实战笔记114:react中usecallback的使用
|
前端开发 JavaScript 测试技术
学习 React Hooks 可能会遇到的五个灵魂问题
学习 React Hooks 可能会遇到的五个灵魂问题
99 0
学习 React Hooks 可能会遇到的五个灵魂问题
|
缓存 前端开发 JavaScript
趣谈 React Hooks
大家好,我是小闲。是一名前端工程师,在我日常编码中,很大一部分时间都是在写组件。那怎么才能缩短写组件的时间?我会回答:复用。你可能在想,不就是复用吗,谁都知道。但,如何复用?通过什么方式复用?我们先从 React 组件讲起。Class 组件React 组件是什么?我愿意称 React 组件为 UI 和 逻辑 的聚合体,当然,这二者在组件中的份额有时候会差的很多,有的组件只负责 UI 展示,有的组件
84 0
趣谈 React Hooks
|
前端开发 API
一次完整的react hooks实践
一次完整的react hooks实践
111 0
一次完整的react hooks实践