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

请教一个 react hook 的问题

  •  3
     
  •   Asuler · 268 天前 · 3138 次点击
    这是一个创建于 268 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    aHandleRef.current = () => {};

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

    害,有没有更优雅的方式
    Asuler
        11
    Asuler  
    OP
       268 天前
    type 是存在 state 中的一个状态,类似小程序底部 tabbar 选中高亮的一中选中状态
    7anshuai
        12
    7anshuai  
       268 天前
    https://react.dev/reference/react/useEffect#removing-unnecessary-function-dependencies

    把 aHandle 或 bHandle 函数定义放在组件外面或者 useEffect(() => { function aHandle() {} })
    sweetcola
        13
    sweetcola  
       268 天前
    之前在 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
       268 天前
    @sweetcola useEvent 其实就跟 ahook 的 useMemoizedFn 一样,但都是用在函数上的,如果涉及的函数很多,每一个都要这样套上,感觉又不太好
    leroy20317
        15
    leroy20317  
       268 天前   ❤️ 5
    // eslint-disable-next-line react-hooks/exhaustive-deps 😏
    sweetcola
        16
    sweetcola  
       268 天前
    @Asuler React 是这样的,不愿意这样做那就只能状态和函数一起外移了
    HTML001
        17
    HTML001  
       268 天前   ❤️ 1
    @Asuler #10 我之前用 react 也遇到同样的问题,最后使用的 8 楼的 ref 方式(但是这样写有种不得劲的感觉,总有种在 react/vue 里面直接操作 dom 一样的违和感)
    zkkz
        18
    zkkz  
       268 天前
    @leroy20317 正解,useEffect 里面拿到的函数 aHandle ,bHandle 都是最新的,不需要放到依赖项里面。
    DesnLee
        19
    DesnLee  
       268 天前
    具体逻辑不清楚,如果没看过建议先看看 https://zh-hans.react.dev/learn/you-might-not-need-an-effect ,有时候并不需要 useEffect
    jjwjiang
        20
    jjwjiang  
       268 天前
    很简单,你不要在 aHandle 和 bHandle 里用 prop 和 state 就可以了,你把他们当成 sateless 的方法,需要这类状态直接通过 effect 里传进去,就不会有依赖问题了
    realJamespond
        21
    realJamespond  
       268 天前
    用 ref 是正解,不过如果 aHandle ,bHandle 没有其他依赖项也可以正常引用,就是一直保持组件渲染第一次的地址
    Radix10
        22
    Radix10  
       267 天前
    这个应该不需要 hook 吧
    super996
        23
    super996  
       267 天前   ❤️ 1
    在切换 type 的那个 onChange/onSelect/onClick 做,
    onChange={(type: string) => {
    if (type === 'aaa') {
    aHandle()
    } else if (type === 'bbb') {
    bHandle()
    }
    // ...
    }}
    Marthemis
        24
    Marthemis  
       267 天前
    meta575 super996 是正解。useEffect 是处理函数的副作用,而不是去监听值(这两者在某些场景下容易混淆)
    wang4012055
        25
    wang4012055  
       267 天前
    函数式编程理念是函数无副作用,所以你的调用函数最好不要引用外部状态,使用传参方式.这样使用 callback 就没什么问题了.
    jinliming2
        26
    jinliming2  
       267 天前 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  
       267 天前
    按你这个需求,好像直接把那一行的 lint 禁用掉好了……
    Terry166
        28
    Terry166  
       267 天前
    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  
       267 天前
    ```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  
       267 天前
    另外,useEffect 一定要分两步看:它“吃”了一个匿名函数,但是不意味着它立刻会调用;调用的时机取决 React 所的定义的依赖判定。
    w4ngzhen
        31
    w4ngzhen  
       267 天前
    “那我就需要把 aHandle 和 bHandle 放入依赖项中,否则 eslint 或者 sonarlint 会报警告” —— eslint 具体的警告是什么?感觉你这种场景是很常见的。aHandle 和 bHandle 有什么特殊之处吗?
    w4ngzhen
        32
    w4ngzhen  
       267 天前
    @meta575 你说的这种也是正确的处理方式,通过封装无状态的方法,在多个 type 变化的地方调用也是解决方案。
    wpzz
        33
    wpzz  
       267 天前
    useEffect 不要这么写,如果里面代码量上来了,refs [] 会监听超级多变量和函数。

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

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