Remix中Defer/Suspense/Await未显示加载回退组件问题求助
Remix 使用 defer + Suspense 不显示加载骨架屏的问题
我创建了一个新的Remix应用,尝试在loader中用defer处理Promise,期望页面加载时显示<Suspense>的骨架屏,但实际页面会冻结直到Promise完成,完全看不到加载状态。想知道这是不是预期行为,哪里理解错了,以及如何实现数据加载前显示骨架屏。
现有代码
Index.tsx
import { defer } from "@remix-run/node"; import { Await, useLoaderData } from "@remix-run/react"; import { Suspense } from "react"; import DataFetchingExample from "~/components/data-fetching"; import Loading from "~/components/Loading"; export function loader() { const fetchPosts = async () => { await new Promise((resolve) => setTimeout(resolve, Number(4000))); const response = await fetch( "https://jsonplaceholder.typicode.com/photos", { // disable cache cache: "no-store", } ); if (!response || response.status !== 200) { console.error("Failed to fetch data"); } const data = await response.json(); const post = data[0]; return post; }; const promiseRequest = fetchPosts(); return defer({ post: promiseRequest, }); } export default function Index() { const data = useLoaderData<typeof loader>(); return ( <div> <h1>Welcome to Remix</h1> <Suspense fallback={<Skeleton />}> <Await resolve={data.post} errorElement={<p>Could not fetch photos</p>}> {(post) => <DataFetchingExample post={post} />} </Await> </Suspense> </div> ); }
components/data-fetching.tsx
type PageProps = { post: { albumId: number, id: number, title: string, url: string, thumbnailUrl: string, }, }; export default function DataFetchingExample({ post }: PageProps) { return ( <div> <p>Todo: {post?.title}</p> </div> ); }
问题原因
- 未启用Streaming SSR:Remix默认不启用流式渲染,即便使用
defer,服务器也会等待所有Promise完成后才返回完整HTML,导致客户端无法看到Suspense的fallback。 - 组件引用错误:代码中导入了
Loading组件,但在Suspense fallback中使用了未导入的<Skeleton />,这会引发组件不存在的错误(可能被忽略,但也是问题点)。 - 对defer的执行逻辑误解:loader代码运行在服务器端,若未启用Streaming,
defer并不会让服务器提前返回页面,反而会等待Promise执行完毕,自然看不到加载状态。
解决方案
1. 启用Streaming SSR
修改app/root.tsx,添加流式渲染的处理逻辑:
import { Links, Meta, Outlet, Scripts, ScrollRestoration, handleRequest, } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import { PassThrough } from "node:stream"; export default function App() { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> </head> <body> <Outlet /> <ScrollRestoration /> <Scripts /> </body> </html> ); } export async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: any ) { const body = new PassThrough(); const serverHtml = await RemixServer( { context: remixContext, url: request.url }, { abortDelay: 5000 } ); const { pipe } = await serverHtml; pipe(body); responseHeaders.set("Content-Type", "text/html"); return new Response(body, { headers: responseHeaders, status: responseStatusCode, }); }
2. 修复组件引用错误
在Index.tsx中,将Suspense的fallback替换为已导入的Loading组件:
<Suspense fallback={<Loading />}> <Await resolve={data.post} errorElement={<p>Could not fetch photos</p>}> {(post) => <DataFetchingExample post={post} />} </Await> </Suspense>
3. 验证效果
启用Streaming后,服务器会先发送包含标题和Loading组件的初始HTML,客户端立即显示加载状态;待Promise完成后,再流式传输数据并渲染DataFetchingExample组件,实现预期的加载体验。
内容的提问来源于stack exchange,提问作者arlioli03
相关产品推荐
相关产品推荐

