DEV Community

Lydia_Yuan
Lydia_Yuan

Posted on

concurrent mode 对 useRef 使用可能造成的影响

需要的前置知识:

  1. Concurrent mode 在 concurrent mode,最后一次 render 不代表「最新的 committed 状态」 有最新 state / props 的优先级较低的 render 可能会重写当前 event handler 使用的 reference
  2. useRef
  3. React strict mode
  4. React render phase & commit phase
  5. useEffect : timing of effect

我在这个 issue 的讨论里面看到了关于未来 concurrent mode 对 useRef 使用可能造成的影响

Q: What issues has "mutation of ref during rendering"? Can you explain me in brief?
A: In concurrent mode (not yet released), it would "remember" the last rendered version, which isn't great if we render different work-in-progress priorities. So it's not "async safe".

这个回答还是有点简略,不太好懂,我再扩展一下

useRef 的功能相当于是一个全局变量,所以改变它其实是会造成副作用的

比如这个 demo:https://codesandbox.io/s/useref-strict-mode-3xpf3?file=/src/App.tsx

strict mode 下之前 OK 的 useRef 用法因为重渲染引发了不必要的副作用(可以看到左边的 bad counter 的 ref 值被多加了一次)

可以对比一下例子里面的两个组件

BadCounter.tsx

const BadCounter = () => {
  const count = useRef(0);
  count.current += 1;
  return (
    <div className="counter">
      BAD COUNTER <br /> count:{count.current}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

显然这在现在非 concurrent mode 的情景下可以正常工作,因为现在 render 阶段(计算组件哪里需要更新,即调用 render 函数 比较本次和之前的计算结果)和 commit 阶段(把计算出来的变化CRUD到 DOM 里)是一比一的
但是我通过把它包在 <StrictMode/> 里面利用 strict mode 触发了两次 Function component bodies 中的内容
进行了两次 render 但是只 commit 了一次,所以点按钮重渲染之后会增加 2

GoodCounter.tsx

import React, { useEffect, useRef } from "react";
const GoodCounter = () => {
  const count = useRef(0);
  let currentCount = count.current;
  useEffect(() => {
    count.current = currentCount;
  });
  currentCount += 1;
  return (
    <div className="counter">
      GOOD COUNTER <br /> count:{currentCount}
    </div>
  );
};
export default GoodCounter;
Enter fullscreen mode Exit fullscreen mode

改进后的组件把对 ref 的修改放进了 useEffect
useEffect 里面的函数只会在 commit phase 去调用
所以就算 render phase : commit phase 不再是 1:1 也可以「展示想要的状态」

PS 这里官方对于 useCallback 里面如何读取总是改变的值的 workaround 的例子就是放在 useEffect 里面处理的:https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback

别人写的使用 concurrent mode 的相同 demo : https://codesandbox.io/s/x2p46v02z4
也是一样的道理

所以未来如果要升级到 concurrent mode 的版本
对于 useRef 的使用需要非常小心

我能想到的对于升级到 concurrent mode 的 best practice

  • 可以通过 strict mode 先对会产生副作用的函数(包括但不仅限于改变 ref.current )进行一波检查
  • 因为考虑到会出现的危险,尽量少用 ~~useRef~~ 考虑能否用其他 async safe 的 hook 替代(比如上面的例子就可以用 useState 或者 useCallback 替代)

参考资料

Discussion (0)