首页 文章详情

三种自定义 hook 的事件封装方式,你会选择哪种?

全栈前端精选 | 120 2024-05-27 11:16 0 0 0
UniSMS (合一短信)

我们经常通过自定义 hook 的方式抽离组件的逻辑,而这种自定义 hook 里很多都是给元素绑定事件的。

绑定事件的写法一共有三种,我们一起来过一遍。

首先是 useHover 的 hook:

      
      import useHover from './useHover';

const App = () => {
  const element = (hovered: boolean) =>
    <div>
      Hover me! {hovered && 'Thanks'}
    </div>
;

  const [hoverable, hovered] = useHover(element);

  return (
    <div>
      {hoverable}
      <div>{hovered ? 'HOVERED' : ''}</div>
    </div>

  );
};

export default App;

浏览器只有 mouseover、mouseleave 事件,封装成 hover 的 hook 还是挺有意义的。

a768c2144dd52b4df6d6cf65e5ca3c5f.webp

这个 hook 接收 React Element 作为参数,绑定事件后返回。

这样实现的:

      
      import { cloneElement, useState } from "react";

export type Element = ((state: boolean) => React.ReactElement) | React.ReactElement;

const useHover = (element: Element): [React.ReactElement, boolean] => {
  const [state, setState] = useState(false);

  const onMouseEnter = (originalOnMouseEnter?: any) => (event: any) => {
    originalOnMouseEnter?.(event);
    setState(true);
  };
  const onMouseLeave = (originalOnMouseLeave?: any) => (event: any) => {
    originalOnMouseLeave?.(event);
    setState(false);
  };

  if (typeof element === 'function') {
    element = element(state);
  }

  const el = cloneElement(element, {
    onMouseEnter: onMouseEnter(element.props.onMouseEnter),
    onMouseLeave: onMouseLeave(element.props.onMouseLeave),
  });

  return [el, state];
};

export default useHover;

传入的可以是 ReactElement 也可以是返回 ReactElement 的函数,内部对函数做下处理:

677431b084a37c4ee9e5e4c8cd93d1da.webp

用 cloneElement 复制 ReactElement,给它添加 onMouseEnter、onMouseLeave 事件。

并用 useState 保存 hover 状态:

dba233cef612a5028d074db73430c8fb.webp

这里注意如果传入的 React Element 本身有 onMouseEnter、onMouseLeave 的事件处理函数,要先调用下:

76e3b967a1c5c4dcfbad30c9aa1a3598.webp

然后来封装 useScrolling 的 hook,它可以拿到元素是否在滚动的状态:

      
      import { useRef } from "react";
import useScrolling from "./useScrolling";

const App = () => {
  const scrollRef = useRef(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;
9b940ec55c466aaa9212583d00f58c7e.webp

和刚才的 useHover 差不多,但是传入的是 ref。

我们实现下:

      
      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:

dcd922a6c240352b2e253705c2809546.webp

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

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

这里用的 ref 的方式绑定事件,是第二种方式。

还有,写 message 组件的时候,item 是 2s 后自动删除,但是如果 hover 上去就不会,等鼠标离开才会重新定时:

47407b8b01f3e2dd1f729774920057c0.webp

所以我写了这个 hook:

      
      import { useEffect, useRef } from 'react';

export interface UseTimerProps {
    id: number;
    duration?: number;
    remove: (id: number) => void;
}

export function useTimer(props: UseTimerProps{
  const { remove, id, duration = 2000 } = props;

  const timer = useRef<number | null>(null);

  const startTimer = () => {
    timer.current = window.setTimeout(() => {
        remove(id);
        removeTimer();
    }, duration);
  };

  const removeTimer = () => {
    if (timer.current) {
        clearTimeout(timer.current);
        timer.current = null;
    }
  };

  useEffect(() => {
    startTimer();
    return () => removeTimer();
  }, []);

  const onMouseEnter = () => {
    removeTimer();
  };

  const onMouseLeave = () => {
    startTimer();
  };

  return {
    onMouseEnter,
    onMouseLeave,
  };
}

它提供了 onMouseEnter、onMouseLeave 事件处理函数,mouseEnter 的时候移除定时器,mouseLeave 的时候重新定时,然后到时间删除:

c5f76b0952bde73de71bf082f4fc27b3.webp

用的时候自己绑定到元素上:

9a7bffebf2190a111a2e9327c4c64ee7.webp3c410217ca6df63f0835bef18f46e53d.webp

这就是封装事件类自定义 hook 的第三种方式。

其实这三种方式用的都很多。

第一种传入 React Element 通过 cloneElement 给它添加事件处理函数。

这个是 react-use 的 hook:

8d8ce67d8f846491a711555e81481574.webp

react-use 是非常流行的通用 hook 库,下载量是 ahooks 的十倍:

ahooks:

5d47baec6b90791c3add26515db86a0a.webp

react-use:df8551f57465bda1f95b3c3220513a1e.webp

第二种传入 ref 然后 addEventListener 绑定事件。

这个也是 react-use 的 hook:

d70cbaadb135d3ada06228f1bb3d331b.webp

第三种方式返回事件处理函数,让调用者自己绑定。

比如 @floating-ui/react 包的 useInteractions,就是返回 props 对象,类似 {onClick: xxx} 这种,让调用者自己绑定:

410261a538f5c67c933428a10eff8e28.webp

这三种自定义 hook 的绑定事件写法,你会选择哪一种?

good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter