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

请教一个关于 useEffect 依赖的问题

  •  
  •   ooo4 · 1 天前 · 1011 次点击

    请教一个关于 useEffect 依赖的问题

    最近在学习 react 和 nextjs ,算初学者,感觉我写的很多 useEffect eslint 都提示缺少依赖,但其实我觉得写的依赖已经够了

    比如这样

      const [conversation, setConversation] = useState<Conversation[]>([])
      useEffect(() => {
        if (currentChatTitle) {
          setConversation(
            conversation.map((i) => {
              return {
                ...i,
                title: i.id === currentChatId ? currentChatTitle : i.title,
              }
            })
          )
        }
      }, [currentChatId, currentChatTitle])
    

    eslint 就说缺少conversation这个依赖,但是加了之后就无限执行这个 useEffect 回调了,其实我连这个currentChatId都不想加入依赖

    eslint 也给了解决方案就是改成setXXX((prev)=>xxx),但这样好麻烦啊,或者就是 disable 掉这一行

      useEffect(() => {
        if (currentChatTitle) {
          // 改成`setXXX((prev)=>xxx)`
          setConversation((conversation) =>
            conversation.map((i) => {
              return {
                ...i,
                title: i.id === currentChatId ? currentChatTitle : i.title,
              }
            })
          )
        }
      }, [currentChatId, currentChatTitle])
    

    请问下各位平时会关掉这个eslintreact-hooks/exhaustive-deps这个规则吗

    20 条回复    2025-02-22 11:22:21 +08:00
    darkengine
        1
    darkengine  
       1 天前
    能说说你这个代码要实现什么效果么,感觉逻辑不太对,可能要多引入一个 state
    MossFox
        2
    MossFox  
       1 天前   ❤️ 2
    就得用后面这段方案。否则的话,前面那个例子能跑通的原因也只是碰巧了,在于 每次因为其他 state 的变化导致的组件 rerender ,顺便更新了你的这个 useEffect 中的 conversation 。

    如果你用前面的方式发现某次显示的 conversation 是旧数据,那么就是掉坑里了。

    至于这个场景,我感觉可以试一下用 useMemo 。如果是要根据两个条件来筛选当前 active 的 conversation 或者类似的,那就
    const activeConversation = useMemo(() => {
    // 在这里面 map

    }, [conversation, xxx_id, xxx_title]);

    这样还能节约一次重绘,useEffect 那种 set 状态相当于 中括号里的变量变化一次重绘,useEffect 触发完因为 set 了 state 又触发一次重绘。useMemo 则可以是 一次绘制里面直接根据中括号里面的值,这一轮绘制就给变化的结果返回去。
    ooo4
        3
    ooo4  
    OP
       1 天前
    @darkengine 就是在/bar/foo 下的这个 page.tsx 更新了一个状态 currentChatTitle ,然后在/bar 下的 page.tsx 要触发 currentChatTitle 的副作用,再更新额外的状态
    ooo4
        4
    ooo4  
    OP
       1 天前
    @MossFox thanks ,我明天试试,顺便在看看文档
    wgbx
        5
    wgbx  
       1 天前
    react 的心智负担很大,往往对初学者不友好,首先,开发者理解的依赖和 react 需要的依赖是不一样的,你这个代码想在 currentChatId, currentChatTitle 变化时执行函数,但是每次函数更新时,conversation 因为在副作用引用了,也会更新,所以也需要监听,合理的办法确实是通过 setConversation 返回当前值进行更新

    如果你连 currentChatId 都不想加,说明 currentChatId 本身是常量或者不更新的值,你应该使用 useMemo 包裹起来,避免重复计算

    eslint 的规则不能关闭,他确实能反映依赖的问题,但是 ahooks 是必须使用的,作为 react 的 hooks 包装,能节省很多代码,另外你这个写法是不能够优化,currentChatTitle 在什么时候更新?初始化更新应该放在 useMount 上,事件触发应该放在函数里,这种情况的副作用不多见
    mizuki9
        6
    mizuki9  
       1 天前
    eslintreact-hooks/exhaustive-deps 规则应该设置报 warning ,不要报 error 。
    多个 state 有时候是可以合并成一个的,某些情况可以解决 useEffect 依赖报 warning 。
    明确知道自己逻辑正确的时候,忽略 warning 就好了,react 写多了就习惯了
    chesha1
        7
    chesha1  
       1 天前
    set 函数可以传入一个函数作为参数,也很清晰啊,这个更新函数接受旧值,返回新值,而且还更安全。如果 set 函数传入值,获取到外面的旧的 conversation 怎么办,传入函数就没问题了

    上面有人推荐使用 useMemo ,我不太建议,看起来你的这更新就是数组替换一下,这个渲染也不太复杂,没有必要缓存,缓存本身也是有成本的,无脑 useMemo 不是全是好处的,不考虑本身的成本,心智负担还变重了
    darkengine
        8
    darkengine  
       1 天前
    @MossFox 的方案是对的,就是要引入一个 activeConversation 用来渲染当前标题,不应该是去改 conversation (应该叫 conversations?)
    wiluxy
        9
    wiluxy  
       1 天前
    典型的滥用 useEffect 的例子,@MossFox 的方案是对的,至于 @chesha1 说的不推荐 useMemo ,这里 useMemo 的作用类似于派生状态,心智负担比用 useEffect 小
    ooo4
        10
    ooo4  
    OP
       1 天前
    @wgbx 不加这个 currentChatId 能满足我的功能,但 eslint 会抛出警告,加了又不满足我的功能了,react 太难了。
    wiluxy
        11
    wiluxy  
       1 天前
    @ooo4 如果不加这个 currentChatId 可以满足你的功能,非要用 useEffect 的话,可以考虑一下用 useReducer 组织状态

    ```typescript
    import React, {useEffect, useReducer, useState} from "react"


    type Conversation = {
    id:string
    title:string
    [key:string]:unknown
    }

    const initialState:Conversation[] = [
    {id:'1',title:"title-1"},
    {id:'2',title:"title-2"},
    {id:'3',title:"title-3"},
    ]

    function App() {
    const [currentChatId,setCurrentChatId] = useState("")
    const [currentChatTitle,setCurrentChatTitle] = useState("")

    const [conversation,setConversation] = useReducer<
    React.Reducer<
    Conversation[],
    (prev:Conversation[],curChatId:typeof currentChatId)=>Conversation[]
    >
    >((prev,action)=>{
    console.log(123)
    return action(prev,currentChatId)
    },initialState)

    useEffect(()=>{
    if(currentChatTitle){
    setConversation((prev,curChatId)=>{
    console.log({curChatId})
    return prev.map(i=>{
    return {
    ...i,
    title: i.id === curChatId ? currentChatTitle : i.title,
    }
    })
    })
    }
    },[currentChatTitle])

    return <div>
    <label htmlFor="currentChatId">currentChatId:</label>
    <input type="text" id="currentChatId" value={currentChatId} onChange={(e)=>setCurrentChatId(e.target.value)} />
    <label htmlFor="currentChatTitle">currentChatTitle:</label>
    <input type="text" id="currentChatTitle" value={currentChatTitle} onChange={(e)=>setCurrentChatTitle(e.target.value)} />
    <ul>
    {
    conversation.map(i=>{
    return <li key={i.id}>{i.title}</li>
    })
    }
    </ul>
    </div>
    }

    export default App;


    ```
    ooo4
        12
    ooo4  
    OP
       1 天前
    @wiluxy 感谢
    ooo4
        13
    ooo4  
    OP
       1 天前
    @MossFox 改成了 useMemo 解决我的问题了,逻辑也清晰多了
    ```js
    const conversationInfo = useMemo(() => {
    return conversation.map((i) => ({
    ...i,
    active: i.id === currentChatId,
    title:
    currentChatTitle && i.id === currentChatId ? currentChatTitle : i.title,
    }))
    }, [conversation, currentChatTitle, currentChatId])
    ```
    jchnxu
        14
    jchnxu  
       1 天前
    @MossFox 老哥和我想的一模一样。我直觉也是这两种解法。
    NerdHND
        15
    NerdHND  
       1 天前
    多用 useMemo, 如果实在是需要通过变更修改 state, 用 useReducer 也可以解决很多问题. 当然, 还是最好多构建单项数据流, 少用 flag
    yunyuyuan
        16
    yunyuyuan  
       1 天前
    2 楼说的很详细了。其实只要理解 react 的每一次渲染都是一个“单独”的闭包,里面的所有 state 都只代表当前渲染,把它当作一次性的,只用来计算和展示,就能搞明白大部分问题
    chesha1
        17
    chesha1  
       23 小时 45 分钟前
    @wiluxy #9 确实,我都没想到直接不要 useEffect 这个问题,最好做法肯定还是直接在 currentChatId 和 currentChatTitle 变化的事件内部直接用 setConversation 。我不推荐 useMemo ,主要是 useMemo 用起来尤其麻烦,需要注意 Memo 的依赖从头到尾都没问题,不然如果传递的层数比较深,谁随手搞一下 props 缓存就失效了(还有一些别的 memo 失效的场景,以前看的文章有点忘记了),所以我的想法是 profiler 之后再优化比较好?不太懂 react ,不知道这个做法是否最优
    wiluxy
        18
    wiluxy  
       23 小时 20 分钟前
    @chesha1 useMemo 、useEffect 漏依赖的问题,可以装个 eslint 插件(@eslint-react/eslint-plugin
    )辅助查看依赖有没有问题,遵循 hooks 规则写下去,等以后 react compiler 完善自动优化吧,现在没有性能问题就不用管太多了,只要功能正常、没有性能问题,re-render 不是什么大事情,老是想着最佳实践很累的。
    X_Del
        19
    X_Del  
       22 小时 50 分钟前   ❤️ 1
    不一定要多用 useMemo ,但一定要少用 useEffect 。
    见到很多 React 新人 useEffect 的时候,会创建很多多余的 state ,比如下面这种代码:

    ```
    const [lightColor, setLightColor] = useState<'red' | 'yellow' | 'green'>('red');
    const [canPass, setCanPass] = useState<boolean>(false);

    useEffect(() => {
    if (lightColor === 'green') setCanPass(true);
    else setCanPass(false);
    }, [lightColor]);
    ```

    这里 canPass 不该是一个 state ,根本就是一个 computed value ,用 useMemo 才对:

    ```
    const [lightColor, setLightColor] = useState<'red' | 'yellow' | 'green'>('red');
    const canPass = useMemo(() => lightColor === 'green', [lightColor]);
    ```

    大多数场合 useMemo 也是多余的,遇到性能问题再优化就可以:

    ```
    canPass = lightColor === 'green';
    ```

    所以我给 React 新人的建议都是:少用 useEffect ,如果遇到了必须 useEffect 的 case ,看看 ahooks 等库里有没有现成的 hook 。
    importmeta
        20
    importmeta  
       4 小时 27 分钟前
    这个问题是 eslint 搞得, 其实也可以不用管, 我调用第三方函数的时候, 这个依赖列表还是保留空数组, 它报警告就报警告, 等熟练了, 知道到底怎么依赖的时候, 关掉这条 eslint 规则就行.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2868 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 07:49 · PVG 15:49 · LAX 23:49 · JFK 02:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.