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