Next.js 14无限滚动加载UI闪烁问题求助
解决方案:分离初始加载与后续加载状态,避免无限滚动时Loading闪烁
核心思路
问题出在你把「初始页面加载」和「后续数据追加」的Loading状态混在一起了。Next.js的Server Action默认会触发全局Loading状态,但你需要单独管控两种加载场景:
- 初始加载:保留
loading.jsx或自定义全屏Loading - 后续滚动加载:仅在列表底部显示轻量加载提示(或静默加载),不触发全局闪烁
具体实现步骤
1. 拆分加载状态变量
在客户端组件中用两个独立状态区分场景:
isInitialLoading:控制首次页面加载的Loading UIisLoadingMore:控制滚动到底部时的加载状态
2. 初始加载逻辑处理
- 首次渲染时通过Server Action获取初始数据,完成后关闭初始Loading
- 若用服务器组件预取初始数据,可直接将数据传递给客户端组件,此时
loading.jsx会自动生效
3. 滚动加载与数据追加
- 用
useState管理列表数据(useRef无法触发组件渲染,所以必须用useState) - 滚动监听触发时,手动切换
isLoadingMore状态,获取新数据后通过扩展运算符追加到现有列表中
示例代码
'use client'; import { useState, useEffect } from 'react'; import { fetchGitHubRepos } from '@/actions'; // 你的Server Action export default function InfiniteRepoList() { const [repos, setRepos] = useState([]); const [isInitialLoading, setIsInitialLoading] = useState(true); const [isLoadingMore, setIsLoadingMore] = useState(false); const [currentPage, setCurrentPage] = useState(1); // 初始加载数据 useEffect(() => { const loadInitialData = async () => { const initialRepos = await fetchGitHubRepos(currentPage); setRepos(initialRepos); setIsInitialLoading(false); }; loadInitialData(); }, []); // 滚动监听逻辑 useEffect(() => { const handleScroll = async () => { // 检测是否滚动到接近底部 const isNearBottom = window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight - 100; if (isNearBottom && !isLoadingMore && !isInitialLoading) { setIsLoadingMore(true); const newRepos = await fetchGitHubRepos(currentPage + 1); // 追加新数据,保留已有内容 setRepos(prev => [...prev, ...newRepos]); setCurrentPage(prev => prev + 1); setIsLoadingMore(false); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [currentPage, isLoadingMore, isInitialLoading]); // 初始加载时显示全局Loading if (isInitialLoading) { return ( <div className="flex h-screen items-center justify-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div> </div> ); } return ( <div className="container mx-auto p-4"> <div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> {repos.map(repo => ( <div key={repo.id} className="border rounded-lg p-4 hover:shadow-md transition-shadow"> <h3 className="font-bold text-lg">{repo.name}</h3> <p className="text-gray-600 mt-1 line-clamp-2">{repo.description}</p> <div className="mt-3 flex items-center gap-2 text-sm text-gray-500"> <span>⭐ {repo.stargazers_count}</span> <span>🍴 {repo.forks_count}</span> </div> </div> ))} </div> {/* 后续加载仅显示底部小Loading,无全局闪烁 */} {isLoadingMore && ( <div className="mt-6 flex justify-center"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div> </div> )} </div> ); }
对应的Server Action示例(app/actions.js):
'use server'; import { Octokit } from "@octokit/rest"; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); export async function fetchGitHubRepos(page = 1, perPage = 12) { const { data } = await octokit.rest.repos.listForUser({ username: "your-github-username", per_page: perPage, page: page, }); return data; }
关键注意事项
- 滚动监听必须放在客户端组件中,因为滚动是浏览器端行为,服务器组件无法处理
- 手动管控
isLoadingMore状态,避免依赖Next.js自动的全局Loading(比如useFormStatus),防止触发页面级闪烁 - 始终用
useState管理列表数据,确保新数据追加时DOM仅更新新增项,保留已有内容
内容的提问来源于stack exchange,提问作者Kahon
相关产品推荐
相关产品推荐

