react 的严格模式下强制重新渲染引发的问题及解决方案及讨论

360 天前
 rizon

前提场景:代码中 buildProcessor 方法不能重复运行 ,useExecuter 方法只会用第一次结果

"use client";

import { useEffect, useMemo, useState } from "react";

export default function RenderTest() {

  const [processor, setProcessor] = useState<number | undefined>();

  // build processor
  useEffect(() => {
    const _processor = buildProcessor();
    console.log("build processor", _processor.processor);
    setProcessor(_processor.processor);
    return () => {
      console.log("cleanup processor", _processor.processor);
      _processor.destroy();
    };
  }, []);

  console.log("renderRenderTest, processor", processor);
  //虽然该组件会被 react 强制执行两次,但是 ExecurerWrap 只会被渲染最后一次
  return processor ? (
    <ExecurerWrap processor={processor} />
  ) : (
    <div>Loading...</div>
  );
}

function ExecurerWrap({ processor }: { processor: number }) {
  const executer = useExecuter(processor);

  console.log("render ExecurerWrap, executer, processor", executer, processor);
  return (
    <div>
      <div>渲染 executer:{executer}</div>
    </div>
  );
}

function buildProcessor() {
  // 该函数构建了网络访问,因此不允许未销毁而重复创建
  const seed = Date.now();
  return {
    processor: seed,
    destroy: () => console.log("destory processor", seed),
  };
}

/**
 * 只会初始化一次,不会使用新的值
 */
function useExecuter(processor?: number) {
  return useMemo(() => {
    console.log("call processor", processor);
    return processor;
  }, []);
}

代码执行结果

page.tsx:33 renderRenderTest, processor undefined
page.tsx:8 mounted
page.tsx:25 build processor 1701061145592
page.tsx:15 unmounted
page.tsx:28 cleanup processor 1701061145592
page.tsx:58 destory processor 1701061145592
page.tsx:8 mounted
page.tsx:25 build processor 1701061145593
page.tsx:33 renderRenderTest, processor 1701061145593
page.tsx:67 call processor 1701061145593
page.tsx:67 call processor 1701061145593
page.tsx:45 render ExecurerWrap, executer, processor 1701061145593 1701061145593

以上我是构建了一个场景(我真实遇到的):useExecuter 方法由于会缓存第一次结果而不再更新,导致我无法在页面上渲染最新的 executer 。因此我采用包装了 ExecurerWrap ,实现按条件触发渲染。最终效果是成功了。

顺便对于 react 的严格模式我得到这样一个结论:它是把 hook 重新执行一次,而不是按着顺序把整个组件函数跑两遍,因此 build processor 构建了两次不一样的值,但是 ExecurerWrap 只有最后一次才真的被调用了。 (这个结论对吗)

最后也是最重要的 我上面这个解决 buildProcessor 方法不能重复运行和 useExecuter 方法只用第一次结果 的方案是不是合适的,有什么其他的方案吗?

————————

我这两天刚开始接触 react ,因此虚心求教大佬们。

2195 次点击
所在节点    React
16 条回复
zed1018
360 天前
1. useEffect 在 development 和 strictmode 下执行两次是他们故意这么设定的,我没记错的话意思就是让你处理好 useEffect 里逻辑的幂等关系。

2. 如果你需要一个更贴近生命周期的 callback ,要么使用 class component ,要么使用 react-use 这个库
jsun969
360 天前
写一个 `useMount`,用 useRef 锁一下
https://gist.github.com/jsun969/102413cdf730a0942483f5fa2fe80167
sweetcola
360 天前
把 buildProcessor 移出去,找个 store 存放或者直接写到另一个文件里 export 出去,我觉得严格模式的一个意义就是提醒开发者,这些东西不应该放在这里,如果组件被卸载后再次挂载,依然会再执行一次。
bojackhorseman
360 天前
所以我用 vue ,因为学不会 react ,
rizon
360 天前
@jsun969 #2 这个方法确实可以保证只执行一次,是个不错的选择。

@zed1018 #1 react-use 这个库挺不错的,不过里面并没有像 2 楼那样的只严格执行一次的 hook ,但是有个 useFirstMountState 效果差不过,也可以。


@sweetcola #3 放全局 store 里也是一种选择,也挺好的。
rizon
360 天前
@jsun969 #2 这种方法有个问题,由于只发生了一次构建,因此不能写清理步骤,但是路由变化后,按说应该发生清理操作,但是因为没有交给 useEffect 进行清理,只能手动在路由变化的地方清理,容易疏忽。
lDqe4OE6iOEUQNM7
360 天前
@rizon
export default function RenderTest() {
const [processor, setProcessor] = useState<number | undefined>();
const [initialized, setInitialized] = useState(false);

// build processor
useEffect(() => {
if (!initialized) {
const _processor = buildProcessor();
console.log("build processor", _processor.processor);
setProcessor(_processor.processor);
setInitialized(true); // 防止重复调用
return () => {
console.log("cleanup processor", _processor.processor);
_processor.destroy();
};
}
}, [initialized]); // 依赖 initialized 状态

// ...
}

function useExecuter(processor?: number) {
return useMemo(() => {
console.log("call processor", processor);
return processor;
}, [processor]); // 添加 processor 作为依赖项
}
rizon
360 天前
@rizon #6 所以这个方法适合无论路由如何变化都不需要重新初始化的绝对场景。
mota
360 天前
感觉保证 buildProcessor 不会重复实例好一点吧,如果放到 view 里面控制也是存一个 processor 的 ref ,然后每次 buildProcessor 的时候判断是否已经有 processor 了,在确保 processor 不会重复构造的话,useExecuter 的第一次这个问题应该也没了吧。
这个单例的逻辑判断最好不要靠渲染逻辑去控制吧。
ragnaroks
360 天前
确保只能初始化一次的东西要么写在外面 export 做 singleton ,要么用 signal 。
mengbi
360 天前
function useExecuter(processor?: number) {
return useMemo(() => {
console.log('call processor', processor)
return processor
}, [processor])
}

不应该这样吗
mengbi
360 天前
哦 上面已经有人发了
aaramsiconm1
360 天前
第一点 解决 buildProcessor 方法不能重复运行 最好采用单例模式创建 buildProcessor 或者使用存在全局的 store ,而不是通过控制 useEffect 的调用次数去保证 buildProcessor 只调用一次,第二点 useExecuter 方法只用第一次结果不再更新,我猜测是因为你的 useMemo 函数传入的第二个数组里没有添加依赖项,你可以尝试将 useExecuter 改写为:

function useExecuter(processor?: number) {
return useMemo(() => {
console.log("call processor", processor);
return processor;
}, [processor]);
}
sillydaddy
360 天前
官网解释 strict 模式下,都有哪些函数会调用 2 次:
https://legacy.reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects

strict 模式下,会 2 次调用下面这些函数:
```
类组件的 constructor, render, 和 shouldComponentUpdate 方法
类组件的 component static getDerivedStateFromProps 方法
函数组件的 bodies
State 的更新函数 updater functions (the first argument to setState)
传递到 useState, useMemo, 和 useReducer 中的函数体
```

React 18 好像是更直接,直接是 mount, unmount 再 mount 的流程:
https://legacy.reactjs.org/docs/strict-mode.html#ensuring-reusable-state
shunia
360 天前
用过几乎所有方案,最后有一天突然开窍了,用 react 不代表所有代码都要基于 react ,所以后面就把这种代码抽出来,做成一个模块,反而收益更高,既方便测试,又可以在必要的时候提升成一个包。
react 里面就让它随便渲染 N 次好了,反正都是 UI 相关代码,就按它自己的心智模型去跑。
himself65
358 天前
同意#15 的观点,你这个 React 用法是有问题的,你需要把 processor 的初始化放在 React 外面。举个例子

```ts
const processor = buildProcessor()
const Component =() => {}
```

如果不能 SSR 那就判断一下 typeof window === "undefined"。
如果是一个异步,包一个 use(promise)

useEffect 是连接外部状态的一个 hook ,那这个东西来初始化整个数据其实是不符合 React 的思路

https://react.dev/reference/react/useEffect#connecting-to-an-external-system

那这个举例,server 是 React UI 之外的已经有的东西,connection 才是 useEffect 去 establish 的事情

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/995553

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX