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

Next.js 14 SSR中Suspense fallback不显示问题求助(非实验性use Hook)

问题:Next.js 14 SSR模式下Suspense骨架屏不显示(仅空白)

使用Next.js v14.1.3(SSR模式)搭配React v18.2.0开发Web应用,客户端通过Firebase Functions获取数据时,期望用Suspense显示骨架屏,但实际仅显示空白(已确认骨架屏的HTML和CSS无问题)。

当前实现逻辑:在useEffect内调用Firebase Functions获取数据,通过useState更新diseases并传递给子组件DiseaseTagList,该子组件被包裹在Suspense中。奇怪的是,安装React实验版本并使用use Hook时,fallback能正常显示,不确定问题是否出在Firebase Functions、getServerSideProps或动态导入上。


代码示例

主组件代码

import { Functions, httpsCallable } from 'firebase/functions';
import dynamic from 'next/dynamic';
import styles from './styles.module.css';
import { Suspense, useState } from 'react';
const IconButton = dynamic(() => import('../../components/IconButton'));
const Shimmer = dynamic(() => import('../../components/Shimmer'));
const Button = dynamic(() => import('../../components/Button'));
const Sidebar = dynamic(() => import('../../components/Sidebar'));
const Background = dynamic(() => import('../../components/Background'));
const DiseaseTagList = dynamic(() => import('../../components/DiseaseTagList'));
const BranchList = dynamic(() => import('../../components/BranchList'));
import Head from "next/head";
import { Settings } from '../../utils/helpers';
import { useRouter } from 'next/router';
import  FirebaseService from '../../config/firebase';

export async function getServerSideProps(context) {
  const firebaseSingleton = FirebaseService.Instance;
  const functions = firebaseSingleton.Functions || {} as Functions;
  const getCategories = httpsCallable(functions, 'getCategories');
  var categories = await getCategories().then((categoriesData: any) => {return categoriesData.data});
  return {props:{categories}};
}

function QuestionsAndAnswers(props: { categories }) {
  const {categories} = props;
  const [isSidebarVisible, setIsSidebarVisible] = useState(false);
  const router = useRouter();
  const [diseases, setDiseases] = useState();
  

  useEffect(() => {
    const firebaseSingleton = FirebaseService.Instance;
    const functions = firebaseSingleton.Functions || {};

    const getMostAskedDiseaseTagsAsync = async () => {
      const getMostAskedDiseaseTags = httpsCallable(
        functions,
        'getMostAskedDiseaseTags'
      );
      await getMostAskedDiseaseTags().then((diseasesData: any) => {
        setDiseases(diseasesData.data);
      });
    }
    getMostAskedDiseaseTagsAsync();
  }, []);

  const toggleSidebar = () => {
    setIsSidebarVisible(!isSidebarVisible);
  };
  
  return (
    <>
      <div        
        className="home-container pt-2 qa-home-container question-detail-page">
        <div className="qa-page">
          <div className="hamburgerMenu pt-2 home-item text-center">
            <Sidebar
              onToggleSidebar={toggleSidebar}
              isSidebarVisible={isSidebarVisible}
            ></Sidebar>
          </div>
          <div className="questions-and-answers">
            <div className={styles["qa-parent-container"]}>
              <div className="go-back-btn">
                <IconButton
                  size="Medium"
                  variant="Secondary"
                  iconName="Back"
                  onClick={() => router.push('/')}
                ></IconButton>
              </div>
              <div className={styles["qa-wrapper"]}>
                <h1>Questions</h1>

                    <Suspense fallback={                        
                    <>
                      <Shimmer type="Text" />
                      <Shimmer type="Text" />
                      <Shimmer type="Text" />
                      <Shimmer type="Text" />
                      <Shimmer type="Text" />
                    </>}>
                      
                      <DiseaseTagList diseases={diseases}/>
                    </Suspense>
                  <BranchList categories={categories}/>
              </div>
            </div>
          </div>
        </div>
      </div>
    </>
  );
}

export default QuestionsAndAnswers;

DiseaseTagList组件代码

import styles from '../../pages/QuestionAndAnswers/styles.module.css';
import Link from 'next/link';
import { transformUrl } from 'src/utils/helpers';


function DiseaseTagList({diseases}) {  
return(
  <div className={styles["qa-container"]}>
  <div className={styles["diseases-container"]}>
    {
      diseases && (diseases as []).map((item: any, i: number) => 
      (
        <div key={i} className={styles["disease"]}>
          <Link href={`/${transformUrl(item.name)}/hastalik/${item.id}?p=1&fromInternal=true`}>{item.name}</Link > 
        </div>
      ))
    }
  </div>
</div>
)}

export default DiseaseTagList;

原因分析

React 18正式版中,Suspense仅支持内置的异步数据源(如React.lazy、Next.js动态导入、React Query/SWR等原生支持Suspense的库),而useEffect + useState的手动数据获取方式不属于Suspense可追踪的异步操作,因此Suspense不会触发fallback显示。实验版本的use Hook允许直接在组件中调用异步函数,让Suspense能捕获该异步状态,所以能正常显示骨架屏。


解决方法

方法1:使用支持Suspense的数据获取库(推荐)

比如SWR或React Query,它们原生支持Suspense,同时能更好地处理缓存、重新获取等逻辑。以SWR为例:

  1. 安装SWR:
npm install swr
  1. 封装数据获取函数:
import useSWR from 'swr';
import { httpsCallable } from 'firebase/functions';
import FirebaseService from '../../config/firebase';

const fetcher = async () => {
  const firebaseSingleton = FirebaseService.Instance;
  const functions = firebaseSingleton.Functions;
  const getMostAskedDiseaseTags = httpsCallable(functions, 'getMostAskedDiseaseTags');
  const res = await getMostAskedDiseaseTags();
  return res.data;
};
  1. 在组件中替换原有的useState和useEffect:
const { data: diseases } = useSWR('most-asked-diseases', fetcher, { suspense: true });

此时Suspense就能正常捕获加载状态,显示fallback骨架屏。

方法2:手动管理加载状态

若不想依赖第三方库,可自行维护加载状态并手动渲染骨架屏:

  1. 添加加载状态变量:
const [diseases, setDiseases] = useState();
const [isLoading, setIsLoading] = useState(true); // 新增加载状态
  1. 在useEffect中更新加载状态:
useEffect(() => {
  const getMostAskedDiseaseTagsAsync = async () => {
    const firebaseSingleton = FirebaseService.Instance;
    const functions = firebaseSingleton.Functions;
    const getMostAskedDiseaseTags = httpsCallable(functions, 'getMostAskedDiseaseTags');
    try {
      const diseasesData = await getMostAskedDiseaseTags();
      setDiseases(diseasesData.data);
    } finally {
      setIsLoading(false); // 无论请求成功/失败,结束加载状态
    }
  };
  getMostAskedDiseaseTagsAsync();
}, []);
  1. 替换Suspense逻辑,手动判断渲染:
{isLoading ? (
  <>
    <Shimmer type="Text" />
    <Shimmer type="Text" />
    <Shimmer type="Text" />
    <Shimmer type="Text" />
    <Shimmer type="Text" />
  </>
) : (
  <DiseaseTagList diseases={diseases} />
)}

方法3:使用React实验版use Hook(不推荐生产环境)

若一定要用Suspense配合原生异步函数,可使用React实验版的use Hook,但该特性仍处于实验阶段,生产环境需谨慎:

  1. 安装实验版React:
npm install react@experimental react-dom@experimental
  1. 重构数据获取逻辑:
import { use } from 'react';
import { httpsCallable } from 'firebase/functions';
import FirebaseService from '../../config/firebase';

function QuestionsAndAnswers(props: { categories }) {
  // ...其他现有代码

  // 定义异步数据获取函数
  const fetchDiseases = async () => {
    const firebaseSingleton = FirebaseService.Instance;
    const functions = firebaseSingleton.Functions;
    const getMostAskedDiseaseTags = httpsCallable(functions, 'getMostAskedDiseaseTags');
    const res = await getMostAskedDiseaseTags();
    return res.data;
  };

  // 使用use Hook直接调用异步函数
  const diseases = use(fetchDiseases());

  // ...其他现有代码
}

此时Suspense能正常捕获异步加载状态,显示fallback骨架屏。


内容的提问来源于stack exchange,提问作者mctuna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 02:19:55