From the beginning, Remix's opinion has always been that you own your server architecture. This is why Remix is built on top of the Web Fetch API and can run on any modern runtime via built-in or community-provided adapters. While we believe that having a server provides the best UX/Performance/SEO/etc. for most apps, it is also undeniable that there exist plenty of valid use cases for a Single Page Application in the real world:
That's why we added support for SPA Mode in 2.5.0 (RFC), which builds heavily on top of the Client Data APIs.
SPA Mode is basically what you'd get if you had your own React Router + Vite setup using createBrowserRouter
/RouterProvider
, but along with some extra Remix goodies:
routes()
)route.lazy
<Link prefetch>
support to eagerly prefetch route modules<head>
management via Remix <Meta>
/<Links>
APIs if you choose to hydrate the full document
SPA Mode tells Remix that you do not plan on running a Remix server at runtime and that you wish to generate a static index.html
file at build time and you will only use Client Data APIs for data loading and mutations.
The index.html
is generated from the HydrateFallback
component in your root.tsx
route. The initial "render" to generate the index.html
will not include any routes deeper than root. This ensures that the index.html
file can be served/hydrated for paths beyond /
(i.e., /about
) if you configure your CDN/server to do so.
You can get started quickly using the SPA Mode template in the repo:
npx create-remix@latest --template remix-run/remix/templates/spa
Or, you can manually opt-into SPA mode in your Remix+Vite app by setting unstable_ssr: false
in your Remix Vite plugin config:
// vite.config.ts
import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
remix({
unstable_ssr: false,
}),
],
});
In SPA Mode, you develop the same way you would for a traditional Remix SSR app, and you actually use a running Remix dev server in order to enable HMR/HDR:
remix vite:dev
When you build your app in SPA Mode, Remix will call the server handler for the /
route and save the rendered HTML in an index.html
file alongside your client side assets (by default build/client/index.html
).
remix vite:build
To run your SPA, you serve your client assets directory via any HTTP server you wish, for example:
npx http-server build/client/
Or, if you are serving via an express
server (although at that point you may want to consider just running Remix in SSR mode 😉):
app.use("/assets", express.static("build/client/assets"));
app.get("*", (req, res, next) =>
res.sendFile(
path.join(process.cwd(), "build/client/index.html"),
next
)
);
SPA Mode only works when using Vite and the Remix Vite plugin
You cannot use server APIs such as headers
, loader
, and action
-- the build will throw an error if you export them
You can only export a HydrateFallback
from your root.tsx
in SPA Mode -- the build will throw an error if you export one from any other routes.
You cannot call serverLoader
/serverAction
from your clientLoader
/clientAction
methods since there is no running server -- those will throw a runtime error if called
We also expect SPA Mode to be useful in helping folks migrate existing React router apps over to Remix apps (SPA or not!).
The first step towards this migration is getting your current React Router app running on vite
, so that you've got whatever plugins you need for your non-JS code (i.e., CSS, SVG, etc.).
If you are currently using BrowserRouter
Once you're using vite, you should be able to drop your BrowserRouter
app into a catch-all Remix route per the steps in the this guide.
If you are currently using RouterProvider
If you are currently using RouterProvider
, then the best approach is to move your routes to individual files and load them via route.lazy
:
Component
export (for RR) and also a default
export (for eventual use by Remix)Once you've got all your routes living in their own files, you can:
app/
directoryloader
/action
function to clientLoader
/clientAction
index.html
file with an app/root.tsx
route that exports a default
component and HydrateFallback