You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14无限滚动加载UI闪烁问题求助

解决方案:分离初始加载与后续加载状态,避免无限滚动时Loading闪烁

核心思路

问题出在你把「初始页面加载」和「后续数据追加」的Loading状态混在一起了。Next.js的Server Action默认会触发全局Loading状态,但你需要单独管控两种加载场景:

  • 初始加载:保留loading.jsx或自定义全屏Loading
  • 后续滚动加载:仅在列表底部显示轻量加载提示(或静默加载),不触发全局闪烁

具体实现步骤

1. 拆分加载状态变量

在客户端组件中用两个独立状态区分场景:

  • isInitialLoading:控制首次页面加载的Loading UI
  • isLoadingMore:控制滚动到底部时的加载状态

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 09:32:27