Port async components from Next.js
You'll write a report page the way Next's App Router teaches, with an async component that awaits its data. fsr will refuse it and tell you where the await goes. Then you'll split the chart into a component that loads on demand with React.lazy. fsr still renders it on the server, so the first paint has the finished chart.
Before you start
Install from crates.io. You need no Node and no package manager.
cargo install snapfire_compiler
cargo install snapfire_fsr_cli
fsr --version
Every command and screenshot on this page was captured with fsr 0.x.
Two things called an async component#
Next uses "async component" for a component that awaits while it renders. React and Vue use it for a component whose code loads on demand, lazy() and defineAsyncComponent(). fsr refuses the first and supports the second. This tutorial does both, in that order.
The chart#
A bar chart with one button that sorts it, in app/src/ui/Bars.tsx:
;
;
A chart section that loads Bars on demand, in app/src/ui/Chart.tsx:
;
;
;
There's no <Suspense> around <Bars>. In a browser-only React app you'd need one, with a fallback to show while the module downloads. You'll see below why fsr doesn't.
The data comes from an async function, standing in for a database or an API, in app/src/sales.ts:
;
Some styling, at the end of app/styles/app.css:
}
}
}The page the Next way#
This is how you'd write it in the App Router: the page awaits its own data.
;
;
There's no "use client" anywhere. Bars has a button with state, so it has to run in the browser, but the build works that out from the component itself.
The build stops at the page rather than sending it to the browser. fsr dev waits for you to fix it.
Move the await#
Do what it says. The await goes into a loader beside the page:
;
;
The page takes the data as props and stops being async:
;
;
ReportProps is generated from what the loader returns, so the page is typed against it without you writing the type. Save both files and the server starts.
The split isn't style. Every loader under a route starts before anything renders, so a layout's data and a page's data load side by side. An await inside a render runs in tree order instead, each level waiting for its parent. That's the waterfall Next apps fix with Promise.all and preloading. A render that only reads its props can also be cached by what it read. A render that fetches mid-way can't.
The lazy part renders on the server#
Open http://127.0.0.1:3000/report and view the page source. The bars are there before any script runs:
fsr followed import("@src/ui/Bars") at build time and lowered Bars like any other component. So the server rendered it, lazy() or not. The first paint has the finished chart. Nothing needs a fallback, because there's never a moment with nothing to show.
The module still loads on demand. Open the network panel and load /: Bars.js isn't requested. Load /report and it arrives once the page is up, when Bars mounts over the markup it already has. Chart.js never arrives at all: Chart only renders a heading and places Bars, so the build kept it on the server and cut the island at Bars, the part with state. Click "Sort by total" and South jumps to the top.
Try it#
Make Chart async instead of the page: export default async function Chart with an await salesByRegion() inside. It's refused the same way, at the line of the async. Any component that awaits while it renders is refused, not just pages.
Then make the whole chart one island instead of letting the build pick. Wrap it in the page:
;
;
;
Reload /report with the network panel open. Chart.js now arrives as well as Bars.js, since the heading is part of the island too. The page looks the same; only what the browser downloads changed. Take the Island back out.