Skip to content

实战中的几个自定义 Hook

Published:

在讲这几个 自定义 Hook 之前,我们可以先来熟悉一下 React 自带的一个 Hook API —— useRef

useRef

const myRef = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

一个作用是用来访问 DOM ,与 React.createRef() 相似。你可以将 ref 对象,这样使用 <div ref={myRef} />,然后 ref.current就会一直指向这个 DOM div节点了。

另一个作用其实在 Hook 中应该是用得更加频繁的。useRef() 返回的 ref 对象 实际上可以很方便保存任何值。useRef() 会在每次渲染时返回同一个 ref 对象。

这就类似于一个 class 的实例变量,看一个例子。

function Timer() {
  const intervalRef = useRef();
  // mount的时候启动一个定时器
  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  }, []);
}

因为setInterval 返回的值放在了 intervalRef,所以无论 Timer 组件重新渲染多少次,interval.current 都不会改变,所以我们可以清除这个循环定时器。

function handleCancelClick() {
  clearInterval(intervalRef.current);
}

所以从概念上讲,可以认为 ref 就像是一个 class 的实例变量。

useRefCallback

useRefCallback 通过使用 useRef 的特性,可以返回一个引用地址永远不变的函数。

这里就会有人问了,这不是和 useCallback 一样么?

实际上还是不一样的,useCallback 的第二个参数可以填写依赖值,如果回调所依赖的值变化了,回调就会更新。在实际开发过程中,其实内部的 state 是经常会发生变化的,这就会导致 useCallback 的效果不是很好,对于比较复杂的子组件,重新渲染会对性能造成一定影响。

使用useRefCallback 可以避免这种情况,来看看以下例子🌰(codesandbox在线例子

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

  // 输入框onChange时,count发生变化  showCount1更新
  const showCount1 = useCallback(() => {
    alert(count);
  }, [count]);

  // 引用不会发生变化
  const showCount2 = useRefCallback(() => {
    alert(count);
  });

  return (
    <>
      <button onClick={() => setCount((c) => c + 1)}>Add Count</button>
      <p>点击👆按钮可以看到子组件的渲染次数</p>
      <div style={{ marginTop: 32 }}>
        <h4>使用useRefCallback</h4>
        <ExpensiveTree showCount={showCount2} />
      </div>
      <div style={{ marginTop: 32 }}>
        <h4>使用useCallback</h4>
        <ExpensiveTree showCount={showCount1} />
      </div>
    </>
  );
};
// 这里使用React.memo
const ExpensiveTree = React.memo<{ [key: string]: any }>(({ showCount }) => {
  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  return (
    <div>
      <p>渲染次数: {renderCountRef.current}</p>
      <button onClick={showCount}>showParentCount</button>
    </div>
  );
});

可以看到,当点击按钮时,使用 useCallback 记忆回调的子组件,会经常发生渲染。

SjcBBT

所以,使用 useRefCallback 来记忆回调函数,可以达到一定的性能优化效果,useRefCallback 的实现也比较简单,源码如下:

function useRefCallback<T extends (...args: any[]) => any>(fn: T) {
  // 这个ref.current的引用用于更新fn
  const fnRef = useRef<T>(fn);
  fnRef.current = fn;

  const persistFn = useRef<T>();
  if (!persistFn.current) {
    persistFn.current = function (...args) {
      return fnRef.current(args);
    } as T;
  }

  return persistFn.current;
}

useExecuteFn

在中后台场景中,一个按钮的点击,许多时候都需要发起一个请求,虽然是一个简单的场景,但确实一个体现细节的地方,来看看“各大工程师”的实现。

偷懒工程师:我直接就一个 onClick 完事,then 完就是 setState

结果就是过几天后台过来开喷了:你 ** 一个同样的请求没事发这么多次干嘛??

这里大家都应该清楚,在碰到网络请求慢的时候,大多数用户都会选择再点击一次,接着就会再次发起请求了。

锁工程师:onClick 的时候,我先给它 this.locked = true 一波。

这种处理方式就还好一点,但是依旧会出现用户多次点击的问题,用户体验不是很好。

Loading工程师:onClick 的时候,显示 loading,有一个用户反馈,可以让用户感知到是在处理某些事情。

这是一种比较不错的处理方式了,loading 的时候也是禁用点击。但是也有许多人偷懒不做这样的处理,因为要显示 loading 态,就意味要多维护一个 state,接着你还要去在相应的位置做 状态更改,显得比较麻烦。

所以我就在思考,有没有一种简易的方法来实现这种方式。

loading 态无需手动维护,请求开始前开始loading,请求结束后停止 loading。

于是就有 useExecuteFn 这个自定义 Hook,看看它是怎么使用的。

const [handleSubmit, submitExecuting] = useExecuteFn(async () => {
  await submitTask();
});

// antd Button
// handleSubmit 执行时 submitExecuting 为true 结束后为 false
return <Button onClick={handleSubmit} loading={submitExecuting}>

这样就达到了自动化 loading 的目的了。

其源码也很简单:

export function useExecuteFn<P extends any[] = any[], V extends any = any>(
  fn: (...args: P) => Promise<V>
) {
  // 维护一个 executing State
  const [executing, setExecuting] = useState(false);

  const executeFn = useRefCallback(async (...args: P) => {
    try {
      setExecuting(true);
      const ret = await fn(...args);
      setExecuting(false);
      return ret;
    } catch (e) {
      setExecuting(false);
      throw e;
    }
  });
  return [executeFn, executing] as const;
}

类似的,如果不需要 loading 这种,只需要一个 lock 的话,也可以 利用 useRef 实现一个 useLockFn 这样的 hook,大概实现方式也与 useExecuteFn 差不多。

usePrevious

这个 Hook 主要用于获取上一轮的 props 或 state。

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  return <h1>Now: {count}, before: {prevCount}</h1>;
}

主要是通过 useRef 来实现:

function usePrevious<T>(value: T) {
  const ref = useRef<T>();
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
}

useToggle

这一个 Hook 主要是做布尔值切换的,比较常用的场景是 弹窗的打开关闭,不需要显示设置 true 或 false。

function Index() {
  const [visible, toggleVisible] = useToggle(); // 这里也可以传默认值
  // 也可手动设置 toggleVisible(false);
  return <Modal visible={visible} onOk={toggleVisible} onCancel={toggleVisible}>xxx</Modal>;
}

源码如下:

export function useToggle(initialState = false) {
  const [state, setState] = useState(initialState);

  /**
   * 可直接传入一个state 否则置反
   */
  const toggle = useCallback((s?: any) => {
    if (typeof s === "boolean") {
      setState(s);
    } else {
      setState(prevState => !prevState);
    }
  }, []);

  return [state, toggle] as const;
}

这里其实也可以支持 toggle 两个值,不过 TS 的类型重载搞起来略蛋疼,有空再研究研究。

大概就这些了…

一些参考

Umi Hooks

react-use