我有一个列表,我希望每次 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 的严格模式,果然关闭了,就又正常了:
到目前为止,我确实可以简单地
以解决这个问题。
但是我觉得使用函数式的 setState 其实并没有太大问题,而 React 的严格模式又是官方推荐的。这俩者结合到一起,咋就出问题了呢?望大佬指点,是不是我使用的方式有问题。
1
qscasdqwezxc 317 天前 via Android 1
useeffect 会 call 两次在 strict mode
防止你有泄露的资源每有清理 你初始化的时候应该先检查有没有初始化过 如果已经初始化过了就不用再初始化了 |
2
7immy 317 天前 via Android 1
|
3
7immy 317 天前 via Android
同 1L ,判断初始化,还有我感觉用不到 effect
|
4
Leviathann 317 天前 1
自己写一个 useMounted(action)
用 ref 记录是否是初次渲染,在 useEffec 里根据 ref 判断要不要执行传入的 action |
5
XCFOX 317 天前 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] }) }, []) ``` |
6
Leviathann 317 天前 1
另外对于异步行为,需要考虑 race condition
所以在 set 之前要判断这个 action 是否已经被取消了 useEffect(() => { let cancelled = false fetch().then(data => { if (!cancelled) { setData(data) }) return () => { cancelled = true } }, []) 由于这个逻辑比较麻烦,所以现在官方推荐是使用专门的数据请求库做数据请求,而不是每次手写 fetch + set |
7
liuzhaowei55 317 天前 via Android 1
@7immy 看了#2 的链接,官方提到了楼主的场景,就是在外部添加一个标记,记录是否执行过
https://zh-hans.react.dev/learn/you-might-not-need-an-effect#initializing-the-application |
8
dcsuibian OP 感谢各位的回答,我最终选择的方案又参考了:
https://www.bilibili.com/video/BV1AY4y1Q742 https://zh-hans.react.dev/reference/react/useRef#useref 使用的是修改 useRef 的 current 的方法。 |
9
codehz 316 天前
我建议要发异步请求什么的,用 swr 这类库去处理吧,之后 ssr 和服务端组件都可以用上
|
10
arnosolo 316 天前
你这个就是列表里有 id 相同的元素了.
另外, 使用 useEffect 时, 一定要返回一个清除函数把上一个请求给取消掉, 因为先发的请求可能会后返回. |
11
lei737123456 315 天前
是因为严格模式下 useEffect 会执行两次,而 useState 只会执行一次,相当于你执行了两遍
setUsers(prevUsers=>[...prevUsers, ...page]) 连续追加了两次相同的 page ,所以你得数据出现了重复 |
12
lei737123456 315 天前 1
@dcsuibian 没必要,你直接这样就行 setUsers([...users, ...page])
|
13
fancy2020 314 天前
应该是需要做 cleanup 吧:
https://react.dev/learn/synchronizing-with-effects#fetching-data |