ahooks源码分析之usePrevious

2,820 阅读2分钟

usePrevious用于保存上一次渲染时的状态。

React官方文档提供了一个实现:

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

usePrevious记录的值初始为空,每轮渲染后记录状态值,这样每次渲染返回的便是上一轮渲染时的值。

react-use同样使用了此实现

ahooks则为用户提供了compare,可以让用户决定是否更新usePrevious记录的值。

import { useRef } from 'react';

export type compareFunction<T> = (prev: T | undefined, next: T) => boolean;

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

  const needUpdate = typeof compare === 'function' ? compare(curRef.current, state) : true;
  if (needUpdate) {
    prevRef.current = curRef.current;
    curRef.current = state;
  }

  return prevRef.current;
}

export default usePrevious;

ahooks 使用了两个ref,一个记录当前值,一个记录之前的值。不过为什么要这样实现呢?这样实现与react-use的实现方式有什么不同?🤔

在一番试验无果后,随意搜索了下却找到了这个issue

什么?ahooks的实现不符合使用规范😯

issue作者给出的链接中说明了在render时读取或修改ref的值时会进行警告,并且Dan在回复中说明了这样做的原因。

image.png

大意是在render时读取ref的值和读取一个随机的全局变量一样。读取的值是什么取决于何时调用render。如果React调用在稍微不同的时间渲染,可能会得到不同的结果。

在未来React默认开启Concurrent模式后,ahooks的实现便会出现问题。

issue作者除了给出解释之外,还提供了一个demo。demo中使用的usePreviousStrictMode下有了不同的行为。

不过demo中渲染用的也是legacy模式,那为什么在StrictMode下行为会不同?🤔

打断点调试了一番,发现进入页面时usePrevious居然被调用了两次,导致curRefpreRef记录的状态出现了问题。

在React issue中搜索StrictModetwice等关键字找到了原因,还是我们的Dan神回复的:

image.png

issue地址

使用了StrictMode且用了Hooks的组件会在开发模式时渲染两次。StrictMode的一个主要目的是方便将现有项目迁移到未来使用concurrent模式的React版本中,会这么设计不奇怪。

至此,作战告捷😊

简单改了下ahooksusePrevious实现。

function usePrevious<T>(state: T, compare?: (prev: T | undefined, next: T) => boolean): T | undefined {
  const ref = useRef<T>();

  useEffect(() => {
    const needUpdate = typeof compare === 'function' ? compare(ref.current, state) : true;
    if (needUpdate) {
      ref.current = state;
    }
  });

  return ref.current;
}