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

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

  •  
  •   dcsuibian · 306 天前 · 1557 次点击
    这是一个创建于 306 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我有一个列表,我希望每次 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  
       306 天前 via Android   ❤️ 1
    useeffect 会 call 两次在 strict mode
    防止你有泄露的资源每有清理
    你初始化的时候应该先检查有没有初始化过
    如果已经初始化过了就不用再初始化了
    7immy
        2
    7immy  
       306 天前 via Android   ❤️ 1
    7immy
        3
    7immy  
       306 天前 via Android
    同 1L ,判断初始化,还有我感觉用不到 effect
    Leviathann
        4
    Leviathann  
       306 天前   ❤️ 1
    自己写一个 useMounted(action)

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

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

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

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

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

    连续追加了两次相同的 page ,所以你得数据出现了重复
    lei737123456
        12
    lei737123456  
       305 天前   ❤️ 1
    @dcsuibian 没必要,你直接这样就行 setUsers([...users, ...page])
    fancy2020
        13
    fancy2020  
       304 天前
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2939 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 14:35 · PVG 22:35 · LAX 06:35 · JFK 09:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.