Viewing docs for an older release. View latest
Preparing for v2
On this page

Preparing for v2

All v2 APIs and behaviors are available in v1 with Future Flags. They can be enabled one at a time to avoid development disruption of your project. After you have enabled all flags, upgrading to v2 should be a non-breaking upgrade.

File System Route Convention

Upgrading without changing files

You can keep using the old convention with @remix-run/v1-route-convention even after upgrading to v2 if you don't want to make the change right now (or ever, it's just a convention and you can use whatever file organization you prefer).

npm i @remix-run/v1-route-convention
const {
  createRoutesFromFolders,
} = require("@remix-run/v1-route-convention");

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    // makes the warning go away in v1.15
    v2_routeConvention: true,
  },

  routes(defineRoutes) {
    // uses the v1 convention, works in v1.15+ and v2
    return createRoutesFromFolders(defineRoutes);
  },
};

Upgrading to the new convention

  • Route nesting is now created by dots (.) in file names instead of folder nesting
  • suffixed_ underscores in segments opt-out of nesting with a potentially matching parent route instead of dots (.).
  • _prefixed underscores in segments create layout routes without a path instead of a __double underscore prefix.
  • _index.tsx files create index routes instead of index.tsx

A routes folder that looks like this in v1:

routes
├── __auth
│   ├── login.tsx
│   ├── logout.tsx
│   └── signup.tsx
├── __public
│   ├── about-us.tsx
│   ├── contact.tsx
│   └── index.tsx
├── dashboard
│   ├── calendar
│   │   ├── $day.tsx
│   │   └── index.tsx
│   ├── projects
│   │   ├── $projectId
│   │   │   ├── collaborators.tsx
│   │   │   ├── edit.tsx
│   │   │   ├── index.tsx
│   │   │   ├── settings.tsx
│   │   │   └── tasks.$taskId.tsx
│   │   ├── $projectId.tsx
│   │   └── new.tsx
│   ├── calendar.tsx
│   ├── index.tsx
│   └── projects.tsx
├── __auth.tsx
├── __public.tsx
└── dashboard.calendar.$projectId.print.tsx

Becomes this with v2_routeConvention:

routes
├── _auth.login.tsx
├── _auth.logout.tsx
├── _auth.signup.tsx
├── _auth.tsx
├── _public._index.tsx
├── _public.about-us.tsx
├── _public.contact.tsx
├── _public.tsx
├── dashboard._index.tsx
├── dashboard.calendar.$day.tsx
├── dashboard.calendar.$projectId._index.tsx
├── dashboard.calendar.$projectId.collaborators.tsx
├── dashboard.calendar.$projectId.edit.tsx
├── dashboard.calendar.$projectId.settings.tsx
├── dashboard.calendar.$projectId.tasks.$taskId.tsx
├── dashboard.calendar.$projectId.tsx
├── dashboard.calendar._index.tsx
├── dashboard.calendar.new.tsx
├── dashboard.calendar.tsx
├── dashboard.projects.tsx
└── dashboard_.calendar.$projectId.print.tsx

Note that parent routes are now grouped together instead of having dozens of routes between them (like the auth routes). Routes with the same path but not the same nesting (like dashboard and dashboard_) also group together.

With the new convention, any route can be a directory with a route.tsx file inside to define the route module. This enables co-location of modules with the route they're used in:

For example, we can move _public.tsx to _public/route.tsx and then co-locate modules the route uses:

routes
├── _auth.tsx
├── _public
│   ├── footer.tsx
│   ├── header.tsx
│   └── route.tsx
├── _public._index.tsx
├── _public.about-us.tsx
└── etc.

For more background on this change, see the Original Proposal.

Route Meta

Instead of apps returning objects and Remix automatically merging them into meta tags, you return arrays of meta descriptors and manage the merge yourself.

Updating without changing meta

You can update your meta exports with the @remix-run/v1-meta package and keep your code mostly the same as v1.

Using the metaV1 function, you can pass in the meta function's arguments and the same meta object it currently returns. This function will use the same merging logic to merge the leaf route's meta with its direct parent route meta before converting it to an array of meta descriptors usable in v2.

export function meta() {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  };
}
import { metaV1 } from "@remix-run/v1-meta";

export function meta({ matches }) {
  return metaV1(matches, {
    title: "...",
    description: "...",
    "og:title": "...",
  });
}

It's important to note that this function will not merge meta across the entire heirarchy by default. This is because you may have some routes that return an array of objects directly without the metaV1 function and this could result in unpredictable behavior. If you want to merge meta across the entire heirarchy, use the metaV1 function for all of your route's meta exports.

Updating to the new meta

export function meta() {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  };
}
export function meta() {
  return [
    { title: "..." },
    { name: "description", content: "..." },
    { property: "og:title", content: "..." },

    // you can now add SEO related <links>
    { tagName: "link", rel: "canonical", href: "..." },

    // and <script type=ld+json>
    {
      "script:ld+json": {
        some: "value",
      },
    },
  ];
}

Note that in v1 the objects returned from nested routes were all merged, you will need to manage the merge yourself now with matches:

export function meta({ matches }) {
  let rootMeta = matches[0].meta;
  let title = rootMeta.find((m) => m.title);

  return [
    title,
    { name: "description", content: "..." },
    { property: "og:title", content: "..." },

    // you can now add SEO related <links>
    { tagName: "link", rel: "canonical", href: "..." },

    // and <script type=ld+json>
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Organization",
        name: "Remix",
      },
    },
  ];
}

The meta v2 docs have more tips on managing the merge.

CatchBoundary and ErrorBoundary

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    v2_errorBoundary: true,
  },
};

In v1, a thrown Response will render the closest CatchBoundary while all other unhandled exceptions render the ErrorBoundary. In v2 there is no CatchBoundary and all unhandled exceptions will render the ErrorBoundary, response or otherwise.

Additionally, the error is no longer passed to ErrorBoundary as props but is accessed with the useRouteError hook.

import { useCatch } from "@remix-run/react";

export function CatchBoundary() {
  const caught = useCatch();

  return (
    <div>
      <h1>Oops</h1>
      <p>Status: {caught.status}</p>
      <p>{caught.data.message}</p>
    </div>
  );
}

export function ErrorBoundary({ error }) {
  console.error(error);
  return (
    <div>
      <h1>Uh oh ...</h1>
      <p>Something went wrong</p>
      <pre>{error.message || "Unknown error"}</pre>
    </div>
  );
}

Becomes:

import {
  useRouteError,
  isRouteErrorResponse,
} from "@remix-run/react";

export function ErrorBoundary() {
  const error = useRouteError();

  // when true, this is what used to go to `CatchBoundary`
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>Oops</h1>
        <p>Status: {error.status}</p>
        <p>{error.data.message}</p>
      </div>
    );
  }

  // Don't forget to typecheck with your own logic.
  // Any value can be thrown, not just errors!
  let errorMessage = "Unknown error";
  if (isDefinitelyAnError(error)) {
    errorMessage = error.message;
  }

  return (
    <div>
      <h1>Uh oh ...</h1>
      <p>Something went wrong.</p>
      <pre>{errorMessage}</pre>
    </div>
  );
}

useTransition

This hook is now called useNavigation to avoid confusion with the recent React hook by the same name. It also no longer has the type field and flattens the submission object into the navigation object itself.

import { useTransition } from "@remix-run/react";

function SomeComponent() {
  let transition = useTransition();
  transition.submission.formData;
  transition.submission.formMethod;
  transition.submission.formAction;
  transition.type;
}
import { useNavigation } from "@remix-run/react";

function SomeComponent() {
  let navigation = useNavigation();

  // transition.submission keys are flattened onto `navigation[key]`
  navigation.formData;
  navigation.formMethod;
  navigation.formAction;

  // this key is removed
  navigation.type;
}

You can derive the previous transition.type with the following examples. Keep in mind, there's probably a simpler way to get the same behavior, usually checking navigation.state, navigation.formData or the data returned from an action with useActionData can get the UX you're looking for. Feel free to ask us in Discord and we'll help you out :D

function Component() {
  let navigation = useNavigation();

  // transition.type === "actionSubmission"
  let isActionSubmission =
    navigation.state === "submitting";

  // transition.type === "actionReload"
  let isActionReload =
    navigation.state === "loading" &&
    navigation.formMethod != null &&
    navigation.formMethod != "get" &&
    // We had a submission navigation and are loading the submitted location
    navigation.formAction === navigation.pathname;

  // transition.type === "actionRedirect"
  let isActionRedirect =
    navigation.state === "loading" &&
    navigation.formMethod != null &&
    navigation.formMethod != "get" &&
    // We had a submission navigation and are now navigating to different location
    navigation.formAction !== navigation.pathname;

  // transition.type === "loaderSubmission"
  let isLoaderSubmission =
    navigation.state === "loading" &&
    navigation.state.formMethod === "get" &&
    // We had a loader submission and are navigating to the submitted location
    navigation.formAction === navigation.pathname;

  // transition.type === "loaderSubmissionRedirect"
  let isLoaderSubmissionRedirect =
    navigation.state === "loading" &&
    navigation.state.formMethod === "get" &&
    // We had a loader submission and are navigating to a new location
    navigation.formAction !== navigation.pathname;
}

useFetcher

Like useNavigation, useFetcher has flattened the submission and removed the type field.

import { useFetcher } from "@remix-run/react";

function SomeComponent() {
  let fetcher = useTransition();
  fetcher.submission.formData;
  fetcher.submission.formMethod;
  fetcher.submission.formAction;
  fetcher.type;
}
import { useNavigation } from "@remix-run/react";

function SomeComponent() {
  let fetcher = useTransition();

  // these keys are flattened
  fetcher.formData;
  fetcher.formMethod;
  fetcher.formAction;

  // this key is removed
  fetcher.type;
}

You can derive the previous fetcher.type with the following examples. Keep in mind, there's probably a simpler way to get the same behavior, usually checking fetcher.state, fetcher.formData or the data returned from an action on fetcher.data can get the UX you're looking for. Feel free to ask us in Discord and we'll help you out :D

function Component() {
  let fetcher = useFetcher();

  // fetcher.type === "done"
  let isDone =
    fetcher.state === "idle" && fetcher.data != null;

  // fetcher.type === "actionSubmission"
  let isActionSubmission = fetcher.state === "submitting";

  // fetcher.type === "actionReload"
  let isActionReload =
    fetcher.state === "loading" &&
    fetcher.formMethod != null &&
    fetcher.formMethod != "get" &&
    // If we returned data, we must be reloading
    fetcher.data != null;

  // fetcher.type === "actionRedirect"
  let isActionRedirect =
    fetcher.state === "loading" &&
    fetcher.formMethod != null &&
    navigation.formMethod != "get" &&
    // If we have no data we must have redirected
    fetcher.data == null;

  // fetcher.type === "loaderSubmission"
  let isLoaderSubmission =
    navigation.state === "loading" &&
    navigation.state.formMethod === "get";

  // fetcher.type === "normalLoad"
  let isNormalLoad =
    navigation.state === "loading" &&
    navigation.state.formMethod == null;
}

formMethod

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    v2_normalizeFormMethod: true,
  },
};

Multiple APIs return the formMethod of a submission. In v1 they return a lowercase version of the method but in v2 they return the UPPERCASE version. This is to bring it in line with HTTP and fetch specifications.

function Something() {
  let navigation = useNavigation();

  // v1
  navigation.formMethod === "post";

  // v2
  navigation.formMethod === "POST";
}

export function shouldRevalidate({ formMethod }) {
  // v1
  formMethod === "post";
  // v2
  formMethod === "POST";
}

Route links properties should all be the React camelCase values instead of HTML lowercase values. These two values snuck in as lowercase in v1. In v2 only the camelCase versions will be valid:

export const links: LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imagesrcset: "...",
      imagesizes: "...",
    },
  ];
};
export const links: V2_LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imageSrcSet: "...",
      imageSizes: "...",
    },
  ];
};

browserBuildDirectory

In your remix.config.js, rename browserBuildDirectory to assetsBuildDirectory.

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  browserBuildDirectory: "./public/build",
};
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  assetsBuildDirectory: "./public/build",
};

serverBuildDirectory

In your remix.config.js, rename serverBuildDirectory to serverBuildPath and specify a module path, not a directory.

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverBuildDirectory: "./build",
};
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverBuildDirectory: "./build/index.js",
};

Remix used to create more than a single module for the server but it now creates a single file.

serverBuildTarget

Instead of specifying a build target, use the Remix Config options to generate the server build your server target expects. This change allows Remix to deploy to more JavaScript runtimes, servers, and hosts without Remix source code needing to know about them.

The following configurations should replace your current serverBuildTarget:

arc

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/_static/build/",
  serverBuildPath: "server/index.js",
  serverMainFields: "main, module",
  serverModuleFormat: "cjs",
  serverPlatform: "node",
  serverMinify: false,
};

cloudflare-pages

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: "functions/[[path]].js",
  serverConditions: "worker",
  serverMainFields: "browser, module, main",
  serverModuleFormat: "esm",
  serverPlatform: "neutral",
  serverDependenciesToBundle: "all",
  serverMinify: true,
};

cloudflare-workers

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: "build/index.js",
  serverConditions: "worker",
  serverMainFields: "browser, module, main",
  serverModuleFormat: "esm",
  serverPlatform: "neutral",
  serverDependenciesToBundle: "all",
  serverMinify: true,
};

deno

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: "build/index.js",
  serverConditions: "deno, worker",
  serverMainFields: "module, main",
  serverModuleFormat: "esm",
  serverPlatform: "neutral",
  serverDependenciesToBundle: "all",
  serverMinify: false,
};

netlify

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: ".netlify/functions-internal/server.js",
  serverConditions: "deno, worker",
  serverMainFields: "main, module",
  serverModuleFormat: "cjs",
  serverPlatform: "node",
  serverMinify: false,
};

node-cjs

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: "build/index.js",
  serverMainFields: "main, module",
  serverModuleFormat: "cjs",
  serverPlatform: "node",
  serverMinify: false,
};

vercel

/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/",
  serverBuildPath: "api/index.js",
  serverMainFields: "main, module",
  serverModuleFormat: "cjs",
  serverPlatform: "node",
  serverMinify: false,
};

Dev Server

We are still stabilizing the new dev server that enables HMR, several CSS libraries (CSS Modules, Vanilla Extract, Tailwind, PostCSS) and simplifies integration with various servers.

We expect a stable v2_dev flag in Remix v1.16.0. Once that ships, we'll have more instructions here to prepare your app for v2.

Docs and examples licensed under MIT