There are two ways to ship a single page app with React Router
When using React Router as a framework, you can enable "SPA Mode" by setting ssr:false
in your react-router.config.ts
file. This will disable runtime server rendering and generate an index.html
at build time that you can serve and hydrate as a SPA.
Typical Single Page apps send a mostly blank index.html
template with little more than an empty <div id="root"></div>
. In contrast, react-router build
(in SPA Mode) pre-renders your root route at build time into an index.html
file. This means you can:
<div>
loader
to load data for your application shellHydrateFallback
)It's important to note that setting ssr:false
only disables runtime server rendering. React Router will still server render your root route at build time to generate the index.html
file. This is why your project still needs a dependency on @react-router/node
and your routes need to be SSR-safe. That means you can't call window
or other browser-only APIs during the initial render, even when server rendering is disabled.
Server rendering is enabled by default. Set the ssr
flag to false
in react-router.config.ts
to disable it.
import { type Config } from "@react-router/dev/config";
export default {
ssr: false,
} satisfies Config;
With this set to false, the server build will no longer be generated.
ssr:false
only disables runtime server rendering. React Router will still server render your root route at build time to generate the index.html
file. This is why your project still needs a dependency on @react-router/node
and your routes need to be SSR-safe. That means you can't call window
or other browser-only APIs during the initial render, even when server rendering is disabled.
HydrateFallback
and optional loader
to your root routeSPA Mode will generate an index.html
file at build-time that you can serve as the entry point for your SPA. This will only render the root route so that it is capable of hydrating at runtime for any path in your application.
To provide a better loading UI than an empty <div>
, you can add a HydrateFallback
component to your root route to render your loading UI into the index.html
at build time. This way, it will be shown to users immediately while the SPA is loading/hydrating.
import LoadingScreen from "./components/loading-screen";
export function Layout() {
return <html>{/*...*/}</html>;
}
export function HydrateFallback() {
return <LoadingScreen />;
}
export default function App() {
return <Outlet />;
}
Because the root route is server-rendered at build time, you can also use a loader
in your root route if you choose. This loader
will be called at build time ans the data will be available via the optional HydrateFallback
loaderData
prop.
import { Route } from "./+types/root";
export async function loader() {
return {
version: await getVersion(),
};
}
export function HydrateFallback({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Loading version {loaderData.version}...</h1>
<AwesomeSpinner />
</div>
);
}
You cannot include a loader
in any other routes in your app when using SPA Mode unless you are pre-rendering those pages.
With server rendering disabled, you can still use clientLoader
and clientAction
to manage route data and mutations.
import { Route } from "./+types/some-route";
export async function clientLoader({
params,
}: Route.ClientLoaderArgs) {
let data = await fetch(`/some/api/stuff/${params.id}`);
return data;
}
export async function clientAction({
request,
}: Route.ClientActionArgs) {
let formData = await request.formData();
return await processPayment(formData);
}
After running react-router build
, deploy the build/client
directory to whatever static host you prefer.
Common to deploying any SPA, you'll need to configure your host to direct all URLs to the index.html
of the client build. Some hosts do this by default, but others don't. As an example, a host may support a _redirects
file to do this:
/* /index.html 200
If you're getting 404s at valid routes for your app, it's likely you need to configure your host.