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. |
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.
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.
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.
π 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: ["**/.*"],
}),
],
});
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
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>
);
}
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"
}
}
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.
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()],
});
@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
π 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 }]
- : []),
-];
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";
?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.
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";
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()],
});
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()],
});
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";
π 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;
}
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
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,
});
Check out the known issues with the Remix Vite plugin on GitHub before filing a new bug report!
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.
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.
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!