前提场景:代码中 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 ,因此虚心求教大佬们。
1
zed1018 341 天前 2
1. useEffect 在 development 和 strictmode 下执行两次是他们故意这么设定的,我没记错的话意思就是让你处理好 useEffect 里逻辑的幂等关系。
2. 如果你需要一个更贴近生命周期的 callback ,要么使用 class component ,要么使用 react-use 这个库 |
2
jsun969 341 天前 1
写一个 `useMount`,用 useRef 锁一下
https://gist.github.com/jsun969/102413cdf730a0942483f5fa2fe80167 |
3
sweetcola 341 天前
把 buildProcessor 移出去,找个 store 存放或者直接写到另一个文件里 export 出去,我觉得严格模式的一个意义就是提醒开发者,这些东西不应该放在这里,如果组件被卸载后再次挂载,依然会再执行一次。
|
4
bojackhorseman 341 天前
|
5
rizon OP |
6
rizon OP @jsun969 #2 这种方法有个问题,由于只发生了一次构建,因此不能写清理步骤,但是路由变化后,按说应该发生清理操作,但是因为没有交给 useEffect 进行清理,只能手动在路由变化的地方清理,容易疏忽。
|
7
lDqe4OE6iOEUQNM7 341 天前
@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 作为依赖项 } |
9
mota 341 天前
感觉保证 buildProcessor 不会重复实例好一点吧,如果放到 view 里面控制也是存一个 processor 的 ref ,然后每次 buildProcessor 的时候判断是否已经有 processor 了,在确保 processor 不会重复构造的话,useExecuter 的第一次这个问题应该也没了吧。
这个单例的逻辑判断最好不要靠渲染逻辑去控制吧。 |
10
ragnaroks 341 天前
确保只能初始化一次的东西要么写在外面 export 做 singleton ,要么用 signal 。
|
11
mengbi 341 天前
function useExecuter(processor?: number) {
return useMemo(() => { console.log('call processor', processor) return processor }, [processor]) } 不应该这样吗 |
12
mengbi 341 天前
哦 上面已经有人发了
|
13
aaramsiconm1 341 天前
第一点 解决 buildProcessor 方法不能重复运行 最好采用单例模式创建 buildProcessor 或者使用存在全局的 store ,而不是通过控制 useEffect 的调用次数去保证 buildProcessor 只调用一次,第二点 useExecuter 方法只用第一次结果不再更新,我猜测是因为你的 useMemo 函数传入的第二个数组里没有添加依赖项,你可以尝试将 useExecuter 改写为:
function useExecuter(processor?: number) { return useMemo(() => { console.log("call processor", processor); return processor; }, [processor]); } |
14
sillydaddy 341 天前
官网解释 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 |
15
shunia 341 天前
用过几乎所有方案,最后有一天突然开窍了,用 react 不代表所有代码都要基于 react ,所以后面就把这种代码抽出来,做成一个模块,反而收益更高,既方便测试,又可以在必要的时候提升成一个包。
react 里面就让它随便渲染 N 次好了,反正都是 UI 相关代码,就按它自己的心智模型去跑。 |
16
himself65 339 天前
同意#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 的事情 |