The following future flags are stable and ready to adopt. To read more about future flags see Development Strategy
First update to the latest minor version of v2.x to have the latest future flags.
👉 Update to latest v2
npm install @remix-run/{dev,react,node,etc.}@2
Background
Remix no longer uses its own, closed compiler (now referred to as the "Classic Compiler"), and instead uses Vite. Vite is a powerful, performant and extensible development environment for JavaScript projects. View the Vite docs for more information on performance, troubleshooting, etc.
While this is not a future flag, new features and some feature flags are only available in the Vite plugin, and the Classic Compiler will be removed in the next version of Remix.
👉 Install Vite
npm install -D vite
Update your Code
👉 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"],
}),
],
});
👉 Remove <LiveReload/>
, keep <Scripts />
import {
- LiveReload,
Outlet,
Scripts,
}
export default function App() {
return (
<html>
<head>
</head>
<body>
<Outlet />
- <LiveReload />
<Scripts />
</body>
</html>
)
}
👉 Update tsconfig.json
Update the types
field in tsconfig.json
and make sure skipLibCheck
, module
, and moduleResolution
are all set correctly.
{
"compilerOptions": {
"types": ["@remix-run/node", "vite/client"],
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Bundler"
}
}
👉 Update/remove remix.env.d.ts
Remove the following type declarations in remix.env.d.ts
- /// <reference types="@remix-run/dev" />
- /// <reference types="@remix-run/node" />
If remix.env.d.ts
is now empty, delete it
rm remix.env.d.ts
Configure path aliases
Vite does not provide any path aliases by default. If you were relying on this feature, such as defining ~
as an alias for the app
directory, 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()],
});
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.
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 }]
- : []),
// ...
];
Fix up CSS imports referenced in 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
-import styles from "~/styles/dashboard.css";
+import styles from "~/styles/dashboard.css?url";
export const links = () => {
return [
{ rel: "stylesheet", href: styles }
];
}
Migrate Tailwind CSS or Vanilla Extract
If you are using Tailwind CSS or Vanilla Extract, see the full migration guide.
Migrate from Remix App Server
👉 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()],
});
Migrate a custom server
If you are migrating a customer server or Cloudflare Functions, see the full migration guide.
Migrate MDX routes
If you're using MDX, you should use the official MDX Rollup plugin. See the full migration guide for a step-by-step walkthrough.
Background
The fetcher lifecycle is now based on when it returns to an idle state rather than when its owner component unmounts: View the RFC for more information.
👉 Enable the Flag
remix({
future: {
v3_fetcherPersist: true,
},
});
Update your Code
It's unlikely to affect your app. You may want to check any usage of useFetchers
as they may persist longer than they did before. Depending on what you're doing, you may render something longer than before.
Background
Changes the relative path matching and linking for multi-segment splats paths like dashboard/*
(vs. just *
). View the CHANGELOG for more information.
👉 Enable the Flag
remix({
future: {
v3_relativeSplatPath: true,
},
});
Update your Code
If you have any routes with a path + a splat like dashboard.$.tsx
or route("dashboard/*")
that have relative links like <Link to="relative">
or <Link to="../relative">
beneath it, you will need to update your code.
👉 Split the route into two
For any splat routes split it into a layout route and a child route with the splat:
└── routes
├── _index.tsx
+ ├── dashboard.tsx
└── dashboard.$.tsx
// or
routes(defineRoutes) {
return defineRoutes((route) => {
route("/", "home/route.tsx", { index: true });
- route("dashboard/*", "dashboard/route.tsx")
+ route("dashboard", "dashboard/layout.tsx", () => {
+ route("*", "dashboard/route.tsx");
});
});
},
👉 Update relative links
Update any <Link>
elements with relative links within that route tree to include the extra ..
relative segment to continue linking to the same place:
// dashboard.$.tsx or dashboard/route.tsx
function Dashboard() {
return (
<div>
<h2>Dashboard</h2>
<nav>
- <Link to="">Dashboard Home</Link>
- <Link to="team">Team</Link>
- <Link to="projects">Projects</Link>
+ <Link to="../">Dashboard Home</Link>
+ <Link to="../team">Team</Link>
+ <Link to="../projects">Projects</Link>
</nav>
</div>
);
}
Background
When a server-side request is aborted, such as when a user navigates away from a page before the loader finishes, Remix will throw the request.signal.reason
instead of an error such as new Error("query() call aborted...")
.
👉 Enable the Flag
remix({
future: {
v3_throwAbortReason: true,
},
});
Update your Code
You likely won't need to adjust any code, unless you had custom logic inside of handleError
that was matching the previous error message to differentiate it from other errors.
Background
with this flag, Remix moves to a "single fetch" approach for data requests when making SPA navigations within your app. Additional details are available in the docs, but the main reason we chose to move to this approach is Simplicity. With Single Fetch, data requests now behave just like document requests and developers no longer need to think about the nuances of how to manage headers, caching, etc., differently between the two. For more advanced use-cases, developers can still opt into fine-grained revalidations.
👉 Enable the Flag (and the types)
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
declare module "@remix-run/node" {
// or cloudflare, deno, etc.
interface Future {
v3_singleFetch: true;
}
}
export default defineConfig({
plugins: [
remix({
future: {
v3_singleFetch: true,
},
}),
tsconfigPaths(),
],
});
Update your Code
You should be able to mostly use your code as-is with the flag enabled, but the following changes should be made over time and will be required prior to the next major version.
👉 Remove json()/defer() in favor of raw objects
Single Fetch supports JSON objects and Promises out of the box, so you can return the raw data from your loader
/action
functions:
-import { json } from "@remix-run/node";
export async function loader({}: LoaderFunctionArgs) {
let tasks = await fetchTasks();
- return json(tasks);
+ return tasks;
}
-import { defer } from "@remix-run/node";
export async function loader({}: LoaderFunctionArgs) {
let lazyStuff = fetchLazyStuff();
let tasks = await fetchTasks();
- return defer({ tasks, lazyStuff });
+ return { tasks, lazyStuff };
}
If you were using the second parameter of json
/defer
to set a custom status or headers on your response, you can continue doing do via the new data
API:
-import { json } from "@remix-run/node";
+import { data } from "@remix-run/node";
export async function loader({}: LoaderFunctionArgs) {
let tasks = await fetchTasks();
- return json(tasks, {
+ return data(tasks, {
headers: {
"Cache-Control": "public, max-age=604800"
}
});
}
👉 Adjust your server abort delay
If you were using a custom ABORT_DELAY
in your entry.server.tsx
file, you should change that to use thew new streamTimeout
API leveraged by Single Fetch:
-const ABORT_DELAY = 5000;
+// Reject/cancel all pending promises after 5 seconds
+export const streamTimeout = 5000;
// ...
function handleBrowserRequest(/* ... */) {
return new Promise((resolve, reject) => {
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
- abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
/* ... */
},
onShellError(error: unknown) {
/* ... */
},
onError(error: unknown) {
/* ... */
},
}
);
- setTimeout(abort, ABORT_DELAY);
+ // Automatically timeout the React renderer after 6 seconds, which ensures
+ // React has enough time to flush down the rejected boundary contents
+ setTimeout(abort, streamTimeout + 1000);
});
}
Background
With this flag, Remix no longer sends the full route manifest up to the client on initial load. Instead, Remix only sends the server-rendered routes up in the manifest and then fetches the remaining routes as the user navigated around the application. Additional details are available in the docs and the blog post
👉 Enable the Flag
remix({
future: {
v3_lazyRouteDiscovery: true,
},
});
Update your Code
You shouldn't need to make any changes to your application code for this feature to work.
You may find some usage for the new <Link discover>
API if you wish to disable eager route discovery on certain links.
Opt into automatic dependency optimization during development. This flag will remain in an "unstable" state until React Router v7 so you do not need to adopt this in your Remix v2 app prior to upgrading to React Router v7.