react学习13:几个简单的自定义hooks

组件里有很多逻辑是可以复用的。

对于常规的 JS 逻辑,我们会封装成函数,也会用一些通用函数的库,比如 lodash。

对于用到 hook 的逻辑,我们会封装成自定义 hook,当然,也会有通用 hook 库,比如 react-useahooks

看周下载量,react-use 是 ahooks 的十倍。

自定义 hook 就是函数封装,和普通函数的区别只是在于名字规范是用 use 开头,并且要用到 react 的内置 hook。

useMountedState

useMountedState 可以用来获取组件是否 mount 到 dom:

js 复制代码
import { useEffect, useState } from 'react';
import {useMountedState} from 'react-use';

const App = () => {
    const isMounted = useMountedState();
    const [,setNum ] = useState(0);

    useEffect(() => {
        setTimeout(() => {
            setNum(1);
        }, 1000);
    }, []);

    return <div>{ isMounted() ? 'mounted' : 'pending' }</div>
};

export default App;

第一次组件渲染的时候,组件还没 mount 到 dom,isMounted()为false,此时显示pending。

1 秒后在useEffect中通过 setState 触发再次渲染的时候, 我们知道 useEffect 是在首次渲染结束后执行的,所以isMounted()为 true, 这时候组件已经 mount 到 dom 了,显示 mounted。

这个 hook 的实现也比较简单:

js 复制代码
import { useCallback, useEffect, useRef } from 'react';

export default function useMountedState(): () => boolean {
  const mountedRef = useRef<boolean>(false);
  const get = useCallback(() => mountedRef.current, []);

  useEffect(() => {
    mountedRef.current = true;

    return () => {
      mountedRef.current = false;
    };
  }, []);

  return get;
}

我们知道, useRef 创建一个可变的 ref 对象,其 .current 属性可以存储任意值,且该值在组件的所有渲染周期中保持不变 (不会因组件重新渲染而重置)。本质是「持久化存储容器」,用于保存跨渲染周期需要保留的数据。

当首次渲染完成后,执行 mountedRef.current = true;setNum(1);再次触发渲染时,mountedRef.current 已经为true了。

使用 useRef 而不是 useState 保存 mount 的值是因为修改 ref.current 并不会引起组件重新渲染。

并且返回的 get 函数要用 useCallback 包裹,这样用它作为其它 memo 组件参数的时候,就不会导致额外的渲染。

useLifeCycles

js 复制代码
import {useLifecycles} from 'react-use';

const App = () => {
  useLifecycles(() => console.log('MOUNTED'), () => console.log('UNMOUNTED'));

  return null;
};

export default App;

这个也是用 useEffect 的特性实现的:

js 复制代码
import { useEffect } from 'react';

const useLifecycles = (mount: Function, unmount?: Function) => {
  useEffect(() => {
    if (mount) {
      mount();
    }
    return () => {
      if (unmount) {
        unmount();
      }
    };
  }, []);
};

export default useLifecycles;

在 useEffect 里调用 mount,这时候 dom 操作完了,组件已经 mount。

然后返回的清理函数里调用 unmount,在组件从 dom 卸载时调用。

这两个 hook 都是依赖 useEffect 的特性来实现的。

useCookie

useCookie 可以方便的增删改 cookie:

js 复制代码
import { useEffect } from "react";
import { useCookie } from "react-use";

const App = () => {
  const [value, updateCookie, deleteCookie] = useCookie("peng");

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

  const updateCookieHandler = () => {
    updateCookie("666");
  };

  return (
    <div>
      <p>cookie 值: {value}</p>
      <button onClick={updateCookieHandler}>更新 Cookie</button>
      <br />
      <button onClick={deleteCookie}>删除 Cookie</button>
    </div>
  );
};
export default App;

它是对 js-cookie 这个包的封装, 安装下:

js 复制代码
npm i --save js-cookie

然后实现 useCookie:

js 复制代码
import { useCallback, useState } from 'react';
import Cookies from 'js-cookie';

const useCookie = (
  cookieName: string
): [string | null, (newValue: string, options?: Cookies.CookieAttributes) => void, () => void] => {
  const [value, setValue] = useState<string | null>(() => Cookies.get(cookieName) || null);

  const updateCookie = useCallback(
    (newValue: string, options?: Cookies.CookieAttributes) => {
      Cookies.set(cookieName, newValue, options);
      setValue(newValue);
    },
    [cookieName]
  );

  const deleteCookie = useCallback(() => {
    Cookies.remove(cookieName);
    setValue(null);
  }, [cookieName]);

  return [value, updateCookie, deleteCookie];
};

export default useCookie;

就是基于 js-cookie 来 get、set、remove cookie。

一般自定义 hook 里返回的函数都要用 useCallback 包裹下,这样调用者就不用自己处理了。

useScrolling

useScrolling 封装了滚动的状态:

js 复制代码
import { useRef } from "react";
import { useScrolling } from "react-use";

const App = () => {
  const scrollRef = useRef<HTMLDivElement>(null);
  const scrolling = useScrolling(scrollRef);

  return (
    <>
    {<div>{scrolling ? "滚动中.." : "没有滚动"}</div>}

    <div ref={scrollRef} style={{height: '200px', overflow: 'auto'}}>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
      <div>guang</div>
    </div>
    </>
  );
};

export default App;

实现下:

js 复制代码
import { RefObject, useEffect, useState } from 'react';

const useScrolling = (ref: RefObject<HTMLElement>): boolean => {
  const [scrolling, setScrolling] = useState<boolean>(false);

  useEffect(() => {
    if (ref.current) {
      let scollingTimer: number;

      const handleScrollEnd = () => {
        setScrolling(false);
      };

      const handleScroll = () => {
        setScrolling(true);
        clearTimeout(scollingTimer);
        scollingTimer = setTimeout(() => handleScrollEnd(), 150);
      };

      ref.current?.addEventListener('scroll', handleScroll);

      return () => {
        if (ref.current) {
          ref.current?.removeEventListener('scroll', handleScroll);
        }
      };
    }
    return () => {};
  }, [ref]);

  return scrolling;
};

export default useScrolling;

用 useState 创建个状态,给 ref 绑定 scroll 事件,scroll 的时候设置 scrolling 为 true。

并且定时器 150ms 以后修改为 false。

这样只要不断滚动,就会一直重置定时器,结束滚动后才会设置为 false。

useSize

useSize 是用来获取 dom 尺寸的,并且在窗口大小改变的时候会实时返回新的尺寸。

js 复制代码
import React, { useRef } from 'react';
import { useSize } from 'ahooks';

export default () => {
  const ref = useRef<HTMLDivElement>(null);
  const size = useSize(ref);
  return (
    <div ref={ref}>
      <p>改变窗口大小试试</p>
      <p>
        width: {size?.width}px, height: {size?.height}px
      </p>
    </div>
  );
};

实现下:

js 复制代码
import ResizeObserver from 'resize-observer-polyfill';
import { RefObject, useEffect, useState } from 'react';

type Size = { width: number; height: number };

function useSize(targetRef: RefObject<HTMLElement>): Size | undefined {

    const [state, setState] = useState<Size | undefined>(
        () => {
            const el = targetRef.current;
            return el ? { width: el.clientWidth, height: el.clientHeight } : undefined
        },
    );

    useEffect(() => {
        const el = targetRef.current;

        if (!el) {
            return;
        }

        const resizeObserver = new ResizeObserver((entries) => {
            entries.forEach((entry) => {
                const { clientWidth, clientHeight } = entry.target;
                setState({ width: clientWidth, height: clientHeight });
            });
        });
        resizeObserver.observe(el);

        return () => {
            resizeObserver.disconnect();
        };
    }, []);

    return state;
}

export default useSize;

用 useState 创建 state,初始值是传入的 ref 元素的宽高。

这里取 clientHeight,也就是不包含边框的高度。

然后用 ResizeObserver 监听元素尺寸的变化,改变的时候 setState 触发重新渲染。

这里为了兼容,用了 resize-observer-polyfill。

js 复制代码
npm i --save resize-observer-polyfill

useHover

下面是ahooks的使用方式:

js 复制代码
import React, { useRef } from 'react';
import { useHover } from 'ahooks';

export default () => {
  const ref = useRef<HTMLDivElement>(null);
  const isHovering = useHover(ref);
  return <div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>;
};

实现下:

js 复制代码
import { RefObject, useEffect, useState } from 'react';

export interface Options {
  onEnter?: () => void;
  onLeave?: () => void;
  onChange?: (isHovering: boolean) => void;
}

export default (ref: RefObject<HTMLElement>, options?: Options): boolean => {
    const { onEnter, onLeave, onChange } = options || {};

    const [isEnter, setIsEnter] = useState<boolean>(false);

    useEffect(() => {
        ref.current?.addEventListener('mouseenter', () => {
            onEnter?.();
            setIsEnter(true);
            onChange?.(true);
        });
    
        ref.current?.addEventListener('mouseleave', () => {
            onLeave?.();
            setIsEnter(false);
            onChange?.(false);
        });
    }, [ref]);

    return isEnter;
};

useTimeout

js 复制代码
import React, { useState } from 'react';
import { useTimeout } from 'ahooks';

export default () => {
  const [state, setState] = useState(1);
  useTimeout(() => {
    setState(state + 1);
  }, 3000);

  return <div>{state}</div>;
};

实现下:

js 复制代码
import { useCallback, useEffect, useRef } from 'react';

const useTimeout = (fn: () => void, delay?: number) => {

  const fnRef = useRef<Function>(fn);
  
  fnRef.current = fn;

  const timerRef = useRef<number>();

  const clear = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  }, []);

  useEffect(() => {
    timerRef.current = setTimeout(fnRef.current, delay);

    return clear;
  }, [delay]);

  return clear;
};

export default useTimeout;

首次渲染,useRef<Function>(fn) 会初始化 fnRef.current 为当前的 fn

当组件重新渲染,组件函数会重新执行,此时会创建一个新的 fn 函数 ,新的 fn 引用的是最新的 state

useRef 的特性是:初始值仅在首次渲染生效,后续渲染不会更新 current ,所以 fnRef.current 仍然指向上一次渲染时的旧 fn (引用的是旧的 state)。

因此,需要手动赋值 fnRef.current = fn

useWhyDidYouUpdate

props 变了会导致组件重新渲染,而 useWhyDidYouUpdate 就是用来打印是哪些 props 改变导致的重新渲染:

js 复制代码
import { useWhyDidYouUpdate } from 'ahooks';
import React, { useState } from 'react';

const Demo: React.FC<{ count: number }> = (props) => {
  const [randomNum, setRandomNum] = useState(Math.random());

  useWhyDidYouUpdate('Demo', { ...props, randomNum });

  return (
    <div>
      <div>
        <span>number: {props.count}</span>
      </div>
      <div>
        randomNum: {randomNum}
        <button onClick={() => setRandomNum(Math.random)}>
          设置随机 state
        </button>
      </div>
    </div>
  );
};

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

  return (
    <div>
      <Demo count={count} />
      <div>
        <button onClick={() => setCount((prevCount) => prevCount - 1)}>减一</button>
        <button onClick={() => setCount((prevCount) => prevCount + 1)}>加一</button>
      </div>
    </div>
  );
};

Demo 组件有 count 的 props,有 randomNum 的 state。

当状态变化导致组件重新渲染时, 都能打印出是那个状态导致的,并且打印出从 from 改变到 to 导致的。

它的实现其实很简单,我们来写一下:

js 复制代码
import { useEffect, useRef } from 'react';

export type IProps = Record<string, any>;

export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  const prevProps = useRef<IProps>({});

  useEffect(() => {
    if (prevProps.current) {
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      const changedProps: IProps = {};

      allKeys.forEach((key) => {
        if (!Object.is(prevProps.current[key], props[key])) {
          changedProps[key] = {
            from: prevProps.current[key],
            to: props[key],
          };
        }
      });

      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', componentName, changedProps);
      }
    }

    prevProps.current = props;
  });
}

Record<string, any> 是任意的对象的 ts 类型。

核心就是 useRef 保存 props 或者其他值,当下次渲染的时候,拿到新的值和上次的对比下,打印值的变化。

Object.is 是 "更严格的 ===",修复了 ===NaN 和正负 0 上的判断缺陷,其他场景行为完全一致。日常开发中,若无需处理这些特殊值,=== 已足够;若需精准判断所有值的相等性,优先使用 Object.is

相关推荐
cos5 小时前
React RCE 漏洞影响自建 Umami 服务 —— 记 CVE-2025-55182
前端·安全·react.js
奋斗猿6 小时前
前端实测:RSC不是银弹,但它真的重构了我的技术栈
前端·react.js
HexCIer6 小时前
CVE-2025-55182 React Server Components "React2Shell" 深度调查与全链路响应报告
react.js·next.js
爱看书的小沐7 小时前
【小沐学WebGIS】基于Cesium.JS绘制雷达波束/几何体/传感器Sensor(Cesium / vue / react )
javascript·vue.js·react.js·雷达·cesium·传感器·波束
用户8168694747257 小时前
React 如何用 MessageChannel 模拟 requestIdleCallback
前端·react.js
ZZJsky1238 小时前
关于 React 进化历史 (上)
react.js
随风一样自由8 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案详解
前端·react.js·前端框架·跨域
一个处女座的程序猿O(∩_∩)O8 小时前
React Native vs React Web:深度对比与架构解析
前端·react native·react.js
@大迁世界8 小时前
紧急:React 19 和 Next.js 的 React 服务器组件存在关键漏洞
服务器·前端·javascript·react.js·前端框架