Vite is a powerful, performant and extensible development environment for JavaScript projects. In order to improve and extend Remix's bundling capabilities, we now support Vite as an alternative compiler. In the future, Vite will become the default compiler for Remix.
We've got a few different Vite-based templates to get you started.
# Minimal server:
npx create-remix@latest --template remix-run/remix/templates/vite
# Express:
npx create-remix@latest --template remix-run/remix/templates/vite-express
# Cloudflare:
npx create-remix@latest --template remix-run/remix/templates/vite-cloudflare
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 accepts options directly.
For example, to configure ignoredRouteFiles
:
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
remix({
ignoredRouteFiles: ["**/*.css"],
}),
],
});
All other bundling-related options are now configured with Vite. This means you have much greater control over the bundling process.
The following subset of Remix config options are supported:
The Vite plugin also accepts the following additional options:
The path to the build directory, relative to the project root. Defaults to
"build"
.
An optional basename for your route paths, passed through to the React Router "basename" option. Please note that this is different from your asset paths. You can can configure the base public path for your assets via the Vite "base" option.
A function that is called after the full Remix build is complete.
Whether to write a .remix/manifest.json
file to the build directory. Defaults
to false
.
An array of presets to ease integration with other tools and hosting providers.
The name of the server file generated in the server build directory. Defaults to "index.js"
.
A function for assigning addressable routes to server bundles.
You may also want to enable the manifest
option since, when server bundles are enabled, it contains mappings between routes and server bundles.
To get started with Cloudflare, you can use the vite-cloudflare
template:
npx create-remix@latest --template remix-run/remix/templates/vite-cloudflare
There are two ways to run your Cloudflare app locally:
# Vite
remix vite:dev
# Wrangler
remix vite:build # build app before running wrangler
wrangler pages dev ./build/client
While Vite provides a better development experience, Wrangler provides closer emulation of the Cloudflare environment by running your server code in Cloudflare's workerd
runtime instead of Node.
To simulate the Cloudflare environment in Vite, Wrangler provides Node proxies to local workerd
bindings.
Remix's Cloudflare Proxy plugin sets up these proxies for you:
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [remixCloudflareDevProxy(), remix()],
});
The proxies are then available within context.cloudflare
in your loader
or action
functions:
export const loader = ({ context }: LoaderFunctionArgs) => {
const { env, cf, ctx } = context.cloudflare;
// ... more loader code here...
};
Check out Cloudflare's getPlatformProxy
docs for more information on each of these proxies.
To configure bindings for Cloudflare resources:
Whenever you change your wrangler.toml
file, you'll need to run wrangler types
to regenerate your bindings.
Then, you can access your bindings via context.cloudflare.env
.
For example, with a KV namespace bound as MY_KV
:
export async function loader({
context,
}: LoaderFunctionArgs) {
const { MY_KV } = context.cloudflare.env;
const value = await MY_KV.get("my-key");
return json({ value });
}
If you'd like to add additional properties to the load context,
you should export a getLoadContext
function from a shared module so that load context in Vite, Wrangler, and Cloudflare Pages are all augmented in the same way:
import { type AppLoadContext } from "@remix-run/cloudflare";
import { type PlatformProxy } from "wrangler";
type Cloudflare = Omit<PlatformProxy<Env>, "dispose">;
declare module "@remix-run/cloudflare" {
interface AppLoadContext {
cloudflare: Cloudflare;
extra: string; // augmented
}
}
type GetLoadContext = (args: {
request: Request;
context: { cloudflare: Cloudflare }; // load context _before_ augmentation
}) => AppLoadContext;
// Shared implementation compatible with Vite, Wrangler, and Cloudflare Pages
export const getLoadContext: GetLoadContext = ({
context,
}) => {
return {
...context,
extra: "stuff",
};
};
getLoadContext
to both the Cloudflare Proxy plugin and the request handler in functions/[[path]].ts
, otherwise you'll get inconsistent load context augmentation depending on how you run your app.
First, pass in getLoadContext
to the Cloudflare Proxy plugin in your Vite config to augment load context when running Vite:
import {
vitePlugin as remix,
cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { getLoadContext } from "./load-context";
export default defineConfig({
plugins: [
remixCloudflareDevProxy({ getLoadContext }),
remix(),
],
});
Next, pass in getLoadContext
to the request handler in your functions/[[path]].ts
file to augment load context when running Wrangler or when deploying to Cloudflare Pages:
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
import { getLoadContext } from "../load-context";
export const onRequest = createPagesFunctionHandler({
build,
getLoadContext,
});
Remix lets you write code that runs on both the client and the server. Out-of-the-box, Vite doesn't support mixing server-only code with client-safe code in the same module. Remix is able to make an exception for routes because we know which exports are server-only and can remove them from the client.
There are a few ways to isolate server-only code in Remix.
The simplest approach is to use .server
modules.
.server
modulesWhile not strictly necessary, .server
modules are a good way to explicitly mark entire modules as server-only.
The build will fail if any code in a .server
file or .server
directory accidentally ends up in the client module graph.
app
βββ .server π marks all files in this directory as server-only
β βββ auth.ts
β βββ db.ts
βββ cms.server.ts π marks this file as server-only
βββ root.tsx
βββ routes
βββ _index.tsx
.server
modules must be within your Remix app directory.
If you want to mix server-only code and client-safe code in the same module, you
can use undefined
in the client.
For example, once you've added the plugin to your Vite config, you can wrap any server-only exports with serverOnly$
:
import { serverOnly$ } from "vite-env-only";
import { db } from "~/.server/db";
export const getPosts = serverOnly$(async () => {
return db.posts.findMany();
});
export const PostPreview = ({ title, description }) => {
return (
<article>
<h2>{title}</h2>
<p>{description}</p>
</article>
);
};
This example would be compiled into the following code for the client:
export const getPosts = undefined;
export const PostPreview = ({ title, description }) => {
return (
<article>
<h2>{title}</h2>
<p>{description}</p>
</article>
);
};
There is a notable difference with the way Vite manages the public
directory compared to the existing Remix compiler. Vite copies files from the public
directory into the client build directory, whereas the Remix compiler left the public
directory untouched and used a subdirectory (public/build
) as the client build directory.
In order to align the default Remix project structure with the way Vite works, the build output paths have been changed. There is now a single buildDirectory
option that defaults to "build"
, replacing the separate assetsBuildDirectory
and serverBuildDirectory
options. This means that, by default, the server is now compiled into build/server
and the client is now compiled into build/client
.
This also means that the following configuration defaults have been changed:
"/"
rather than "/build/"
.serverBuildFile
which defaults to "index.js"
. This file will be written into the server directory within your configured buildDirectory
.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 { 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: ["**/*.css"],
}),
],
});
Vite provides a robust client-side runtime for development features like HMR,
making the <LiveReload />
component obsolete. When using the Remix Vite plugin in development,
the <Scripts />
component will automatically include Vite's client-side runtime and other dev-only scripts.
π Remove <LiveReload/>
, keep <Scripts />
import {
- LiveReload,
Outlet,
Scripts,
}
export default function App() {
return (
<html>
<head>
</head>
<body>
<Outlet />
- <LiveReload />
<Scripts />
</body>
</html>
)
}
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
.
Since the module types provided by vite/client
are not compatible with the module types implicitly included with @remix-run/dev
, you'll also need to enable the skipLibCheck
flag in your TypeScript config. Remix won't require this flag in the future once the Vite plugin is the default compiler.
π Rename remix.env.d.ts
to env.d.ts
-/remix.env.d.ts
+/env.d.ts
π Replace @remix-run/dev
types with vite/client
in env.d.ts
-/// <reference types="@remix-run/dev" />
+/// <reference types="vite/client" />
/// <reference types="@remix-run/node" />
π Replace reference to remix.env.d.ts
with env.d.ts
in tsconfig.json
- "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx"],
π Ensure skipLibCheck
is enabled in tsconfig.json
"skipLibCheck": true,
π Ensure module
and moduleResolution
fields are set correctly in tsconfig.json
"module": "ESNext",
"moduleResolution": "Bundler",
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 remix vite:dev
.
The Remix Vite plugin doesn't install any global Node polyfills so you'll need to install them yourself if you were relying on remix-serve
to provide them. The easiest way to do this is by calling installGlobals
at the top of your Vite config.
The Vite dev server's default port is different to remix-serve
so you'll need to configure this via Vite's server.port
option if you'd like to maintain the same port.
You'll also need to update to the new build output paths, which are build/server
for the server and build/client
for client assets.
π Update your dev
, build
and start
scripts
{
"scripts": {
"dev": "remix vite:dev",
"build": "remix vite:build",
"start": "remix-serve ./build/server/index.js"
}
}
π Install global Node polyfills in your Vite config
import { vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
import { defineConfig } from "vite";
+installGlobals();
export default defineConfig({
plugins: [remix()],
});
π Configure your Vite dev server port (optional)
export default defineConfig({
server: {
port: 3000,
},
plugins: [remix()],
});
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.
You can then load the virtual module named "virtual:remix/server-build"
during development to create a Vite-based request handler.
You'll also need to update your server code to reference the new build output paths, which are build/server
for the server build and build/client
for client assets.
For example, if you were using Express, here's how you could do it.
π Update your server.mjs
file
import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import express from "express";
installGlobals();
const viteDevServer =
process.env.NODE_ENV === "production"
? undefined
: await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
})
);
const app = express();
// handle asset requests
if (viteDevServer) {
app.use(viteDevServer.middlewares);
} else {
app.use(
"/assets",
express.static("build/client/assets", {
immutable: true,
maxAge: "1y",
})
);
}
app.use(express.static("build/client", { maxAge: "1h" }));
// handle SSR requests
app.all(
"*",
createRequestHandler({
build: viteDevServer
? () =>
viteDevServer.ssrLoadModule(
"virtual:remix/server-build"
)
: await import("./build/server/index.js"),
})
);
const port = 3000;
app.listen(port, () =>
console.log("http://localhost:" + port)
);
π Update your build
, dev
, and start
scripts
{
"scripts": {
"dev": "node ./server.mjs",
"build": "remix vite:build",
"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.ts
node --loader tsm ./server.ts
Just remember that there might be some noticeable slowdown for initial server startup if you do this.
The Remix Vite plugin only officially supports Cloudflare Pages which is specifically designed for fullstack applications, unlike Cloudflare Workers Sites. If you're currently on Cloudflare Workers Sites, refer to the Cloudflare Pages migration guide.
π Setup the Cloudflare Remix preset, then add "workerd"
and "worker"
conditions
import {
vitePlugin as remix,
cloudflarePreset as cloudflare,
} from "@remix-run/dev";
import { defineConfig } from "vite";
import { getBindingsProxy } from "wrangler";
export default defineConfig({
plugins: [
remix({
presets: [cloudflare(getBindingsProxy)],
}),
],
ssr: {
resolve: {
externalConditions: ["workerd", "worker"],
},
},
});
Your Cloudflare app may be setting the the Remix Config server
field to generate a catch-all Cloudflare Function.
With Vite, this indirection is no longer necessary.
Instead, you can author a catch-all route directly for Cloudflare, just like how you would for Express or any other custom servers.
π Create a catch-all route for Remix
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
// @ts-ignore - the server build file is generated by `remix vite:build`
import * as build from "../build/server";
export const onRequest = createPagesFunctionHandler({
build,
});
While you'll mostly use Vite during development, you can also use Wrangler to preview and deploy your app. To learn more, see Cloudflare > Vite & Wrangler.
π Update your package.json
scripts
{
"scripts": {
"dev": "remix vite:dev",
"build": "remix vite:build",
"preview": "wrangler pages dev ./build/client",
"deploy": "wrangler pages deploy ./build/client"
}
}
When using the existing Remix compiler's default options, the server was compiled into build
and the client was compiled into public/build
. Due to differences with the way Vite typically works with its public
directory compared to the existing Remix compiler, these output paths have changed.
π Update references to build output paths
build/server
by default.build/client
by default.For example, to update the Dockerfile from the Blues Stack:
-COPY --from=build /myapp/build /myapp/build
-COPY --from=build /myapp/public /myapp/public
+COPY --from=build /myapp/build/server /myapp/build/server
+COPY --from=build /myapp/build/client /myapp/build/client
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 { 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.
The @remix-run/css-bundle
cssBundleHref
export will always be undefined
.
π Uninstall @remix-run/css-bundle
npm uninstall @remix-run/css-bundle
π Remove references to cssBundleHref
- 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 }]
- : []),
// ...
];
If a route's links
function is only used to wire up cssBundleHref
, you can remove it 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 }]
- : []),
- ];
links
If you are referencing CSS in a links
function, you'll need to update the corresponding CSS imports to use Vite's explicit ?url
import syntax.
π Add ?url
to CSS imports used in links
.css?url
imports require Vite v5.1 or newer
-import styles from "~/styles/dashboard.css";
+import styles from "~/styles/dashboard.css?url";
export const links = () => {
return [
{ rel: "stylesheet", href: styles }
];
}
If your project is using Tailwind CSS, 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
config option was enabled.
π Add the tailwindcss
plugin to your PostCSS config if it's missing
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
π Migrate Tailwind CSS import
If you're referencing your Tailwind CSS file in a links
function, you'll need to migrate your Tailwind CSS import statement.
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 { 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
The Remix plugin expects to process JavaScript or TypeScript files, so any transpilation from other languages β like MDX β must be done first. In this case, that means putting the MDX plugin before the Remix plugin.
π Add the MDX Rollup plugin to your Vite config
import mdx from "@mdx-js/rollup";
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [mdx(), remix()],
});
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 { 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: [
mdx({
remarkPlugins: [
remarkFrontmatter,
remarkMdxFrontmatter,
],
}),
remix(),
],
});
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,
});
You can use the NODE_OPTIONS
environment variable to start a debugging session:
NODE_OPTIONS="--inspect-brk" npm run dev
Then you can attach a debugger from your browser.
For example, in Chrome you can open up chrome://inspect
or click the NodeJS icon in the dev tools to attach the debugger.
vite-plugin-inspect
shows you each how each Vite plugin transforms your code and how long each plugin takes.
Remix includes a --profile
flag for performance profiling.
remix vite:build --profile
When running with --profile
, a .cpuprofile
file will be generated that can be shared or upload to speedscope.app to for analysis.
You can also profile in dev by pressing p + enter
while the dev server is running to start a new profiling session or stop the current session.
If you need to profile dev server startup, you can also use the --profile
flag to initialize a profiling session on startup:
remix vite:dev --profile
Remember that you can always check the Vite performance docs for more tips!
To visualize and analyze your bundle, you can use the rollup-plugin-visualizer plugin:
import { vitePlugin as remix } from "@remix-run/dev";
import { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
remix(),
// `emitFile` is necessary since Remix builds more than one bundle!
visualizer({ emitFile: true }),
],
});
Then when you run remix vite:build
, it'll generate a stats.html
file in each of your bundles:
build
βββ client
β βββ assets/
β βββ favicon.ico
β βββ stats.html π
βββ server
βββ index.js
βββ stats.html π
Open up stats.html
in your browser to analyze your bundle.
Check the debugging and performance sections for general troubleshooting tips. Also, see if anyone else is having a similar problem by looking through the known issues with the remix vite plugin on github.
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.
Vite supports both ESM and CJS dependencies, but sometimes you might still run into issues with ESM / CJS interop. Usually, this is because a dependency is not properly configured to support ESM. And we don't blame them, its really tricky to support both ESM and CJS properly.
For a walkthrough of fixing an example bug, check out π₯ How to Fix CJS/ESM Bugs in Remix.
To diagnose if one of your dependencies is misconfigured, check publint or Are The Types Wrong.
Additionally, you can use the vite-plugin-cjs-interop plugin smooth over issues with default
exports for external CJS dependencies.
Finally, you can also explicitly configure which dependencies to bundle into your server bundled
with Vite's ssr.noExternal
option to emulate the Remix compiler's serverDependenciesToBundle
with the Remix Vite plugin.
If you see errors in the browser console during development that point to server code, you likely need to explicitly isolate server-only code. For example, if you see something like:
Uncaught ReferenceError: process is not defined
Then you'll need to track down which module is pulling in dependencies that except server-only globals like process
and isolate code either in a separate .server
module or with vite-env-only
.
Since Vite uses Rollup to treeshake your code in production, these errors only occur in development.
The Remix Vite plugin is only intended for use in your application's development server and production builds. While there are other Vite-based tools such as Vitest and Storybook that make use of the Vite config file, the Remix Vite plugin has not been designed for use with these tools. We currently recommend excluding the plugin when used with other Vite-based tools.
For Vitest:
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, loadEnv } from "vite";
export default defineConfig({
plugins: [!process.env.VITEST && remix()],
test: {
environment: "happy-dom",
// Additionally, this is to load ".env.test" during vitest
env: loadEnv("test", process.cwd(), ""),
},
});
For Storybook:
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
const isStorybook = process.argv[1]?.includes("storybook");
export default defineConfig({
plugins: [!isStorybook && remix()],
});
Alternatively, you can use separate Vite config files for each tool. For example, to use a Vite config specifically scoped to Remix:
remix vite:dev --config vite.config.remix.ts
When React is used to render the entire document (as Remix does) you can run into issues when elements are dynamically injected into the head
element. If the document is re-mounted, the existing head
element is removed and replaced with an entirely new one, removing any style
elements that Vite injects during development.
This is a known React issue that is fixed in their canary release channel. If you understand the risks involved, you can pin your app to a specific React version and then use package overrides to ensure this is the only version of React used throughout your project. For example:
{
"dependencies": {
"react": "18.3.0-canary-...",
"react-dom": "18.3.0-canary-..."
},
"overrides": {
"react": "18.3.0-canary-...",
"react-dom": "18.3.0-canary-..."
}
}
It's worth stressing that this issue with styles that were injected by Vite only happens in development. Production builds won't have this issue since static CSS files are generated.
In Remix, this issue can surface when rendering alternates between your root route's default component export and its ErrorBoundary and/or HydrateFallback exports since this results in a new document-level component being mounted.
It can also happen due to hydration errors since it causes React to re-render the entire page from scratch. Hydration errors can be caused by your app code, but they can also be caused by browser extensions that manipulate the document.
This is relevant for Vite becauseβduring developmentβVite transforms CSS imports into JS files that inject their styles into the document as a side-effect. Vite does this to support lazy-loading and HMR of static CSS files.
For example, let's assume your app has the following CSS file:
* { margin: 0 }
During development, this CSS file will be transformed into the following JavaScript code when imported as a side effect:
import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/app/styles.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client";
const __vite__id = "/path/to/app/styles.css";
const __vite__css = "*{margin:0}"
__vite__updateStyle(__vite__id, __vite__css);
import.meta.hot.accept();
import.meta.hot.prune(()=>__vite__removeStyle(__vite__id));
This transformation is not applied to production code, which is why this styling issue only affects development.
When using Cloudflare Pages, you may encounter the following error from wrangler pages dev
:
ERROR: Your worker called response.clone(), but did not read the body of both clones.
This is wasteful, as it forces the system to buffer the entire response body
in memory, rather than streaming it through. This may cause your worker to be
unexpectedly terminated for going over the memory limit. If you only meant to
copy the response headers and metadata (e.g. in order to be able to modify
them), use `new Response(response.body, response)` instead.
This is a known issue with Wrangler.
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!