🎚️ Remix Levers: SPA

Single page applications has been the status quo for building applications on Web, thanks to all the cool JS frameworks that enable building very complex applications such as linear.

Since the beginning, Remix has been pretty opinionated that you need a server and your application should be a multi-page application. This was pretty hard for some teams to adopt, having to manage yet another server.

Remix's Hybrid Model

When you enter a website's url that is build with Remix, the server first render's an HTML and sends you that, along with some cool JS scripts, check your first request in the network tab and you'll see.

While you see the page's content, Remix already has triggered some requests to download the React components (bundled versions) and recreate the React Virtual-DOM tree in a process called hydration, til now, just standard React stuff.

What is cool now, is if you click a link 1, Remix will prevent the page to be reloaded, instead it'll fetch necessary data, JavaScript and other resources for the next URL and render it with the React component for that page. This means resources don't need to be re-downloaded and no time and resources are spent on network roundtrips unless it's necessary.

Kent C. Dodds has an invaluable blog post on these architectures, he is much more qualified than me to teach you this, so check it out.

- But wait, isn't this what SPA is?

+ Yeah dude, cool isn't? But not exactly an SPA, you see, Remix has a server and many other superpowers that SPAs just doesn't have. This means Remix has the potential to get rid of the server though 😉

Reinventing wheels

When Remix first created, it used esbuild to create a compiler and gradually added support for requests from community, such as supporting different approaches for CSS like:

  • side-effect imports:
import "~/styles.css"
  • import CSS as URL


import styles from "~/styles.css"

export const links = () => [{ href: styles, rel: "stylesheet" }]

Tailwind, Postcss just to name a few, they lacked Sass support though, you had to spin your own Sass compiler, and it was a pain to say the least!

The community had gigantic amount of cool plugins to do all sorts of magic, but Remix's custom compiler was just not the kind of system to extend, it was not open to the community either! Neither they had the man-power to do that much of work, while also keeping the insane pace of delivering features, massive kudos to them.

Vite to the rescue

When Remix was created Vite was not what is now, right now it's one of the most popular ways to build web applications, especially in the React community, after Create React App project the React teams official and goto project was no longer getting the maintenance and features needed, React's new documentations Suggests using a framework like Remix or Next for new projects.

Remix team, initially Ryan Florence and Michael Jackson (not that one), had built React Router since React's blooming days. Vite+RR couldn't be more popular, so the chemistry can't be ignored, right?

Remixing Remix with Vite

Indeed not, and Remix team decided to move to Vite and make Remix a vite plugin so everyone can enjoy using Remix while not being locked out and able to use different plugins to Transform MDX files, Images, different CSS approaches, and anything else that Vite and it's plugin ecosystem makes available now and in the future.

Here is the most basic Vite config and you got yourself a Remix project

// vite.config.ts
import { defineConfig } from "vite"
import { vitePlugin as remix } from "@remix-run/dev"

export default defineConfig({
  plugins: [remix()],

Remix Levers 🎚️

If you have been following Remix's progress over the years, you'd notice Remix is giving you APIs that are similar to a gear shift, your loaders are able to block rendering until data is available, but if a route's data is too slow to fetch, you can defer it and render some feedback immediately.

You can export a clientLoader and a clientAction functions to enable very advanced data fetching and submission strategies.

You can export a shouldRevalidate function that can add optimization on top of Remix's own, so a route can only fetch fresh data when it wants.

Remix's approach is like an automatic gear shift, it does most of the awesome stuff automatically, but if you want to go manual, it doesn't get in the way.

Remix SPA: A new lever

Remix's vite plugin accepts a new optional ssr?: boolean flag, when it is true, or not provided, Remix works exactly as it did before.

The power of this lever is, when you pull it towards false, your application is no longer server rendered!

Instead, Remix will use your app/root.tsx file to generate an index.html (yes you no longer need your Vite's index.html), and your app will be working just like your RR+Vite apps, but you still can define your routes as files under app/routes folder.

This might kill the party, but feature-wise, SPA mode is not that different from a typical RR app, not because Remix doesn't provide it, but because for years, as Remix introduces new features, they'll introduce those features into React Router, then remove the Remix implementation and just use React Router under the hood. Which means most of the hot features are already present, but setting up RR and tracking routes can be a pain, especially if you're focused on delivering very fast.

Remix helps you avoid tons of boilerplate and instead focus on bringing your idea to life.

The killer advantage of Remix SPA is that, you're one true/false away from making your App to be a server-side rendered fully dynamic application or a single page application with no specific server to your frontend. This makes migration very easy.

Take advantage of the productivity and power that Remix provides:

  • Create a new RemixTM app with:

    npx create-remix@latest --template remix-run/remix/templates/vite

    That generates a project with this Vite config:

    // vite.config.ts
    import { defineConfig } from "vite"
    import { vitePlugin as remix } from "@remix-run/dev"
    export default defineConfig({
      plugins: [remix()],
  • Create a new Remix SPA app with:

    npx create-remix@latest --template remix-run/remix/templates/spa

    That generates a project with this Vite config:

    // vite.config.ts
    import { defineConfig } from "vite"
    import { vitePlugin as remix } from "@remix-run/dev"
    export default defineConfig({
    plugins: [remix({ ssr: false })],

    I bet you almost didn't spot the difference between a Fullstack project and an SPA project when you use Remix!

Checkout the Remix teams post Remix Vite Stable, Remix - SPA Mode docs and the migration guide for further details.


  1. Links that is rendered with Remix's Link or NavLink components.