请教一个关于 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
这个规则吗
![]() |
1
darkengine 1 天前
能说说你这个代码要实现什么效果么,感觉逻辑不太对,可能要多引入一个 state
|
![]() |
2
MossFox 1 天前 ![]() 就得用后面这段方案。否则的话,前面那个例子能跑通的原因也只是碰巧了,在于 每次因为其他 state 的变化导致的组件 rerender ,顺便更新了你的这个 useEffect 中的 conversation 。
如果你用前面的方式发现某次显示的 conversation 是旧数据,那么就是掉坑里了。 至于这个场景,我感觉可以试一下用 useMemo 。如果是要根据两个条件来筛选当前 active 的 conversation 或者类似的,那就 const activeConversation = useMemo(() => { // 在这里面 map }, [conversation, xxx_id, xxx_title]); 这样还能节约一次重绘,useEffect 那种 set 状态相当于 中括号里的变量变化一次重绘,useEffect 触发完因为 set 了 state 又触发一次重绘。useMemo 则可以是 一次绘制里面直接根据中括号里面的值,这一轮绘制就给变化的结果返回去。 |
![]() |
3
ooo4 OP @darkengine 就是在/bar/foo 下的这个 page.tsx 更新了一个状态 currentChatTitle ,然后在/bar 下的 page.tsx 要触发 currentChatTitle 的副作用,再更新额外的状态
|
![]() |
5
wgbx 1 天前
react 的心智负担很大,往往对初学者不友好,首先,开发者理解的依赖和 react 需要的依赖是不一样的,你这个代码想在 currentChatId, currentChatTitle 变化时执行函数,但是每次函数更新时,conversation 因为在副作用引用了,也会更新,所以也需要监听,合理的办法确实是通过 setConversation 返回当前值进行更新
如果你连 currentChatId 都不想加,说明 currentChatId 本身是常量或者不更新的值,你应该使用 useMemo 包裹起来,避免重复计算 eslint 的规则不能关闭,他确实能反映依赖的问题,但是 ahooks 是必须使用的,作为 react 的 hooks 包装,能节省很多代码,另外你这个写法是不能够优化,currentChatTitle 在什么时候更新?初始化更新应该放在 useMount 上,事件触发应该放在函数里,这种情况的副作用不多见 |
6
mizuki9 1 天前
eslintreact-hooks/exhaustive-deps 规则应该设置报 warning ,不要报 error 。
多个 state 有时候是可以合并成一个的,某些情况可以解决 useEffect 依赖报 warning 。 明确知道自己逻辑正确的时候,忽略 warning 就好了,react 写多了就习惯了 |
7
chesha1 1 天前
set 函数可以传入一个函数作为参数,也很清晰啊,这个更新函数接受旧值,返回新值,而且还更安全。如果 set 函数传入值,获取到外面的旧的 conversation 怎么办,传入函数就没问题了
上面有人推荐使用 useMemo ,我不太建议,看起来你的这更新就是数组替换一下,这个渲染也不太复杂,没有必要缓存,缓存本身也是有成本的,无脑 useMemo 不是全是好处的,不考虑本身的成本,心智负担还变重了 |
![]() |
8
darkengine 1 天前
@MossFox 的方案是对的,就是要引入一个 activeConversation 用来渲染当前标题,不应该是去改 conversation (应该叫 conversations?)
|
![]() |
9
wiluxy 1 天前
典型的滥用 useEffect 的例子,@MossFox 的方案是对的,至于 @chesha1 说的不推荐 useMemo ,这里 useMemo 的作用类似于派生状态,心智负担比用 useEffect 小
|
![]() |
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; ``` |
![]() |
13
ooo4 OP |
![]() |
15
NerdHND 1 天前
多用 useMemo, 如果实在是需要通过变更修改 state, 用 useReducer 也可以解决很多问题. 当然, 还是最好多构建单项数据流, 少用 flag
|
![]() |
16
yunyuyuan 1 天前
2 楼说的很详细了。其实只要理解 react 的每一次渲染都是一个“单独”的闭包,里面的所有 state 都只代表当前渲染,把它当作一次性的,只用来计算和展示,就能搞明白大部分问题
|
17
chesha1 1 天前
@wiluxy #9 确实,我都没想到直接不要 useEffect 这个问题,最好做法肯定还是直接在 currentChatId 和 currentChatTitle 变化的事件内部直接用 setConversation 。我不推荐 useMemo ,主要是 useMemo 用起来尤其麻烦,需要注意 Memo 的依赖从头到尾都没问题,不然如果传递的层数比较深,谁随手搞一下 props 缓存就失效了(还有一些别的 memo 失效的场景,以前看的文章有点忘记了),所以我的想法是 profiler 之后再优化比较好?不太懂 react ,不知道这个做法是否最优
![]() |
![]() |
18
wiluxy 1 天前
@chesha1 useMemo 、useEffect 漏依赖的问题,可以装个 eslint 插件(@eslint-react/eslint-plugin
)辅助查看依赖有没有问题,遵循 hooks 规则写下去,等以后 react compiler 完善自动优化吧,现在没有性能问题就不用管太多了,只要功能正常、没有性能问题,re-render 不是什么大事情,老是想着最佳实践很累的。 |
![]() |
19
X_Del 23 小时 32 分钟前 ![]() 不一定要多用 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 。 |
![]() |
20
importmeta 5 小时 8 分钟前
这个问题是 eslint 搞得, 其实也可以不用管, 我调用第三方函数的时候, 这个依赖列表还是保留空数组, 它报警告就报警告, 等熟练了, 知道到底怎么依赖的时候, 关掉这条 eslint 规则就行.
|