concurrent mode 对 useRef 使用可能造成的影响
Published: 2021-11-25 06:41:58


需要的前置知识：

1. [Concurrent mode](https://reactjs.org/docs/concurrent-mode-intro.html)
    在 concurrent mode，最后一次 render 不代表「最新的 committed 状态」
    有最新 state / props 的优先级较低的 render 可能会重写当前 event handler 使用的 reference
2. [useRef](https://github.com/facebook/react/blob/cae635054e17a6f107a39d328649137b83f25972/packages/react-dom/src/server/ReactPartialRendererHooks.js#L372)
3. [React strict mode](https://reactjs.org/docs/strict-mode.html)
4. [React render phase & commit phase](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits) 
5. [useEffect : timing of effect](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits)

我在这个 [issue](https://github.com/facebook/react/issues/14099#issuecomment-436647120) 的讨论里面看到了关于未来 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](https://codesandbox.io/s/useref-strict-mode-3xpf3?file=/src/App.tsx)


![strict mode 下之前 OK 的 useRef 用法因为重渲染引发了不必要的副作用（可以看到左边的 bad counter 的 ref 值被多加了一次）](https://paper-attachments.dropbox.com/s_1016595EFD966570AF518621E802F0C51F71D38AA80638CEF86FE4ABF14B01DC_1626515390876_Screen+Shot+2021-07-17+at+17.49.08.png)


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

BadCounter.tsx

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

显然这在现在非 concurrent mode 的情景下可以正常工作，因为现在 [render 阶段（计算组件哪里需要更新，即调用 render 函数 比较本次和之前的计算结果）和 commit 阶段（把计算出来的变化CRUD到 DOM 里）](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits)是一比一的
但是我通过把它包在 `<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;

改进后的组件把对 ref 的修改放进了 `useEffect` 中
[useEffect](https://reactjs.org/docs/hooks-reference.html#timing-of-effects) [里面的函数只会在 commit phase 去调用](https://reactjs.org/docs/hooks-reference.html#timing-of-effects)
所以就算 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` 替代）

参考资料

- [How To Properly Use the React useRef Hook in Concurrent Mode](https://betterprogramming.pub/how-to-properly-use-the-react-useref-hook-in-concurrent-mode-38c54543857b)
- [React lifecycle methods diagram](https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)
- [[Hooks] Alternative useCallback implementation](https://github.com/reactjs/rfcs/issues/83)