V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rizon
V2EX  ›  React

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

  •  
  •   rizon ·
    othorizon · 2023-11-27 13:13:52 +08:00 · 2239 次点击
    这是一个创建于 391 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前提场景:代码中 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 ,因此虚心求教大佬们。

    16 条回复    2023-11-29 10:58:47 +08:00
    zed1018
        1
    zed1018  
       2023-11-27 13:21:17 +08:00   ❤️ 2
    1. useEffect 在 development 和 strictmode 下执行两次是他们故意这么设定的,我没记错的话意思就是让你处理好 useEffect 里逻辑的幂等关系。

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

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


    @sweetcola #3 放全局 store 里也是一种选择,也挺好的。
    rizon
        6
    rizon  
    OP
       2023-11-27 14:46:17 +08:00
    @jsun969 #2 这种方法有个问题,由于只发生了一次构建,因此不能写清理步骤,但是路由变化后,按说应该发生清理操作,但是因为没有交给 useEffect 进行清理,只能手动在路由变化的地方清理,容易疏忽。
    lDqe4OE6iOEUQNM7
        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 作为依赖项
    }
    rizon
        8
    rizon  
    OP
       2023-11-27 14:50:00 +08:00
    @rizon #6 所以这个方法适合无论路由如何变化都不需要重新初始化的绝对场景。
    mota
        9
    mota  
       2023-11-27 15:18:30 +08:00
    感觉保证 buildProcessor 不会重复实例好一点吧,如果放到 view 里面控制也是存一个 processor 的 ref ,然后每次 buildProcessor 的时候判断是否已经有 processor 了,在确保 processor 不会重复构造的话,useExecuter 的第一次这个问题应该也没了吧。
    这个单例的逻辑判断最好不要靠渲染逻辑去控制吧。
    ragnaroks
        10
    ragnaroks  
       2023-11-27 15:41:04 +08:00
    确保只能初始化一次的东西要么写在外面 export 做 singleton ,要么用 signal 。
    mengbi
        11
    mengbi  
       2023-11-27 16:16:38 +08:00
    function useExecuter(processor?: number) {
    return useMemo(() => {
    console.log('call processor', processor)
    return processor
    }, [processor])
    }

    不应该这样吗
    mengbi
        12
    mengbi  
       2023-11-27 16:17:22 +08:00
    哦 上面已经有人发了
    aaramsiconm1
        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]);
    }
    sillydaddy
        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
    shunia
        15
    shunia  
       2023-11-27 21:06:57 +08:00
    用过几乎所有方案,最后有一天突然开窍了,用 react 不代表所有代码都要基于 react ,所以后面就把这种代码抽出来,做成一个模块,反而收益更高,既方便测试,又可以在必要的时候提升成一个包。
    react 里面就让它随便渲染 N 次好了,反正都是 UI 相关代码,就按它自己的心智模型去跑。
    himself65
        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 的事情
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1290 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 17:30 · PVG 01:30 · LAX 09:30 · JFK 12:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.