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

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>
  );
}

问题原因

  1. 未启用Streaming SSR:Remix默认不启用流式渲染,即便使用defer,服务器也会等待所有Promise完成后才返回完整HTML,导致客户端无法看到Suspense的fallback。
  2. 组件引用错误:代码中导入了Loading组件,但在Suspense fallback中使用了未导入的<Skeleton />,这会引发组件不存在的错误(可能被忽略,但也是问题点)。
  3. 对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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 04:17:32