fengshi123/blog

一文搞定常用的自定义 React Hooks

fengshi123 opened this issue · 3 comments

前言

通过上一篇文章《一文归纳 React Hooks 常用场景》,我们根据使用场景分别进行举例说明,帮助你认识理解并可以熟练运用 React Hooks 大部分特性了。本文则对 hooks 进一步加深,让我们通过自定义一些 hooks,解决我们在平时项目中非常常用的需求场景,做到代码高复用低耦合,从而加深对 hooks 的理解和运用。

辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

1、实现自定义的 useMount

首先我们自定义一个 useMount hook,其功能为在 Dom 渲染之后执行相关函数,即类似于 class 组件写法中的 componentDidMount 生命周期钩子的功能。
我么基于以下原理实现:如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。如果在函数组件中实现该功能,即代码如下所示

useEffect(() => {
  console.log('mount');
}, []);

现在我们将这个功能进行抽取,封装成为 useMount hook,则可以如下实现,其中该钩子支持传入一个回调执行函数 fn 作为参数。

import { useEffect } from 'react';

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []);
};

export default useMount;

现在我们就可以在相关业务场景中使用这个 useMount hook 了,如下所示,只会在 MyPage 初次渲染时执行一次 fun,即使我们多次点击 button,使 count 不断增加,页面不断更新,也不会再执行 fun。

import React, { useCallback, useState } from 'react';
import useMount from './useMount';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    console.log('mount');
  }, []);

  useMount(fun);

  return (
    <div >
      <button type="button" onClick={() => { setCount(count + 1); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

2、实现自定义的 useUnmount

本节我们自定义一个 useUnmount hook,其功能为在 Dom 卸载之前执行相关函数,即类似于 class 组件写法中的 componentWillUnmount 生命周期钩子的功能。
我么基于以下原理实现:如果 effect 有返回一个函数,React 将会在执行清除操作时调用它。如果在函数组件中实现该功能,即代码如下所示

useEffect(() => () => {
  console.log('unmount');
});

现在我们将这个功能进行抽取,封装成为 useUnmount hook,则可以如下实现,其中该钩子支持传入一个回调执行函数 fn 作为参数。

import { useEffect } from 'react';

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    fn();
  }, []);
};

export default useUnmount;

现在我们就可以在相关业务场景中使用这个 useUnmount hook 了,如下所示,只会在 MyComponet 卸载时执行一次 fun。

import React, { useCallback, useState } from 'react';
import useUnmount from './useUnmount';

const MyComponent = () => {
  const fun = useCallback(() => {
    console.log('unmount');
  }, []);

  useUnmount(fun);

  return <div>Hello World</div>;
};


const MyPage = () => {
  const [state, setState] = useState(true);

  return (
    <div >
      {state && <MyComponent />}
      <button type="button" onClick={() => { setState(!state); }}>
        切换
      </button>
    </div>
  );
};

export default MyPage;

3、实现自定义的 useUpdate

我们都知道如果想让 function 组件重新渲染,我们不得不更新 state,但是有时候业务需要的 state 是没必要更新的,我们不能仅仅为了让组件会重新渲染而强制让一个 state 做无意义的更新,所以这个时候我们就可以自定义一个更新的 hook 来优雅的实现组件的强制更新,类似于 class 组件的 forceUpdate 的功能,实现代码如下

import { useCallback, useState } from 'react';

const useUpdate = () => {
  const [, setState] = useState({});

  return useCallback(() => setState({}), []);
};

export default useUpdate;

useUpdate 的使用实例如下所示,点击按钮时,调用 update,会看到 Time 的值在变化,说明组件已经强制更新了。

import React from 'react';
import useUpdate from './useUpdate';

const MyPage = () => {
  const update = useUpdate();

  return (
    <div >
      <button type="button" onClick={update}>
      Time: {Date.now()}
      </button>
    </div>
  );
};

export default MyPage;

4、实现自定义的 usePrevious

平时在实现需求时,经常需要保存上一次渲染时 state 的值,so 这个 hook 就是用来保存上一次渲染状态的。如下所示为实现逻辑,主要用到 useRef.current 来存放变量。

import { useRef } from 'react';

function usePrevious<T> (state: T): T|undefined {
  const prevRef = useRef<T>();
  const curRef = useRef<T>();

  prevRef.current = curRef.current;
  curRef.current = state;

  return prevRef.current;
}

export default usePrevious;

usePrevious 的使用实例如下所示,当点击按钮使 count 增加时,previous 会保留 count 的上一个值。

import React, { useState } from 'react';
import usePrevious from './usePrevious';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const previous = usePrevious(count);

  return (
    <div >
      <div>新值:{count}</div>
      <div>旧值:{previous}</div>
      <button type="button" onClick={() => { setCount(count + 1); }}>
        增加
      </button>
    </div>
  );
};

export default MyPage;

5、实现自定义的 useTimeout

在 hook 中,我们使用 setTimeout 之后,需要在 dom 卸载时,手动进行 clearTimeout 将定时器移除,否则可能造成内存泄漏。假设我们在项目中多次用到,那我们则需要多次重复写移除代码,并且有时候可能由于疏忽,将其遗忘。so,为什么不能将它封装成 hook,在需要的时候调用即可。

import { useEffect } from 'react';

function useTimeout (fn: () => void, delay: number) {
  useEffect(() => {
    const timer = setTimeout(() => {
      fn();
    }, delay);
    return () => {
      clearTimeout(timer); // 移除定时器
    };
  }, [delay]);
}

export default useTimeout;

如下所示,我们只需要告诉 useTimeout 多少毫秒去调用哪个方法,不需要再去考虑移除定时器的事情了。

import React, { useState } from 'react';
import useTimeout from './useTimeout';

const MyPage = () => {
  const [count, setCount] = useState(0);

  useTimeout(() => {
    setCount(count => count + 1);
  }, 3000);

  return (
    <div >
      <button type="button">
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

6、实现自定义的 useInterval

useInterval 封装 setInterval 功能,其原因和用法跟 useTimeout 一样,这里不再赘述。

import { useEffect } from 'react';

function useInterval (fn: () => void, delay: number) {
  useEffect(() => {
    const timer = setInterval(() => {
      fn();
    }, delay);
    return () => {
      clearInterval(timer); // 移除定时器
    };
  }, [delay]);
}

export default useInterval;

7、实现自定义的 useDebounce

防抖在我们日常开发中是非常常见的,比如:按钮点击、文本编辑保存等,为防止用户过于频繁操作,需要进行防抖处理。**防抖的定义:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时间,才执行代码一次。**类比于生活中的场景就例如坐公交,在一定时间内,如果有乘客陆续刷卡上车,司机就不会开车,当乘客没有刷卡了,司机才开车。
防抖功能的基本实现和相关注释如下所示

function debounce(fn,wait){
    let timeout1;
    return function(){
        clearTimeout(timeout1);  // 重新清零
        let context = this;  // 保存上下文
        let args = arguments; // 获取传入的参数
        timeout1 = setTimeout(()=> {
            fn.apply(context, args);
        },wait)
    }
}

我们将以上的实现用 hooks 自定义的方式来写,useDebounce hook 相关代码如下,其中传入的两个参数为:fn(要执行的回调方法)和 delay(防抖时间),然后该 hook 返回一个执行方法

import { useCallback, useRef } from 'react';

const useDebounce = (fn: Function, delay = 100) => {
  const time1 = useRef<any>();

  return useCallback((...args) => {
    if (time1.current) {
      clearTimeout(time1.current);
    }
    time1.current = setTimeout(() => {
      fn(...args);
    }, delay);
  }, [delay]);
};

export default useDebounce;

现在我们就可以在相关业务场景中使用这个 useDebounce hook 了,如下所示,我们不断点击 button,count 也不会增加,只有点击间隔超过 3000ms,count 数才会增加。

import React, { useCallback, useState } from 'react';
import useDebounce from './useDebounce';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    setCount(count => count + 1);
  }, []);

  const run = useDebounce(fun, 3000);

  return (
    <div >
      <button type="button" onClick={() => { run(); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

8、实现自定义的 useThrottle

节流在我们日常开发中是非常常见的,比如:滚动条监听、图片放大镜效果功能等,我们不必每次鼠标滚动都触发,这样可以降低计算的频率,而不必去浪费资源。节流的定义:函数节流是指一定时间内 js 方法只跑一次。类比于生活中的场景就例如人眨眼睛,就是一定时间内眨一次。
节流功能的基本实现和相关注释如下所示,跟防抖很类似

function throttle(fn, wait){
  let timeout;
  return function(){
      if(timeout) return; // 如果已经触发,则不再触发
      let args = arguments;
      let context = this;
      timeout = setTimeout(()=>{
        fn.apply(context,args); // 执行
        timeout = null; // 执行后,将标志设置为未触发
      },wait)
  }
}

我们将以上的实现用 hooks 自定义的方式来写,useThrottle hook 相关代码如下,其中传入的两个参数为:fn(要执行的回调方法)和 delay(节流时间),然后该 hook 返回一个执行方法

import { useCallback, useRef } from 'react';

const useThrottle = (fn: Function, delay = 100) => {
  const time1 = useRef<any>();

  return useCallback((...args) => {
    if (time1.current) {
      return;
    }
    time1.current = setTimeout(() => {
      fn(...args);
      time1.current = null;
    }, delay);
  }, [delay]);
};

export default useThrottle;

现在我们就可以在相关业务场景中使用这个 useThrottle hook 了,如下所示,我们不断点击 button,count 只会在连续间隔 3000ms 增加一次,不会每次点击都会增加一次。

import React, { useCallback, useState } from 'react';
import useThrottle from './useThrottle';

const MyPage = () => {
  const [count, setCount] = useState(0);
  const fun = useCallback(() => {
    setCount(count => count + 1);
  }, []);

  const run = useThrottle(fun, 3000);

  return (
    <div >
      <button type="button" onClick={() => { run(); }}>
        增加 {count}
      </button>
    </div>
  );
};

export default MyPage;

总结

本文是 react hooks 三部曲中的第二篇,按照预期,后续我们会写 react hooks 三部曲中的第三篇,敬请期待。

辛苦整理良久,还望手动点赞鼓励~
博客 github地址为:github.com/fengshi123/… ,汇总了作者的所有博客,欢迎关注及 star ~

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

作者写错了~

useUnmount 处的代码为啥不是这样写呢

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return fn();
  }, []);
};

这样写也戳啦,应该是酱紫啦

const useUnmount = (fn: () => void) => {
  useEffect(() => {
    // 👇
    return ()=>fn(); // 或者 return fn
  }, []);
};