Next.js 14 SSR中Suspense fallback不显示问题求助(非实验性use Hook)
使用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为例:
- 安装SWR:
npm install swr
- 封装数据获取函数:
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; };
- 在组件中替换原有的
useState和useEffect:
const { data: diseases } = useSWR('most-asked-diseases', fetcher, { suspense: true });
此时Suspense就能正常捕获加载状态,显示fallback骨架屏。
方法2:手动管理加载状态
若不想依赖第三方库,可自行维护加载状态并手动渲染骨架屏:
- 添加加载状态变量:
const [diseases, setDiseases] = useState(); const [isLoading, setIsLoading] = useState(true); // 新增加载状态
- 在
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(); }, []);
- 替换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,但该特性仍处于实验阶段,生产环境需谨慎:
- 安装实验版React:
npm install react@experimental react-dom@experimental
- 重构数据获取逻辑:
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

