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

124 天前
 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 的严格模式又是官方推荐的。这俩者结合到一起,咋就出问题了呢?望大佬指点,是不是我使用的方式有问题。

1311 次点击
所在节点    React
13 条回复
qscasdqwezxc
124 天前
useeffect 会 call 两次在 strict mode
防止你有泄露的资源每有清理
你初始化的时候应该先检查有没有初始化过
如果已经初始化过了就不用再初始化了
7immy
124 天前
7immy
124 天前
同 1L ,判断初始化,还有我感觉用不到 effect
Leviathann
124 天前
自己写一个 useMounted(action)

用 ref 记录是否是初次渲染,在 useEffec 里根据 ref 判断要不要执行传入的 action
XCFOX
124 天前
我认为应该根据 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
124 天前
另外对于异步行为,需要考虑 race condition

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

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

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

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

连续追加了两次相同的 page ,所以你得数据出现了重复
lei737123456
122 天前
@dcsuibian 没必要,你直接这样就行 setUsers([...users, ...page])
fancy2020
121 天前

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

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

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

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

© 2021 V2EX