求助大佬, React 严格模式+函数式`setState`导致 key 重复

2024-01-06 00:00:20 +08:00
 dcsuibian

我有一个列表,我希望每次 ajax 请求获取一页数据后,将新的一页数据追加到列表当中。

在组件挂载时,我希望读取第一页的内容。简化后的代码可以写成这样:

import {useEffect, useState} from "react";

interface User {
    id: number
    name: string
}

const pageData = [
    {id: 1, name: '张三'},
]
export default function App() {
    const [users, setUsers] = useState<User[]>([])
    const fetchUsers = async () => {
        const page = pageData // 异步获取到数据
        setUsers([...users, ...page])
    }
    useEffect(() => {
        fetchUsers() // 发送异步请求
    }, [])
    // 渲染列表
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    )
}

这样不会有任何问题。

但是今天我问 ChatGPT 的时候,他给了我这么个回答。

我觉得有点道理。所以我就在想,我是往列表里追加数据,那肯定是依赖于前一个状态的。因此我就把代码从

setUsers([...users, ...page])

改成了

setUsers(prevUsers=>[...prevUsers, ...page])

完整代码 :

import {useEffect, useState} from "react";

interface User {
    id: number
    name: string
}

const pageData = [
    {id: 1, name: '张三'},
]
export default function App() {
    const [users, setUsers] = useState<User[]>([])
    const fetchUsers = async () => {
        const page = pageData // 异步获取到数据
        setUsers(prevUsers=>[...prevUsers, ...page])
    }
    useEffect(() => {
        fetchUsers() // 发送异步请求
    }, [])
    // 渲染列表
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    )
}

但这时候就出现了报错:

列表的数据追加了两次,key 也重复了。

我立马想到 React 的严格模式,果然关闭了,就又正常了:

到目前为止,我确实可以简单地

  1. 把 useState 改回去
  2. 关闭严格模式

以解决这个问题。

但是我觉得使用函数式的 setState 其实并没有太大问题,而 React 的严格模式又是官方推荐的。这俩者结合到一起,咋就出问题了呢?望大佬指点,是不是我使用的方式有问题。

1676 次点击
所在节点    React
13 条回复
qscasdqwezxc
2024-01-06 00:06:53 +08:00
useeffect 会 call 两次在 strict mode
防止你有泄露的资源每有清理
你初始化的时候应该先检查有没有初始化过
如果已经初始化过了就不用再初始化了
7immy
2024-01-06 00:27:01 +08:00
7immy
2024-01-06 00:41:31 +08:00
同 1L ,判断初始化,还有我感觉用不到 effect
Leviathann
2024-01-06 00:45:29 +08:00
自己写一个 useMounted(action)

用 ref 记录是否是初次渲染,在 useEffec 里根据 ref 判断要不要执行传入的 action
XCFOX
2024-01-06 01:08:20 +08:00
我认为应该根据 key 对 users 做去重。

假如有一个最新用户列表和一个 [加载更多用户] 按钮,页面不按页码分页,每次点击按钮时直接把下一页数据塞进当前列表里。

假如在第一次数据加载后过了一段时间再去点击 [加载更多用户] ,在这一段时间内可能会新的用户,此时用户列表内的 key 就会碰撞。

举例来说,假如每页取 3 项:
第一次加载第一页用户为: [张三, 张四, 张五];
新用户张一、张二注册,此时数据库中最新的 6 个用户为: [张一, 张二, 张三, 张四, 张五,张六];
第二次加载第二页用户为: [张四, 张五, 张六],此时前端用户列表为 [张三, 张四, 张五, 张四, 张五,张六] ,其中 张四, 张五 为重复数据;

既然无法避免重复,那就对列表做去重处理。

```tsx
const fetchUsers = useCallback(async () => {
const page = pageData // 异步获取到数据
setUsers((prevUsers) => {
const newUsers = page.filter(
(user) => !prevUsers.find((u) => u.id === user.id)
)
return [...prevUsers, ...newUsers]
})
}, [])
```
Leviathann
2024-01-06 01:36:11 +08:00
另外对于异步行为,需要考虑 race condition

所以在 set 之前要判断这个 action 是否已经被取消了

useEffect(() => {
let cancelled = false
fetch().then(data => {
if (!cancelled) {
setData(data)
})
return () => {
cancelled = true
}
}, [])

由于这个逻辑比较麻烦,所以现在官方推荐是使用专门的数据请求库做数据请求,而不是每次手写 fetch + set
liuzhaowei55
2024-01-06 09:18:02 +08:00
@7immy 看了#2 的链接,官方提到了楼主的场景,就是在外部添加一个标记,记录是否执行过

https://zh-hans.react.dev/learn/you-might-not-need-an-effect#initializing-the-application
dcsuibian
2024-01-06 21:40:01 +08:00
感谢各位的回答,我最终选择的方案又参考了:
https://www.bilibili.com/video/BV1AY4y1Q742
https://zh-hans.react.dev/reference/react/useRef#useref
使用的是修改 useRef 的 current 的方法。
codehz
2024-01-06 22:38:09 +08:00
我建议要发异步请求什么的,用 swr 这类库去处理吧,之后 ssr 和服务端组件都可以用上
arnosolo
2024-01-07 10:35:57 +08:00
你这个就是列表里有 id 相同的元素了.
另外, 使用 useEffect 时, 一定要返回一个清除函数把上一个请求给取消掉, 因为先发的请求可能会后返回.
lei737123456
2024-01-07 20:27:56 +08:00
是因为严格模式下 useEffect 会执行两次,而 useState 只会执行一次,相当于你执行了两遍
setUsers(prevUsers=>[...prevUsers, ...page])

连续追加了两次相同的 page ,所以你得数据出现了重复
lei737123456
2024-01-07 20:36:46 +08:00
@dcsuibian 没必要,你直接这样就行 setUsers([...users, ...page])
fancy2020
2024-01-08 14:39:57 +08:00

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1006312

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX