Streaming allows you to enhance user experience by delivering content as soon as it's available, rather than waiting for the entire content of a page to be ready.
Ensure your hosting provider supports streaming, not all of them do. If your responses don't seem to stream, this might be the cause.
There are three steps to streaming data:
Ready from Start: Remix apps created using starter templates are pre-configured for streaming.
Manual Setup Needed?: If your project began from scratch or used an older template, verify entry.server.tsx
and entry.client.tsx
have streaming support. If you don't see these files then you are using the defaults and streaming is supported. If you have created your own entries, the following are the template defaults for your reference:
A route module without streaming might look like this:
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader({
params,
}: LoaderFunctionArgs) {
const [product, reviews] = await Promise.all([
db.getProduct(params.productId),
db.getReviews(params.productId),
]);
return json({ product, reviews });
}
export default function Product() {
const { product, reviews } =
useLoaderData<typeof loader>();
return (
<>
<ProductPage data={product} />
<ProductReviews data={reviews} />
</>
);
}
In order to render streamed data, you need to use <Suspense>
from React and <Await>
from Remix. It's a bit of boilerplate, but straightforward:
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
import { ReviewsSkeleton } from "./reviews-skeleton";
export async function loader({
params,
}: LoaderFunctionArgs) {
// existing code
}
export default function Product() {
const { product, reviews } =
useLoaderData<typeof loader>();
return (
<>
<ProductPage data={product} />
<Suspense fallback={<ReviewsSkeleton />}>
<Await resolve={reviews}>
{(reviews) => <ProductReviews data={reviews} />}
</Await>
</Suspense>
</>
);
}
This code will continue to work even before we start deferring data. It's a good idea to do the component code first. If you run into issues, it's easier to track down where the problem lies.
Now that our project and route component are set up stream data, we can start deferring data in our loaders. We'll use the defer
utility from Remix to do this.
Note the change in the async promise code.
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { defer } from "@remix-run/node"; // or cloudflare/deno
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
import { ReviewsSkeleton } from "./reviews-skeleton";
export async function loader({
params,
}: LoaderFunctionArgs) {
// 👇 note this promise is not awaited
const reviewsPromise = db.getReviews(params.productId);
// 👇 but this one is
const product = await db.getProduct(params.productId);
return defer({
product,
reviews: reviewsPromise,
});
}
export default function Product() {
const { product, reviews } =
useLoaderData<typeof loader>();
// existing code
}
Instead of awaiting the reviews promise, we pass it to defer
. This tells Remix to stream that promise over the network to the browser.
That's it! You should now be streaming data to the browser.
It's important to initiate promises for deferred data before you await any other promises, otherwise you won't get the full benefit of streaming. Note the difference with this less efficient code example:
export async function loader({
params,
}: LoaderFunctionArgs) {
const product = await db.getProduct(params.productId);
// 👇 this won't initiate loading until `product` is done
const reviewsPromise = db.getReviews(params.productId);
return defer({
product,
reviews: reviewsPromise,
});
}