React用户列表滚动懒加载实现遇问题,求正确解决方案
React 用户列表滚动懒加载问题排查与修复
我有一个包含1000个用户的数组,原本通过map方法渲染全部用户,现需实现懒加载:初始加载前50个用户,滚动至底部时加载下一批50个,直至所有用户加载完成。我尝试了两种实现方式,但均存在问题:第一种仅加载前50个后就停止;第二种使用Intersection Observer实现了滚动加载,但存在bug(如共400个用户却仅加载250个)。
无懒加载实现的组件
import { useState, useEffect, useRef } from "react"; import { users } from "../../users/generateUsers.ts"; import UserItem from "../userItem/UserItem.tsx"; import { nanoid } from "nanoid"; import "../userList/UserList.css"; export default function UserList() { return ( <div className="wrapper"> <div className="content"> {users.map((user, index) => ( <div className={`userCard `} key={nanoid()}> <span className="card-number"> {index + 1} </span> <UserItem color={user.color} speed={user.speed} name={user.name} time={user.time} /> </div> ))} </div> </div> ); }
懒加载实现第一版(滚动事件监听)
import { useState, useEffect, useRef } from "react"; import { users } from "../../users/generateUsers.ts"; import UserItem from "../userItem/UserItem.tsx"; import { nanoid } from "nanoid"; import "../userList/UserList.css"; import { User } from "../../types/types.ts"; export default function UserList() { const batchSize = 50; const [loadedUsers, setLoadedUsers] = useState<User[]>([]); const [startIndex, setStartIndex] = useState(0); const [totalUsers, setTotalUsers] = useState(users.length); const [selectedUser, setSelectedUser] = useState<User | null>(null); const contentRef = useRef<HTMLDivElement>(null); useEffect(() => { loadMoreUsers(); }, []); const loadMoreUsers = () => { const endIndex = Math.min(startIndex + batchSize, totalUsers); const nextBatch = users.slice(startIndex, endIndex); setLoadedUsers((prevLoadedUsers) => [...prevLoadedUsers, ...nextBatch]); setStartIndex(endIndex); }; const handleScroll = () => { const contentElement = contentRef.current; if ( contentElement && contentElement.scrollTop + contentElement.clientHeight === contentElement.scrollHeight && startIndex < totalUsers ) { loadMoreUsers(); } }; return ( <div className="wrapper" onScroll={handleScroll}> <div className="content" ref={contentRef}> {loadedUsers.map((user, index) => ( <div className={`userCard `} key={nanoid()}> <span className="card-number">{index + 1}</span> <UserItem color={user.color} speed={user.speed} name={user.name} time={user.time} /> </div> ))} </div> </div> ); }
使用Intersection Observer的第二版
import { useState, useEffect, useRef } from "react"; import { users } from "../../users/generateUsers.ts"; import UserItem from "../userItem/UserItem.tsx"; import { nanoid } from "nanoid"; import "../userList/UserList.css"; export default function UserList() { const batchSize = 50; const [loadedUsers, setLoadedUsers] = useState<any[]>([]); const [startIndex, setStartIndex] = useState(0); const [totalUsers, setTotalUsers] = useState(users.length); const [selectedUser, setSelectedUser] = useState<any | null>(null); const sentinelRef = useRef<HTMLDivElement>(null); useEffect(() => { loadMoreUsers(); }, []); useEffect(() => { const observer = new IntersectionObserver( (entries) => { const target = entries[0]; if (target.isIntersecting && startIndex < totalUsers) { loadMoreUsers(); } }, { threshold: 0.9 } ); if (sentinelRef.current) { observer.observe(sentinelRef.current); } return () => { if (sentinelRef.current) { observer.unobserve(sentinelRef.current); } }; }, [startIndex, totalUsers]); const loadMoreUsers = () => { const endIndex = Math.min(startIndex + batchSize, totalUsers); const nextBatch = users.slice(startIndex, endIndex); setLoadedUsers((prevLoadedUsers) => [...prevLoadedUsers, ...nextBatch]); setStartIndex(endIndex); }; const handleUserClick = (user: any) => { setSelectedUser(user); }; return ( <div className="wrapper"> <div className="content"> {loadedUsers.map((user, index) => ( <div className={`userCard `} key={nanoid()} onClick={() => handleUserClick(user)} > <span className="card-number">{index + 1}</span> <UserItem color={user.color} speed={user.speed} name={user.name} time={user.time} selected={selectedUser === user} /> </div> ))} </div> <div ref={sentinelRef} style={{ height: "1px" }} /> </div> ); }
问题排查与修复
第一版问题原因及修复
问题点
- 滚动事件绑定错误:
onScroll绑定在wrapper上,但如果wrapper未设置overflow: auto和固定高度,滚动条实际属于window,导致事件无法触发。 - 滚动判断条件严苛:
scrollTop + clientHeight === scrollHeight因浏览器精度问题,可能无法触发。 - 闭包问题:
handleScroll中读取的startIndex是旧值,导致加载判断失效。
修复后代码
import { useState, useEffect, useRef } from "react"; import { users } from "../../users/generateUsers.ts"; import UserItem from "../userItem/UserItem.tsx"; import { nanoid } from "nanoid"; import "../userList/UserList.css"; import { User } from "../../types/types.ts"; export default function UserList() { const batchSize = 50; const [loadedUsers, setLoadedUsers] = useState<User[]>([]); const [startIndex, setStartIndex] = useState(0); const totalUsers = users.length; const wrapperRef = useRef<HTMLDivElement>(null); useEffect(() => { loadMoreUsers(); }, []); useEffect(() => { const wrapper = wrapperRef.current; if (!wrapper) return; const handleScroll = () => { if ( wrapper.scrollTop + wrapper.clientHeight >= wrapper.scrollHeight - 10 && startIndex < totalUsers ) { loadMoreUsers(); } }; wrapper.addEventListener("scroll", handleScroll); return () => wrapper.removeEventListener("scroll", handleScroll); }, [startIndex, totalUsers]); const loadMoreUsers = () => { const endIndex = Math.min(startIndex + batchSize, totalUsers); setLoadedUsers(prev => [...prev, ...users.slice(startIndex, endIndex)]); setStartIndex(endIndex); }; return ( <div className="wrapper" ref={wrapperRef} style={{ overflow: "auto", height: "100vh" }} > <div className="content"> {loadedUsers.map((user, index) => ( <div className="userCard" key={nanoid()}> <span className="card-number">{index + 1}</span> <UserItem color={user.color} speed={user.speed} name={user.name} time={user.time} /> </div> ))} </div> </div> ); }
第二版问题原因及修复
问题点
- 阈值设置过高:
threshold: 0.9要求1px高的sentinel元素90%可见,几乎无法触发。 - Observer重复创建:每次
startIndex变化都重新创建Observer,导致观察逻辑混乱。 - 闭包问题:回调中读取的
startIndex可能为旧值,导致加载判断错误。
修复后代码
import { useState, useEffect, useRef } from "react"; import { users } from "../../users/generateUsers.ts"; import UserItem from "../userItem/UserItem.tsx"; import { nanoid } from "nanoid"; import "../userList/UserList.css"; import { User } from "../../types/types.ts"; export default function UserList() { const batchSize = 50; const [loadedUsers, setLoadedUsers] = useState<User[]>([]); const [startIndex, setStartIndex] = useState(0); const totalUsers = users.length; const sentinelRef = useRef<HTMLDivElement>(null); const observerRef = useRef<IntersectionObserver | null>(null); useEffect(() => { loadMoreUsers(); }, []); useEffect(() => { if (!observerRef.current) { observerRef.current = new IntersectionObserver( (entries) => { const target = entries[0]; if (target.isIntersecting && startIndex < totalUsers) { loadMoreUsers(); } }, { threshold: 0.1 } ); } const sentinel = sentinelRef.current; if (sentinel) observerRef.current.observe(sentinel); return () => { if (sentinel && observerRef.current) { observerRef.current.unobserve(sentinel); } }; }, [startIndex, totalUsers]); const loadMoreUsers = () => { const endIndex = Math.min(startIndex + batchSize, totalUsers); setLoadedUsers(prev => [...prev, ...users.slice(startIndex, endIndex)]); setStartIndex(endIndex); if (endIndex >= totalUsers && sentinelRef.current && observerRef.current) { observerRef.current.unobserve(sentinelRef.current); } }; return ( <div className="wrapper"> <div className="content"> {loadedUsers.map((user, index) => ( <div className="userCard" key={nanoid()}> <span className="card-number">{index + 1}</span> <UserItem color={user.color} speed={user.speed} name={user.name} time={user.time} /> </div> ))} </div> <div ref={sentinelRef} style={{ height: "20px", margin: "10px 0", backgroundColor: "transparent" }} /> </div> ); }
内容的提问来源于stack exchange,提问作者Somebody
相关产品推荐
相关产品推荐

