请教一个关于 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这个规则吗

能说说你这个代码要实现什么效果么,感觉逻辑不太对,可能要多引入一个 state

就得用后面这段方案。否则的话,前面那个例子能跑通的原因也只是碰巧了,在于 每次因为其他 state 的变化导致的组件 rerender ,顺便更新了你的这个 useEffect 中的 conversation 。

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

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

}, [conversation, xxx_id, xxx_title]);

这样还能节约一次重绘,useEffect 那种 set 状态相当于 中括号里的变量变化一次重绘,useEffect 触发完因为 set 了 state 又触发一次重绘。useMemo 则可以是 一次绘制里面直接根据中括号里面的值,这一轮绘制就给变化的结果返回去。

就是在/bar/foo 下的这个 page.tsx 更新了一个状态 currentChatTitle ,然后在/bar 下的 page.tsx 要触发 currentChatTitle 的副作用,再更新额外的状态

thanks ,我明天试试,顺便在看看文档

react 的心智负担很大,往往对初学者不友好,首先,开发者理解的依赖和 react 需要的依赖是不一样的,你这个代码想在 currentChatId, currentChatTitle 变化时执行函数,但是每次函数更新时,conversation 因为在副作用引用了,也会更新,所以也需要监听,合理的办法确实是通过 setConversation 返回当前值进行更新

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

eslint 的规则不能关闭,他确实能反映依赖的问题,但是 ahooks 是必须使用的,作为 react 的 hooks 包装,能节省很多代码,另外你这个写法是不能够优化,currentChatTitle 在什么时候更新?初始化更新应该放在 useMount 上,事件触发应该放在函数里,这种情况的副作用不多见

eslintreact-hooks/exhaustive-deps 规则应该设置报 warning ,不要报 error 。
多个 state 有时候是可以合并成一个的,某些情况可以解决 useEffect 依赖报 warning 。
明确知道自己逻辑正确的时候,忽略 warning 就好了,react 写多了就习惯了

set 函数可以传入一个函数作为参数,也很清晰啊,这个更新函数接受旧值,返回新值,而且还更安全。如果 set 函数传入值,获取到外面的旧的 conversation 怎么办,传入函数就没问题了

上面有人推荐使用 useMemo ,我不太建议,看起来你的这更新就是数组替换一下,这个渲染也不太复杂,没有必要缓存,缓存本身也是有成本的,无脑 useMemo 不是全是好处的,不考虑本身的成本,心智负担还变重了

的方案是对的,就是要引入一个 activeConversation 用来渲染当前标题,不应该是去改 conversation (应该叫 conversations?)

典型的滥用 useEffect 的例子,@MossFox 的方案是对的,至于 说的不推荐 useMemo ,这里 useMemo 的作用类似于派生状态,心智负担比用 useEffect 小

不加这个 currentChatId 能满足我的功能,但 eslint 会抛出警告,加了又不满足我的功能了,react 太难了。

如果不加这个 currentChatId 可以满足你的功能,非要用 useEffect 的话,可以考虑一下用 useReducer 组织状态

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;

感谢

改成了 useMemo 解决我的问题了,逻辑也清晰多了

 const conversationInfo = useMemo(() => {
 return conversation.map((i) => ({
 ...i,
 active: i.id === currentChatId,
 title:
 currentChatTitle && i.id === currentChatId ? currentChatTitle : i.title,
 }))
 }, [conversation, currentChatTitle, currentChatId])

老哥和我想的一模一样。我直觉也是这两种解法。

多用 useMemo, 如果实在是需要通过变更修改 state, 用 useReducer 也可以解决很多问题. 当然, 还是最好多构建单项数据流, 少用 flag

2 楼说的很详细了。其实只要理解 react 的每一次渲染都是一个“单独”的闭包,里面的所有 state 都只代表当前渲染,把它当作一次性的,只用来计算和展示,就能搞明白大部分问题

#9 确实,我都没想到直接不要 useEffect 这个问题,最好做法肯定还是直接在 currentChatId 和 currentChatTitle 变化的事件内部直接用 setConversation 。我不推荐 useMemo ,主要是 useMemo 用起来尤其麻烦,需要注意 Memo 的依赖从头到尾都没问题,不然如果传递的层数比较深,谁随手搞一下 props 缓存就失效了(还有一些别的 memo 失效的场景,以前看的文章有点忘记了),所以我的想法是 profiler 之后再优化比较好?不太懂 react ,不知道这个做法是否最优

useMemo 、useEffect 漏依赖的问题,可以装个 eslint 插件(@eslint-react/eslint-plugin
)辅助查看依赖有没有问题,遵循 hooks 规则写下去,等以后 react compiler 完善自动优化吧,现在没有性能问题就不用管太多了,只要功能正常、没有性能问题,re-render 不是什么大事情,老是想着最佳实践很累的。

不一定要多用 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 。

这个问题是 eslint 搞得, 其实也可以不用管, 我调用第三方函数的时候, 这个依赖列表还是保留空数组, 它报警告就报警告, 等熟练了, 知道到底怎么依赖的时候, 关掉这条 eslint 规则就行.