Viewing docs for an older release. View latest
Vite (Unstable)
On this page

Vite (Unstable)

Vite support is currently unstable and only intended to gather early feedback. We don't yet recommend using this in production.

Vite is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we're currently exploring the use of Vite as an alternative compiler to esbuild.

Legend: βœ… (Tested),❓ (Untested), ⏳ (Not Yet Supported)

Feature Node Deno Cloudflare Notes
Built-in dev server βœ… ❓ ⏳
Other servers (e.g. Express) βœ… ❓ ⏳
HMR βœ… ❓ ⏳
HDR βœ… ❓ ⏳
MDX routes βœ… ❓ ⏳ Supported with some deprecations.

Getting started

To get started with a minimal server, you can use the unstable-vite template:

npx create-remix@nightly --template remix-run/remix/templates/unstable-vite

If you'd rather customize your server, you can use the unstable-vite-express template:

npx create-remix@nightly --template remix-run/remix/templates/unstable-vite-express

These templates include a vite.config.ts file which is where the Remix Vite plugin is configured.

Configuration

The Vite plugin does not use remix.config.js. Instead, the plugin directly accepts the following subset of Remix config options:

For example, to configure ignoredRouteFiles:

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/.*"],
    }),
  ],
});

All other bundling-related options are now configured with Vite. This means you have much greater control over the bundling process.

Additional features & plugins

One of the reasons that Remix is moving to Vite is so you have less to learn when adopting Remix. This means that, for any additional bundling features you'd like to use, you should reference Vite documentation and the Vite plugin community rather than the Remix documentation.

Vite has many features and plugins that are not built into the existing Remix compiler. The use of any such features will render the existing Remix compiler unable to compile your app, so only use them if you intend to use Vite exclusively from here on out.

Migrating

Setup Vite

πŸ‘‰ Install Vite as a development dependency

npm install -D vite

Remix is now just a Vite plugin, so you'll need to hook it up to Vite.

πŸ‘‰ Replace remix.config.js with vite.config.ts at the root of your Remix app

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

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

The subset of supported Remix config options should be passed directly to the plugin:

export default defineConfig({
  plugins: [
    remix({
      ignoredRouteFiles: ["**/.*"],
    }),
  ],
});

TypeScript integration

Vite handles imports for all sorts of different file types, sometimes in ways that differ from the existing Remix compiler, so let's reference Vite's types from vite/client instead of the obsolete types from @remix-run/dev.

πŸ‘‰ Replace your remix.env.d.ts with a new env.d.ts file

/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />

πŸ‘‰ Replace reference to remix.env.d.ts in tsconfig.json

- "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],

LiveReload before Scripts

This is a temporary workaround for a limitation that will be removed in the future.

For React Fast Refresh to work, it needs to be initialized before any app code is run. That means it needs to come before your <Scripts /> element that loads your app code.

We're working on a better API that would eliminate issues with ordering scripts. But for now, you can work around this limitation by manually moving <LiveReload /> before <Scripts />. If your app doesn't the Scripts component, you can safely ignore this step.

πŸ‘‰ Ensure <LiveReload /> comes before <Scripts />

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
+        <LiveReload />
        <Scripts />
-        <LiveReload />
      </body>
    </html>
  );
}

Migrating from Remix App Server

If you were using remix-serve in development (or remix dev without the -c flag), you'll need to switch to the new minimal dev server. It comes built-in with the Remix Vite plugin and will take over when you run vite dev.

πŸ‘‰ Update your dev and build scripts

{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build && vite build --ssr",
    "start": "remix-serve ./build/index.js"
  }
}

Migrating from a custom server

If you were using a custom server in development, you'll need to edit your custom server to use Vite's connect middleware. This will delegate asset requests and initial render requests to Vite during development, letting you benefit from Vite's excellent DX even with a custom server.

Remix exposes APIs for exactly this purpose:

import {
  unstable_createViteServer, // provides middleware for handling asset requests
  unstable_loadViteServerBuild, // handles initial render requests
} from "@remix-run/dev";

For example, if you were using Express, here's how you could do it.

πŸ‘‰ Update your server.mjs file

import {
  unstable_createViteServer,
  unstable_loadViteServerBuild,
} from "@remix-run/dev";
import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";

installGlobals();

const vite =
  process.env.NODE_ENV === "production"
    ? undefined
    : await unstable_createViteServer();

const app = express();

// handle asset requests
if (vite) {
  app.use(vite.middlewares);
} else {
  app.use(
    "/build",
    express.static("public/build", {
      immutable: true,
      maxAge: "1y",
    })
  );
}
app.use(express.static("public", { maxAge: "1h" }));

// handle SSR requests
app.all(
  "*",
  createRequestHandler({
    build: vite
      ? () => unstable_loadViteServerBuild(vite)
      : await import("./build/index.js"),
  })
);

const port = 3000;
app.listen(port, () =>
  console.log("http://localhost:" + port)
);

πŸ‘‰ Update your dev, build, and start scripts

{
  "scripts": {
    "dev": "node ./server.mjs",
    "build": "vite build && vite build --ssr",
    "start": "cross-env NODE_ENV=production node ./server.mjs"
  }
}

If you prefer, you can instead author your custom server in TypeScript. You could then use tools like tsx or tsm to run your custom server:

tsx ./server.tsx
node --loader tsm ./server.ts

Just remember that there might be some noticeable slowdown for initial server startup if you do this.

Configure path aliases

The Remix compiler leverages the paths option in your tsconfig.json to resolve path aliases. This is commonly used in the Remix community to define ~ as an alias for the app directory.

Vite does not provide any path aliases by default. If you were relying on this feature, you can install the vite-tsconfig-paths plugin to automatically resolve path aliases from your tsconfig.json in Vite, matching the behavior of the Remix compiler:

πŸ‘‰ Install vite-tsconfig-paths

npm install -D vite-tsconfig-paths

πŸ‘‰ Add vite-tsconfig-paths to your Vite config

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

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

Optionally remove @remix-run/css-bundle

Vite has built-in support for CSS side-effect imports, PostCSS and CSS Modules, among other CSS bundling features.

The Remix Vite plugin automatically attaches bundled CSS to the relevant routes so the @remix-run/css-bundle package can be removed if you only intend to use Vite in your project.

πŸ‘‰ Remove references to @remix-run/css-bundle

-import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

export const links: LinksFunction = () => [
-  ...(cssBundleHref
-    ? [{ rel: "stylesheet", href: cssBundleHref }]
-    : []),
  // ...
];

Of course, if this is the only style sheet for a given route, you can remove the links function entirely.

-import { cssBundleHref } from "@remix-run/css-bundle";
-import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

-export const links: LinksFunction = () => [
-  ...(cssBundleHref
-    ? [{ rel: "stylesheet", href: cssBundleHref }]
-    : []),
-];

Fix up CSS imports

In Vite, CSS files are typically imported as side effects.

During development, Vite injects imported CSS files into the page via JavaScript, and the Remix Vite plugin will inline imported CSS alongside your link tags to avoid a flash of unstyled content. In the production build, the Remix Vite plugin will automatically attach CSS files to the relevant routes.

This also means that in many cases you won't need the links function export anymore.

πŸ‘‰ Convert CSS imports to side effects

// No need to export a links function anymore:
-import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

-import dashboardStyles from "./dashboard.css?url";

-export const links: LinksFunction = () => [
-  { rel: "stylesheet", href: dashboardStyles },
-];

// Just import the CSS as a side effect:
+import "./dashboard.css";

While Vite supports importing static asset URLs via an explicit ?url query string, which in theory would match the behavior of the existing Remix compiler when used for CSS files, there is a known Vite issue with ?url for CSS imports. This may be fixed in the future, but in the meantime you should exclusively use side-effect imports for CSS.

Enable Tailwind via PostCSS

If your project is using Tailwind, you'll first need to ensure that you have a PostCSS config file which will get automatically picked up by Vite. This is because the Remix compiler didn't require a PostCSS config file when Remix's tailwind option was enabled.

πŸ‘‰ Add PostCSS config if it's missing, including the tailwindcss plugin

export default {
  plugins: {
    tailwindcss: {},
  },
};

If your project already has a PostCSS config file, you'll need to add the tailwindcss plugin if it's not already present. This is because the Remix compiler included this plugin automatically when Remix's tailwind option was enabled.

πŸ‘‰ Add the tailwindcss plugin to your PostCSS config if it's missing

export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

πŸ‘‰ Convert Tailwind CSS import to a side effect

If you haven't already, be sure to convert your CSS imports to side effects.

// Don't export as a link descriptor:
-import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

-import tailwind from "./tailwind.css";

-export const links: LinksFunction = () => [
-  { rel: "stylesheet", href: tailwind },
-];

// Import as a side effect instead:
+import "./tailwind.css";

Add Vanilla Extract plugin

If you're using Vanilla Extract, you'll need to set up the Vite plugin.

πŸ‘‰ Install the official Vanilla Extract plugin for Vite

npm install -D @vanilla-extract/vite-plugin

πŸ‘‰ Add the Vanilla Extract plugin to your Vite config

import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import { defineConfig } from "vite";

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

Add MDX plugin

If you're using MDX, since Vite's plugin API is an extension of the Rollup plugin API, you should use the official MDX Rollup plugin:

πŸ‘‰ Install the MDX Rollup plugin

npm install -D @mdx-js/rollup

πŸ‘‰ Add the MDX Rollup plugin to your Vite config

import mdx from "@mdx-js/rollup";
import { unstable_vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remix(), mdx()],
});
Add MDX frontmatter support

The Remix compiler allowed you to define frontmatter in MDX. If you were using this feature, you can achieve this in Vite using remark-mdx-frontmatter.

πŸ‘‰ Install the required Remark frontmatter plugins

npm install -D remark-frontmatter remark-mdx-frontmatter

πŸ‘‰ Pass the Remark frontmatter plugins to the MDX Rollup plugin

import mdx from "@mdx-js/rollup";
import { unstable_vitePlugin as remix } from "@remix-run/dev";
import remarkFrontmatter from "remark-frontmatter";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix(),
    mdx({
      remarkPlugins: [
        remarkFrontmatter,
        remarkMdxFrontmatter,
      ],
    }),
  ],
});

In the Remix compiler, the frontmatter export was named attributes. This differs from the frontmatter plugin's default export name of frontmatter. Although it's possible to configure the frontmatter export name, we recommend updating your app code to use the default export name instead.

πŸ‘‰ Rename MDX attributes export to frontmatter within MDX files

---
title: Hello, World!
---

-# {attributes.title}
+# {frontmatter.title}

πŸ‘‰ Rename MDX attributes export to frontmatter for consumers

import Component, {
-  attributes,
+  frontmatter,
} from "./posts/first-post.mdx";
Define types for MDX files

πŸ‘‰ Add types for *.mdx files to env.d.ts

/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />

declare module "*.mdx" {
  let MDXComponent: (props: any) => JSX.Element;
  export const frontmatter: any;
  export default MDXComponent;
}
Map MDX frontmatter to route exports

The Remix compiler allowed you to define headers, meta and handle route exports in your frontmatter. This Remix-specific feature is obviously not supported by the remark-mdx-frontmatter plugin. If you were using this feature, you should manually map frontmatter to route exports yourself:

πŸ‘‰ Map frontmatter to route exports for MDX routes

---
meta:
  - title: My First Post
  - name: description
    content: Isn't this awesome?
headers:
  Cache-Control: no-cache
---

export const meta = frontmatter.meta;
export const headers = frontmatter.headers;

# Hello World

Note that, since you're explicitly mapping MDX route exports, you're now free to use whatever frontmatter structure you like.

---
title: My First Post
description: Isn't this awesome?
---

export const meta = () => {
  return [
    { title: frontmatter.title },
    {
      name: "description",
      content: frontmatter.description,
    },
  ];
};

# Hello World
Update MDX filename usage

The Remix compiler also provided a filename export from all MDX files. This was primarily designed to enable linking to collections of MDX routes. If you were using this feature, you can achieve this in Vite via glob imports which give you a handy data structure that maps file names to modules. This makes it much easier to maintain a list of MDX files since you no longer need to import each one manually.

For example, to import all MDX files in the posts directory:

const posts = import.meta.glob("./posts/*.mdx");

This is equivalent to writing this by hand:

const posts = {
  "./posts/a.mdx": () => import("./posts/a.mdx"),
  "./posts/b.mdx": () => import("./posts/b.mdx"),
  "./posts/c.mdx": () => import("./posts/c.mdx"),
  // etc.
};

You can also eagerly import all MDX files if you'd prefer:

const posts = import.meta.glob("./posts/*.mdx", {
  eager: true,
});

Troubleshooting

Check out the known issues with the Remix Vite plugin on GitHub before filing a new bug report!

HMR

If you are expecting hot updates but getting full page reloads, check out our discussion on Hot Module Replacement to learn more about the limitations of React Fast Refresh and workarounds for common issues.

Server code not treeshaken in development

In production, Vite treeshakes server-only code from your client bundle, just like the existing Remix compiler. However, in development, Vite lazily compiles each module on-demand and therefore does not treeshake across module boundaries.

If you run into browser errors in development that reference server-only code, be sure to place that server-only code in a .server file.

At first, this might seem like a compromise for DX when compared to the existing Remix compiler, but the mental model is simpler: .server is for server-only code, everything else could be on both the client and the server.

Acknowledgements

Vite is an amazing project and we're grateful to the Vite team for their work. Special thanks to Matias Capeletto, Arnaud BarrΓ©, and Bjorn Lu from the Vite team for their guidance.

The Remix community was quick to explore Vite support and we're grateful for their contributions:

Finally, we were inspired by how other frameworks implemented Vite support:

We're definitely late to the Vite party, but we're excited to be here now!

Docs and examples licensed under MIT