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

请教一个 react hook 的问题

  •  3
     
  •   Asuler · 2024-03-29 10:34:07 +08:00 · 3393 次点击
    这是一个创建于 367 天前的主题,其中的信息可能已经有所发展或是发生改变。

    关于 react 的 useEffect ,如果我要监听一个值,并且在值变化的时候做一些函数调用

      useEffect(()=>{
        if(type === aaa){
          aHandle();
        }else if(type === bbb){
          bHandle();
        }
      }, [type])
    

    那我就需要把 aHandle 和 bHandle 放入依赖项中,否则 eslint 或者 sonarlint 会报警告

      useEffect(()=>{
        if(type === aaa){
          aHandle();
        }else if(type === bbb){
          bHandle();
        }
      }, [type, aHandle, bHandle])
    

    那这样岂不是每次 render ,都会重新生成新的 ahandle 和 bHandle ,每次都会触发 useEffect 吗?

    我知道有 useCallback 和 useRef 可以解决函数在每次 render 都重新生成的问题,但问题是假如我在 aHandle 里去调接口,也要获取很多放在 state 中的值作为参数,那么 useCallback 还得把那些值全部放在 useCallback 的依赖项里,搞得越来越复杂了。

    难道只能用 useRef 或者基于 useRef+useCallback 封装的一些 hook ,把每个 aHandle 或者 bHandle 给套上吗。

    有没有更优雅一点的写法,我想要有一个 useXXXEffect,可以只监听一个 type ,在这里面获取到的其他值都是最新的,不再额外需要传入 aHandle 或者 bHandle 。

    有没有这样的 hook 或者封装成这种效果的 hook

    第 1 条附言  ·  364 天前
    统一回复下:eslint 和 sonarlint 不是我说禁用就能禁用的,公司私有化部署了代码扫描平台,会用这些扫描代码,作为员工绩效的一部分,我实属无奈呀,非常难受,如果是自己的项目,那我就直接不管了,害
    第 2 条附言  ·  364 天前
    再统一回复下:未出世的 useEvent ,useRef ,useMemoizedFn 之类的,本质上都要对那些 aHandle ,bHandle 套上,但是
    1. 我需要给每个 handle 函数都给套上
    2. 如果有些函数本身已经套了某种 hook 的,我难道还要再套一层?比如 useDebounceFn ,useThrottleFn ,已经套过一层了,难道要再套一层,我感觉不太优雅了
    53 条回复    2024-04-04 09:08:28 +08:00
    NessajCN
        1
    NessajCN  
       2024-03-29 10:36:39 +08:00
    aHandle bHandle 不会每次 render 重新生成
    weixind
        2
    weixind  
       2024-03-29 10:40:43 +08:00
    使用 react-query 或者类似的东西。
    Asuler
        3
    Asuler  
    OP
       2024-03-29 10:44:01 +08:00
    @NessajCN 啊?
    sadyx
        4
    sadyx  
       2024-03-29 10:46:56 +08:00
    ahooks/useRequest 感觉可以满足你的需求
    Nyeshuai
        5
    Nyeshuai  
       2024-03-29 10:52:01 +08:00   ❤️ 2
    这个场景不需要 useEffect, 如果你需要根据某个值触发对应操作, 应该是在获取到 type 的位置调用对应函数, 即便有多个位置.
    10bkill1p
        6
    10bkill1p  
       2024-03-29 10:53:26 +08:00
    react unforget
    XFeng34
        7
    XFeng34  
       2024-03-29 10:57:50 +08:00   ❤️ 1
    meta575 说的对,你没要实现这个功能的话,可以试试 ahooks 提供的 useMemoizedFn
    lisianthus
        8
    lisianthus  
       2024-03-29 11:03:09 +08:00
    我是这样这样处理的:每次渲染重新生成函数,然后用 ref 引用这个函数,这样的话函数就能获取到最新值,也不用把函数加到 useEffect 的依赖里,不知道有没有优雅的写法。

    aHandleRef.current = () => {};

    useEffect(() => {
    if (type === "a") {
    aHandleRef.current();
    }
    }, [type])
    sjhhjx0122
        9
    sjhhjx0122  
       2024-03-29 11:05:44 +08:00
    如果你的 type 是外部传进来的,其实你完全可以直接写不需要 useEffect
    Asuler
        10
    Asuler  
    OP
       2024-03-29 11:17:26 +08:00
    但是用了 useRef 的话,其实就跟 ahook 里的 useMemoizedFn 类似处理了,这样子就又回到我说的问题了: 难道只能用 useRef 或者基于 useRef+useCallback 封装的一些 hook ,把每个 aHandle 或者 bHandle 给套上吗

    害,有没有更优雅的方式
    Asuler
        11
    Asuler  
    OP
       2024-03-29 11:19:45 +08:00
    type 是存在 state 中的一个状态,类似小程序底部 tabbar 选中高亮的一中选中状态
    7anshuai
        12
    7anshuai  
       2024-03-29 11:21:37 +08:00
    https://react.dev/reference/react/useEffect#removing-unnecessary-function-dependencies

    把 aHandle 或 bHandle 函数定义放在组件外面或者 useEffect(() => { function aHandle() {} })
    sweetcola
        13
    sweetcola  
       2024-03-29 11:22:06 +08:00
    之前在 reactjs/rfcs 看到的 useEvent 就是用来解决这种问题的

    ```
    function useEvent(handler) {
    const handlerRef = useRef(null);
    useLayoutEffect(() => {
    handlerRef.current = handler;
    });
    return useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
    }, []);
    }
    ```

    或者直接
    ```
    const xxx = useRef();
    xxx = () => {};

    ...

    xxx.current();
    ```
    Asuler
        14
    Asuler  
    OP
       2024-03-29 11:25:52 +08:00
    @sweetcola useEvent 其实就跟 ahook 的 useMemoizedFn 一样,但都是用在函数上的,如果涉及的函数很多,每一个都要这样套上,感觉又不太好
    leroy20317
        15
    leroy20317  
       2024-03-29 11:26:16 +08:00   ❤️ 5
    // eslint-disable-next-line react-hooks/exhaustive-deps 😏
    sweetcola
        16
    sweetcola  
       2024-03-29 11:32:17 +08:00
    @Asuler React 是这样的,不愿意这样做那就只能状态和函数一起外移了
    HTML001
        17
    HTML001  
       2024-03-29 11:32:34 +08:00   ❤️ 1
    @Asuler #10 我之前用 react 也遇到同样的问题,最后使用的 8 楼的 ref 方式(但是这样写有种不得劲的感觉,总有种在 react/vue 里面直接操作 dom 一样的违和感)
    zkkz
        18
    zkkz  
       2024-03-29 11:51:01 +08:00
    @leroy20317 正解,useEffect 里面拿到的函数 aHandle ,bHandle 都是最新的,不需要放到依赖项里面。
    DesnLee
        19
    DesnLee  
       2024-03-29 13:38:11 +08:00
    具体逻辑不清楚,如果没看过建议先看看 https://zh-hans.react.dev/learn/you-might-not-need-an-effect ,有时候并不需要 useEffect
    jjwjiang
        20
    jjwjiang  
       2024-03-29 13:48:09 +08:00
    很简单,你不要在 aHandle 和 bHandle 里用 prop 和 state 就可以了,你把他们当成 sateless 的方法,需要这类状态直接通过 effect 里传进去,就不会有依赖问题了
    realJamespond
        21
    realJamespond  
       2024-03-29 13:49:13 +08:00
    用 ref 是正解,不过如果 aHandle ,bHandle 没有其他依赖项也可以正常引用,就是一直保持组件渲染第一次的地址
    Radix10
        22
    Radix10  
       2024-03-29 14:32:16 +08:00
    这个应该不需要 hook 吧
    super996
        23
    super996  
       2024-03-29 14:46:41 +08:00   ❤️ 1
    在切换 type 的那个 onChange/onSelect/onClick 做,
    onChange={(type: string) => {
    if (type === 'aaa') {
    aHandle()
    } else if (type === 'bbb') {
    bHandle()
    }
    // ...
    }}
    Marthemis
        24
    Marthemis  
       2024-03-29 15:04:35 +08:00
    meta575 super996 是正解。useEffect 是处理函数的副作用,而不是去监听值(这两者在某些场景下容易混淆)
    wang4012055
        25
    wang4012055  
       2024-03-29 15:11:16 +08:00
    函数式编程理念是函数无副作用,所以你的调用函数最好不要引用外部状态,使用传参方式.这样使用 callback 就没什么问题了.
    jinliming2
        26
    jinliming2  
       2024-03-29 15:33:36 +08:00 via iPhone
    不知道你的 aHandle 和 bHandle 的具体逻辑,不过仅目前的这段代码的逻辑来说,按照我的思路,我会把 aHandle 和 bHandle 直接写成 useEffect 。
    useEffect(() => {
    if (type !== aaa) return;
    // aHandle 的函数体,直接处理,而不是调函数
    }, [type]);
    useEffect(() => {
    if (type !== bbb) return;
    // bHandle 的函数体,如果要异步处理,就立即执行包一下
    let cancel = false;
    (async () => {
    await xxx;
    if (cancel) {
    return;
    }
    //...
    })();
    return () => {
    cancel = true;
    };
    }, [type]);
    IvanLi127
        27
    IvanLi127  
       2024-03-29 16:08:29 +08:00
    按你这个需求,好像直接把那一行的 lint 禁用掉好了……
    Terry166
        28
    Terry166  
       2024-03-29 16:17:25 +08:00
    Effect Events are not reactive and must always be omitted from dependencies of your Effect. This is what lets you put non-reactive code (where you can read the latest value of some props and state) inside of them.
    参考文档:
    https://react.dev/learn/separating-events-from-effects#reading-latest-props-and-state-with-effect-events
    w4ngzhen
        29
    w4ngzhen  
       2024-03-29 16:42:37 +08:00
    ```jsx
    const App = () => {
    const [count, setCount] = useState(1);
    const handle = () => {
    console.log('做些事');
    }
    useEffect(() => {
    handle();
    }, [count])
    return <button onClick={() => setCount(count + 1)}>Add One</div>
    }
    ```

    感觉大家没有回答到点上啊。首先,React 中的函数式组件,每“运行”一次,是一个时刻的结果。比如上面的 App 函数,完成一次加载以后。实际上就是运行了一次 App 函数,对于第一次视为 t1 ,t1 流程是:
    1. 初始化了一个 count 的 state
    2. 通过变量`handle`定义了一个函数
    3. 执行了一次 useEffect
    4. 返回了一个`<button />`

    这里面最关键的点是步骤 3 执行 useEffect 。在第一次运行的时候,这个匿名方法:

    ```js
    // t1 时刻的匿名函数
    () => {
    handle(); // t1 时刻的 handle 变量
    }
    ```

    被 React 存放到了内部,并且它捕获了 t1 时刻的变量`handle`,并且,通过`[count]`定义了依赖项。并且,t1 的匿名函数会执行一次。

    当你点击按钮的时候,由于调用了 setCount ,在上述场景下,会导致 App 重新执行一次,我们把第二次执行的流程视为 t2 。它的过程是:

    1. 由于第 2 次了,useState 拿到的值不再是初始值,而是上一次 set 的值,在上面的例子是 2 ;
    2. 通过变量`handle`定义了一个函数。这里的 handle ,跟 t1 阶段的 handle 完全是两个变量,它们仅仅是名字一样,代码块一样而已。
    3. 执行一次 useEffect 。此时,生成了一个 t2 时刻的匿名函数:

    ```js
    // t2 匿名
    () => {
    handle(); // 这里的 handle 也是 t2 时刻的 handle ,跟 t1 的 handle 没有任何关系
    }
    ```

    此时,t1 的 count = 1 与 t2 的 count = 2 不一样了,所以,useEffect 中的匿名函数( t2 版本)会执行一次,handle 自然就是 t2 版本的 handle 。

    另外,上述场景中的 handle 能用 count 这个 state 吗?当然可以,因为 t2 时刻的 handle 捕获的是 t2 时刻的 count 。
    w4ngzhen
        30
    w4ngzhen  
       2024-03-29 16:45:24 +08:00
    另外,useEffect 一定要分两步看:它“吃”了一个匿名函数,但是不意味着它立刻会调用;调用的时机取决 React 所的定义的依赖判定。
    w4ngzhen
        31
    w4ngzhen  
       2024-03-29 16:46:46 +08:00
    “那我就需要把 aHandle 和 bHandle 放入依赖项中,否则 eslint 或者 sonarlint 会报警告” —— eslint 具体的警告是什么?感觉你这种场景是很常见的。aHandle 和 bHandle 有什么特殊之处吗?
    w4ngzhen
        32
    w4ngzhen  
       2024-03-29 16:48:33 +08:00
    @meta575 你说的这种也是正确的处理方式,通过封装无状态的方法,在多个 type 变化的地方调用也是解决方案。
    wpzz
        33
    wpzz  
       2024-03-29 16:50:31 +08:00
    useEffect 不要这么写,如果里面代码量上来了,refs [] 会监听超级多变量和函数。

    这里面函数又套了 useCallback ,维护会爆炸。
    Makabaka01
        34
    Makabaka01  
       2024-03-29 17:17:33 +08:00
    把 lint 禁掉就行了,hooks lint 不太智能,挺烦的
    neotheone2333
        35
    neotheone2333  
       2024-03-29 17:32:21 +08:00
    非常的典型的 useEvent 场景,直接用 useMemoizedFn 就行了。否则就放在 onClick 里面做
    leoskey
        36
    leoskey  
       2024-03-29 17:46:51 +08:00
    正如前排所说你可能不需要使用 useEffect 。useEffect 不应该用于监听某个东西的变化,应该用于组件时卸载清理的工作,例如取消订阅。

    官方文档专门对这种情况进行了说明 https://react.dev/learn/you-might-not-need-an-effect
    leoskey
        37
    leoskey  
       2024-03-29 17:48:35 +08:00
    非要这么写,那就按你第一份代码,把那一行的 lint 禁用
    otakustay
        38
    otakustay  
       2024-03-29 18:03:37 +08:00
    最新版本是 useEffectEvent ,以前版本是用 useRef 存它,然后再补个 useEffect 更新它
    connection
        39
    connection  
       2024-03-29 20:18:10 +08:00
    @otakustay 以前都得这么干 TAT
    iOCZS
        40
    iOCZS  
       2024-03-29 20:30:47 +08:00
    主要是 aHandle 是不是纯的,它是纯的话,你甚至可以拿到组件外边去。如果不纯的话,它依赖什么数据?依赖变化的时候,它是需要更新的。
    Asuler
        41
    Asuler  
    OP
       2024-03-29 20:45:18 +08:00
    @w4ngzhen eslint 或者 sonarlint 警告就是说你没把 aHandle 和 bHandle 加到依赖项里,他们的特殊之处就是里面包含了一些异步的复杂逻辑,需要每次调用时都取到外部最新的值
    Asuler
        42
    Asuler  
    OP
       2024-03-29 20:46:27 +08:00
    统一回复下:eslint 和 sonarlint 不是我说禁用就能禁用的,公司私有化部署了代码扫描平台,会用这些扫描代码,作为员工绩效的一部分,我实属无奈呀,非常难受,如果是自己的项目,那我就直接不管了,害
    Asuler
        43
    Asuler  
    OP
       2024-03-29 20:47:30 +08:00
    所以说,useEffect 的用法,我其实理解错了,不应该这么用对么
    Asuler
        44
    Asuler  
    OP
       2024-03-29 20:50:16 +08:00
    再统一回复下:未出世的 useEvent ,useRef ,useMemoizedFn 之类的,本质上都要对那些 aHandle ,bHandle 套上,但是
    1. 我需要给每个 handle 函数都给套上
    2. 如果有些函数本身已经套了某种 hook 的,我难道还要再套一层?比如 useDebounceFn ,useThrottleFn ,已经套过一层了,难道要再套一层,我感觉不太优雅了
    rocmax
        45
    rocmax  
       2024-03-29 20:51:30 +08:00 via Android
    是不是把 vue 里用 watch 的习惯带来了?
    type 在哪里变的就在哪里挂处理函数,useeffect 不是干这个的
    lee88688
        46
    lee88688  
       2024-03-29 22:44:45 +08:00
    闭包问题,在 react 里面没有太好的解决方案,op 说的是否还要再往上套的问题是肯定的,如果想要保持引用稳定就是要付出这个代价,这也就是 react 目前的问题。
    vue 或者 solidjs 这些没有这个问题是因为他们所有的数据外面都有一层壳,vue 的 val.value ,solidjs 的 value(),都是帮你在外面加了一层,当然渲染机制也不同,所以加壳就加吧。
    zbowen66
        47
    zbowen66  
       2024-03-30 11:52:56 +08:00
    @rocmax #45 别洗了,缺点就是缺点
    dudubaba
        48
    dudubaba  
       2024-03-30 16:07:15 +08:00
    这种感觉就是 eslint 的锅, 这种场景还是很多的,因为 useEffect 不能直接用 async ,所以函数定义在 useEffect 里又不能共用,抽出来又出现你这种依赖报错问题,但是实际上不加函数依赖是正常的,用这种依赖问题又一大堆冗余代码。如果全局 eslint 改不掉建议用 eslint-disable-next-line 这种禁用掉。
    rocmax
        49
    rocmax  
       2024-03-30 16:45:34 +08:00 via Android
    @zbowen66 vue boy 这就破防了?
    vue 里面不是一样不推荐滥用 watch 吗?到处 watch 的屎山我是见过的,哪里改变状态在哪里处理 state 不很正常吗?
    react 手动依赖收集是比较麻烦,但这跟不该用 useeffect 监听状态修改有一毛钱关系吗?
    zbowen66
        50
    zbowen66  
       2024-03-30 23:23:54 +08:00
    @rocmax #49 哈哈,说点 react 的缺点就被喷是 vue boy ,祝你生活愉快😁,告辞
    jjwjiang
        51
    jjwjiang  
       364 天前
    @w4ngzhen 确实是这样,但是如果在 function 里有闭包那就是另一回事了。所以我觉得 eslint 这规则大部分时候是没用的,这规则最终导致的结果就是需要 memo 一大堆东西
    HTML001
        52
    HTML001  
       362 天前
    四天过去了,OP 最终选择什么方案?
    Asuler
        53
    Asuler  
    OP
       361 天前
    @HTML001 hook 往上套呗,代码都写了那么多了,不可能重构的,只要没出线上问题就不重构
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   965 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 20:32 · PVG 04:32 · LAX 13:32 · JFK 16:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.