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

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

  •  
  •   dcsuibian · 2024-01-06 00:00:20 +08:00 · 1642 次点击
    这是一个创建于 378 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我有一个列表,我希望每次 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 的时候,他给了我这么个回答。

    image-20240105232743265

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

    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>
        )
    }
    

    但这时候就出现了报错:

    image-20240105233219474

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

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

    image-20240105233658151

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

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

    以解决这个问题。

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

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

    用 ref 记录是否是初次渲染,在 useEffec 里根据 ref 判断要不要执行传入的 action
    XCFOX
        5
    XCFOX  
       2024-01-06 01:08:20 +08:00   ❤️ 1
    我认为应该根据 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
        6
    Leviathann  
       2024-01-06 01:36:11 +08:00   ❤️ 1
    另外对于异步行为,需要考虑 race condition

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

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

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

    https://zh-hans.react.dev/learn/you-might-not-need-an-effect#initializing-the-application
    dcsuibian
        8
    dcsuibian  
    OP
       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
        9
    codehz  
       2024-01-06 22:38:09 +08:00
    我建议要发异步请求什么的,用 swr 这类库去处理吧,之后 ssr 和服务端组件都可以用上
    arnosolo
        10
    arnosolo  
       2024-01-07 10:35:57 +08:00
    你这个就是列表里有 id 相同的元素了.
    另外, 使用 useEffect 时, 一定要返回一个清除函数把上一个请求给取消掉, 因为先发的请求可能会后返回.
    lei737123456
        11
    lei737123456  
       2024-01-07 20:27:56 +08:00
    是因为严格模式下 useEffect 会执行两次,而 useState 只会执行一次,相当于你执行了两遍
    setUsers(prevUsers=>[...prevUsers, ...page])

    连续追加了两次相同的 page ,所以你得数据出现了重复
    lei737123456
        12
    lei737123456  
       2024-01-07 20:36:46 +08:00   ❤️ 1
    @dcsuibian 没必要,你直接这样就行 setUsers([...users, ...page])
    fancy2020
        13
    fancy2020  
       2024-01-08 14:39:57 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2701 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:14 · PVG 18:14 · LAX 02:14 · JFK 05:14
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.