v2へのアップグレード

このドキュメントは、Classic Remix コンパイラ を使用している場合の v1 から v2 への移行に関するガイダンスを提供しています。Vite への移行に関する追加のガイダンスについては、Remix Vite ドキュメント を参照してください。

すべての v2 API と動作は、将来のフラグ を使用して v1 で利用できます。プロジェクトの開発を中断しないように、一度に1つずつ有効にすることができます。すべてのフラグを有効にしたら、v2へのアップグレードは破壊的なアップグレードにならないはずです。

問題が発生した場合は、トラブルシューティング セクションを参照してください。

一般的なアップグレードの問題の簡単な概要については、🎥 2 分で v2 へ をご覧ください。

classic-remix-compiler: (Classic Remix コンパイラのリンクをここに挿入) remix-vite: (Remix Vite ドキュメントのリンクをここに挿入) future-flags: (将来のフラグに関するセクションへのリンクをここに挿入) troubleshooting: (トラブルシューティングセクションへのリンクをここに挿入) 2-min-to-v2: (2 分で v2 へ動画へのリンクをここに挿入)

remix dev

設定オプションについては、remix dev ドキュメントを参照してください。

dev-docs: (This needs a URL to be added here. The original markdown left this as a placeholder.)

remix-serve

Remixアプリサーバー(remix-serve)を使用している場合は、v2_devを有効化してください。

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

以上です!

カスタムアプリサーバー

独自のアプリサーバー(server.js)を使用している場合は、v2_devとの統合方法の例については、テンプレートをご覧ください。または、次の手順に従ってください。

  1. v2_devを有効にする:

    remix.config.js
    /** @type {import('@remix-run/dev').AppConfig} */
    module.exports = {
      future: {
        v2_dev: true,
      },
    };
  2. package.jsonscriptsを更新する:

    • remix watchremix devに置き換える
    • 冗長なNODE_ENV=developmentを削除する
    • アプリサーバーを実行するために-c / --commandを使用する

    例:

    package.json
     {
       "scripts": {
    -    "dev:remix": "cross-env NODE_ENV=development remix watch",
    -    "dev:server": "cross-env NODE_ENV=development node ./server.js"
    +    "dev": "remix dev -c 'node ./server.js'",
       }
     }
  3. アプリが実行されたら、Remixコンパイラに「ready」メッセージを送信する

    server.js
    import { broadcastDevReady } from "@remix-run/node";
    // import { logDevReady } from "@remix-run/cloudflare" // CloudFlareを使用する場合は`logDevReady`を使用
     
    const BUILD_DIR = path.join(process.cwd(), "build");
     
    // ... サーバーの設定コードはこちら ...
     
    const port = 3000;
    app.listen(port, async () => {
      console.log(`👉 http://localhost:${port}`);
      broadcastDevReady(await import(BUILD_DIR));
    });
  4. (オプション)--manual

    requireキャッシュのクリアに依存していた場合は、--manualフラグを使用して引き続き実行できます。

    remix dev --manual -c 'node ./server.js'

    詳細は、マニュアルモードガイドをご覧ください。

v1からv2へのアップグレード後

v1でfuture.v2_devフラグを有効にして動作確認が済んだら、v2にアップグレードできます。 v2_devtrueに設定していただけなら、それを削除すれば動作するはずです。

v2_dev設定を使用している場合は、dev設定フィールドに移動する必要があります。

remix.config.js
  /** @type {import('@remix-run/dev').AppConfig} */
  module.exports = {
-   future: {
-     v2_dev: {
-       port: 4004
-     }
-   }
+   dev: {
+     port: 4004
+   }
  }

ファイルシステムルート規約

ファイルを変更せずにアップグレードする

すぐに変更したくない場合(または、まったく変更したくない場合でも構いません。これは単なる慣例であり、任意のファイル構成を使用できます)、@remix-run/v1-route-convention を使用した古い規則を引き続きv2にアップグレードした後も使用できます。

npm i -D @remix-run/v1-route-convention
remix.config.js
const {
  createRoutesFromFolders,
} = require("@remix-run/v1-route-convention");
 
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  future: {
    // v1.15以降で警告を消去します
    v2_routeConvention: true,
  },
 
  routes(defineRoutes) {
    // v1の規則を使用します。v1.15以降とv2で動作します
    return createRoutesFromFolders(defineRoutes);
  },
};

新規規約へのアップグレード

  • ルートのネストは、フォルダのネストではなく、ファイル名内のドット(.)で作成されるようになりました。
  • セグメント内の suffixed_ アンダーバーは、ドット(.)ではなく、潜在的に一致する親ルートとのネストをオプトアウトします。
  • セグメント内の _prefixed アンダーバーは、__double アンダーバープレフィックスではなく、パスを持たないレイアウトルートを作成します。
  • _index.tsx ファイルは、index.tsx ではなくインデックスルートを作成します。

v1 のようなルートフォルダ:

app/
├── 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.projects.$projectId.print.tsx
└── root.tsx

v2_routeConvention を使用すると、次のようになります。

app/
├── 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._index.tsx
│   ├── dashboard.calendar.$day.tsx
│   ├── dashboard.calendar.tsx
│   ├── dashboard.projects.$projectId._index.tsx
│   ├── dashboard.projects.$projectId.collaborators.tsx
│   ├── dashboard.projects.$projectId.edit.tsx
│   ├── dashboard.projects.$projectId.settings.tsx
│   ├── dashboard.projects.$projectId.tasks.$taskId.tsx
│   ├── dashboard.projects.$projectId.tsx
│   ├── dashboard.projects.new.tsx
│   ├── dashboard.projects.tsx
│   └── dashboard_.projects.$projectId.print.tsx
└── root.tsx

親ルートは、(認証ルートのように)それらの間に数十ものルートがあるのではなく、一緒にグループ化されるようになりました。パスは同じだがネストが異なるルート(dashboarddashboard_ など)も一緒にグループ化されます。

新しい規約では、任意のルートを内部に route.tsx ファイルを持つディレクトリにすることができ、ルートモジュールの定義が可能になります。これにより、使用するルートとモジュールの共存が可能になります。

たとえば、_public.tsx_public/route.tsx に移動し、ルートが使用するモジュールを共存させることができます。

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

この変更の詳細については、元の「フラットルート」提案 を参照してください。

Route headers

Remix v2では、ルートのheaders関数の動作がわずかに変更されました。remix.config.jsfuture.v2_headersフラグを使用して、この新しい動作を事前にオプトインできます。

v1では、Remixはリーフ(末端)の「レンダリング済み」ルートのheaders関数の結果のみを使用していました。すべての潜在的なリーフにheaders関数を追加し、それに応じてparentHeadersをマージするのは開発者の責任でした。これはすぐに面倒になり、新しいルートを追加したときに、親からの同じヘッダーを共有したい場合でも、headers関数の追加を忘れることが容易に起こりえます。

v2では、Remixはレンダリングされたルートの中で最も深いheaders関数を用いるようになりました。これにより、共通の祖先からルート間でヘッダーをより簡単に共有できます。必要に応じて、特定の動作が必要なより深いルートにheaders関数を追加できます。

Route meta

Remix v2では、ルートmeta関数のシグネチャとRemixが内部的にメタタグを処理する方法が変更されました。

metaからオブジェクトを返す代わりに、記述子の配列を返すようになり、マージを自分で管理するようになりました。これによりmeta APIはlinksに近づき、メタタグのレンダリング方法に関する柔軟性と制御性が向上します。

さらに、<Meta />は階層内のすべてのルートのメタをレンダリングしなくなりました。リーフルートのmetaから返されたデータのみがレンダリングされます。関数引数のmatchesにアクセスすることで、親ルートのメタを含めることもできます。

この変更に関する詳細については、元のv2 meta提案を参照してください。

meta-v2-matches: (link_to_matches_documentation) meta-v2-rfc: (link_to_rfc)

v2 で v1 の meta 規約を使用する

v1 の規約を引き続き使用するには、@remix-run/v1-meta パッケージを使用して meta エクスポートを更新できます。

metaV1 関数を使用すると、meta 関数の引数と、現在返しているオブジェクトと同じオブジェクトを渡すことができます。この関数は、同じマージロジックを使用してリーフルートのメタをその直接の親ルートのメタとマージしてから、v2 で使用できるメタ記述子の配列に変換します。

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

この関数は、デフォルトでは階層全体でメタをマージしません。これは、metaV1 関数を使用せずにオブジェクトの配列を直接返すルートがある場合があり、予期しない動作につながる可能性があるためです。階層全体でメタをマージする場合は、すべてのルートの meta エクスポートに metaV1 関数を使用してください。

parentsData 引数

v2 では、meta 関数は parentsData 引数を受け取りません。これは、metamatches 引数 を介してすべてのルートマッチにアクセスできるようになったためです。これには、各マッチのローダーデータが含まれています。

parentsData のAPIを複製するために、@remix-run/v1-meta パッケージは getMatchesData 関数を提供します。これは、各マッチのデータがルートのIDでキー付けされたオブジェクトを返します。

app/routes/v1-route.tsx
export function meta(args) {
  const parentData = args.parentsData["routes/parent"];
}

以下のようになります。

app/routes/v2-route.tsx
import { getMatchesData } from "@remix-run/v1-meta";
 
export function meta(args) {
  const matchesData = getMatchesData(args);
  const parentData = matchesData["routes/parent"];
}

新しいmetaへの更新

app/routes/v1-route.tsx
export function meta() {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  };
}
app/routes/v2-route.tsx
export function meta() {
  return [
    { title: "..." },
    { name: "description", content: "..." },
    { property: "og:title", content: "..." },
 
    // SEO関連の<link>タグを追加できるようになりました
    { tagName: "link", rel: "canonical", href: "..." },
 
    // <script type=ld+json>も追加できます
    {
      "script:ld+json": {
        some: "value",
      },
    },
  ];
}

matches 引数

v1 では、ネストされたルートから返されるオブジェクトはすべてマージされていましたが、v2ではmatches を使用して自分でマージする必要があります。

app/routes/v2-route.tsx
export function meta({ matches }) {
  const rootMeta = matches[0].meta;
  const title = rootMeta.find((m) => m.title);
 
  return [
    title,
    { name: "description", content: "..." },
    { property: "og:title", content: "..." },
 
    // SEO 関連の <link> タグを追加できるようになりました
    { tagName: "link", rel: "canonical", href: "..." },
 
    // <script type=ld+json> も追加できます
    {
      "script:ld+json": {
        "@context": "https://schema.org",
        "@type": "Organization",
        name: "Remix",
      },
    },
  ];
}

meta のドキュメントには、ルートメタのマージに関するさらに詳しいヒントがあります。

CatchBoundaryErrorBoundary

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

v1では、スローされたResponseは最も近いCatchBoundaryをレンダリングし、その他の未処理の例外はすべてErrorBoundaryをレンダリングしました。v2ではCatchBoundaryがなくなり、すべての未処理の例外はErrorBoundaryをレンダリングします。レスポンスに関わらず同様です。

さらに、エラーはErrorBoundaryにプロパティとして渡されなくなり、useRouteErrorフックを使用してアクセスするようになりました。

app/routes/v1-route.tsx
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>
  );
}

以下のようになります。

app/routes/v2-route.tsx
import {
  useRouteError,
  isRouteErrorResponse,
} from "@remix-run/react";
 
export function ErrorBoundary() {
  const error = useRouteError();
 
  // trueの場合、これは以前`CatchBoundary`に渡されていたものです
  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>Oops</h1>
        <p>Status: {error.status}</p>
        <p>{error.data.message}</p>
      </div>
    );
  }
 
  // 独自のロジックで型チェックすることを忘れないでください。
  // スローできる値はエラーだけではありません!
  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>
  );
}

formMethod

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

複数のAPIがサブミッションのformMethodを返します。v1ではメソッドの小文字バージョンを返しましたが、v2では大文字バージョンを返すようになりました。これはHTTPとfetchの仕様に合わせるためです。

function Something() {
  const navigation = useNavigation();
 
  // v1
  navigation.formMethod === "post";
 
  // v2
  navigation.formMethod === "POST";
}
 
export function shouldRevalidate({ formMethod }) {
  // v1
  formMethod === "post";
 
  // v2
  formMethod === "POST";
}

useTransition

このフックは、同じ名前の最近のReactフックと混同を避けるため、useNavigation に名前が変更されました。また、type フィールドが削除され、submission オブジェクトはnavigation オブジェクト自体にフラット化されました。

app/routes/v1-route.tsx
import { useTransition } from "@remix-run/react";
 
function SomeComponent() {
  const transition = useTransition();
  transition.submission.formData;
  transition.submission.formMethod;
  transition.submission.formAction;
  transition.type;
}
app/routes/v2-route.tsx
import { useNavigation } from "@remix-run/react";
 
function SomeComponent() {
  const navigation = useNavigation();
 
  // transition.submission のキーは `navigation[key]` にフラット化されます
  navigation.formData;
  navigation.formMethod;
  navigation.formAction;
 
  // このキーは削除されました
  navigation.type;
}

以前のtransition.type は、以下の例のように導き出すことができます。ただし、通常はnavigation.statenavigation.formData、またはuseActionDataでアクションから返されたデータをチェックすることで、同じ動作を実現するより簡単な方法があることに注意してください。Discordで質問していただければ、喜んでお手伝いさせていただきます :D

function Component() {
  const navigation = useNavigation();
 
  // transition.type === "actionSubmission"
  const isActionSubmission =
    navigation.state === "submitting";
 
  // transition.type === "actionReload"
  const isActionReload =
    navigation.state === "loading" &&
    navigation.formMethod != null &&
    navigation.formMethod != "GET" &&
    // サブミッションナビゲーションがあり、送信された場所にロードしている
    navigation.formAction === navigation.location.pathname;
 
  // transition.type === "actionRedirect"
  const isActionRedirect =
    navigation.state === "loading" &&
    navigation.formMethod != null &&
    navigation.formMethod != "GET" &&
    // サブミッションナビゲーションがあり、別の場所に移動している
    navigation.formAction !== navigation.location.pathname;
 
  // transition.type === "loaderSubmission"
  const isLoaderSubmission =
    navigation.state === "loading" &&
    navigation.state.formMethod === "GET" &&
    // ローダーサブミッションがあり、送信された場所に移動している
    navigation.formAction === navigation.location.pathname;
 
  // transition.type === "loaderSubmissionRedirect"
  const isLoaderSubmissionRedirect =
    navigation.state === "loading" &&
    navigation.state.formMethod === "GET" &&
    // ローダーサブミッションがあり、新しい場所に移動している
    navigation.formAction !== navigation.location.pathname;
}

GET サブミッションに関する注意

Remix v1では、<Form method="get">submit({}, { method: 'get' })などのGETサブミッションは、transition.stateidle -> submitting -> idleという状態遷移をしていました。フォームを「送信」しているにもかかわらず、GETナビゲーションを実行し、ローダー(アクションではない)のみを実行しているため、これはセマンティックに正しくありません。機能的には、<Link>navigate()と変わりませんが、ユーザーが入力値を介して検索パラメータ値を指定している可能性があります。

v2では、GETサブミッションはロード中のナビゲーションとしてより正確に反映され、navigation.stateを通常のリンクの動作と合わせるためにidle -> loading -> idleという状態遷移をします。GETサブミッションが<Form>またはsubmit()から来た場合、useNavigation.form*が設定されるため、必要に応じて区別できます。

useFetcher

useNavigationと同様に、useFetchersubmissionをフラット化し、typeフィールドを削除しました。

app/routes/v1-route.tsx
import { useFetcher } from "@remix-run/react";
 
function SomeComponent() {
  const fetcher = useFetcher();
  fetcher.submission.formData;
  fetcher.submission.formMethod;
  fetcher.submission.formAction;
  fetcher.type;
}
app/routes/v2-route.tsx
import { useFetcher } from "@remix-run/react";
 
function SomeComponent() {
  const fetcher = useFetcher();
 
  // これらのキーはフラット化されています
  fetcher.formData;
  fetcher.formMethod;
  fetcher.formAction;
 
  // このキーは削除されました
  fetcher.type;
}

以前のfetcher.typeは、以下の例のように導き出すことができます。ただし、通常はfetcher.statefetcher.formData、またはfetcher.dataでアクションから返されたデータをチェックすることで同じ動作を実現できる、より簡単な方法がある可能性が高いことに注意してください。Discordで質問していただければ、喜んでお手伝いさせていただきます :D

function Component() {
  const fetcher = useFetcher();
 
  // fetcher.type === "init"
  const isInit =
    fetcher.state === "idle" && fetcher.data == null;
 
  // fetcher.type === "done"
  const isDone =
    fetcher.state === "idle" && fetcher.data != null;
 
  // fetcher.type === "actionSubmission"
  const isActionSubmission = fetcher.state === "submitting";
 
  // fetcher.type === "actionReload"
  const isActionReload =
    fetcher.state === "loading" &&
    fetcher.formMethod != null &&
    fetcher.formMethod != "GET" &&
    // データが返された場合、リロードしている必要があります
    fetcher.data != null;
 
  // fetcher.type === "actionRedirect"
  const isActionRedirect =
    fetcher.state === "loading" &&
    fetcher.formMethod != null &&
    fetcher.formMethod != "GET" &&
    // データがない場合、リダイレクトしている必要があります
    fetcher.data == null;
 
  // fetcher.type === "loaderSubmission"
  const isLoaderSubmission =
    fetcher.state === "loading" &&
    fetcher.formMethod === "GET";
 
  // fetcher.type === "normalLoad"
  const isNormalLoad =
    fetcher.state === "loading" &&
    fetcher.formMethod == null;
}

GET送信に関する注意

Remix v1では、<fetcher.Form method="get">fetcher.submit({}, { method: 'get' })などのGET送信は、fetcher.stateidle -> submitting -> idleとなっていました。フォームを「送信」しているにもかかわらず、GETリクエストを実行し、ローダー(アクションではない)のみを実行しているため、これはセマンティックに完全に正確ではありません。機能的にはfetcher.load()と変わりませんが、ユーザーが入力値を介して検索パラメータ値を指定している可能性があります。

v2では、GET送信はロードリクエストとしてより正確に反映されるため、fetcher.stateを通常のfetcherロードの動作と合わせるためにidle -> loading -> idleとなります。GET送信が<fetcher.Form>またはfetcher.submit()からのものである場合、fetcher.form*が設定されるため、必要に応じて区別できます。

リンク imagesizesimagesrcset

ルートの links プロパティは、HTMLの小文字値ではなく、すべてReactのキャメルケース値にする必要があります。これらの2つの値はv1で小文字として紛れ込んでいました。v2では、キャメルケースのバージョンのみが有効です。

app/routes/v1-route.tsx
export const links: LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imagesrcset: "...",
      imagesizes: "...",
    },
  ];
};
app/routes/v2-route.tsx
export const links: V2_LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imageSrcSet: "...",
      imageSizes: "...",
    },
  ];
};

browserBuildDirectory

remix.config.jsファイルにおいて、browserBuildDirectoryassetsBuildDirectoryに名前変更してください。

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

devServerBroadcastDelay

remix.config.js から devServerBroadcastDelay を削除してください。このオプションが必要だった競合状態は、v2 または v2_dev で解消されました。

remix.config.js
  /** @type {import('@remix-run/dev').AppConfig} */
  module.exports = {
-   devServerBroadcastDelay: 300,
  };

devServerPort

remix.config.jsファイルにおいて、devServerPortfuture.v2_dev.portに名前を変更してください。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  devServerPort: 8002,
};
remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  // v1.x の間は、future フラグを使用します
  future: {
    v2_dev: {
      port: 8002,
    },
  },
};

v1からv2にアップグレードすると、これはルートレベルのdev設定にフラット化されます。

dev-after-upgrading: (This needs a URL or further context to translate accurately. Please provide the link target.)

serverBuildDirectory

remix.config.jsファイルにおいて、serverBuildDirectoryserverBuildPathに名前変更し、ディレクトリではなく、モジュールパスを指定してください。

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

Remixは以前、サーバー用に複数のモジュールを作成していましたが、現在は単一ファイルを作成するようになりました。

serverBuildTarget

ビルドターゲットを指定する代わりに、remix.config.js オプションを使用して、サーバーターゲットが期待するサーバービルドを生成します。この変更により、Remixソースコードがそれらを知る必要なく、Remixをより多くのJavaScriptランタイム、サーバー、およびホストにデプロイできるようになります。

現在のserverBuildTargetを置き換えるべき設定は以下のとおりです。

arc

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/_static/build/",
  serverBuildPath: "server/index.js",
  serverMainFields: ["main", "module"], // デフォルト値、削除できます
  serverMinify: false, // デフォルト値、削除できます
  serverModuleFormat: "cjs", // 1.x のデフォルト値、アップグレード前に追加
  serverPlatform: "node", // デフォルト値、削除できます
};

cloudflare-pages

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/", // デフォルト値、削除できます
  serverBuildPath: "functions/[[path]].js",
  serverConditions: ["worker"],
  serverDependenciesToBundle: "all",
  serverMainFields: ["browser", "module", "main"],
  serverMinify: true,
  serverModuleFormat: "esm", // 2.x のデフォルト値、アップグレードしたら削除できます
  serverPlatform: "neutral",
};

cloudflare-workers

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/", // デフォルト値、削除できます
  serverBuildPath: "build/index.js", // デフォルト値、削除できます
  serverConditions: ["worker"],
  serverDependenciesToBundle: "all",
  serverMainFields: ["browser", "module", "main"],
  serverMinify: true,
  serverModuleFormat: "esm", // 2.x のデフォルト値、アップグレードしたら削除できます
  serverPlatform: "neutral",
};

deno

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/", // デフォルト値、削除できます
  serverBuildPath: "build/index.js", // デフォルト値、削除できます
  serverConditions: ["deno", "worker"],
  serverDependenciesToBundle: "all",
  serverMainFields: ["module", "main"],
  serverMinify: false, // デフォルト値、削除できます
  serverModuleFormat: "esm", // 2.x のデフォルト値、アップグレードしたら削除できます
  serverPlatform: "neutral",
};

node-cjs

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  publicPath: "/build/", // デフォルト値、削除できます
  serverBuildPath: "build/index.js", // デフォルト値、削除できます
  serverMainFields: ["main", "module"], // デフォルト値、削除できます
  serverMinify: false, // デフォルト値、削除できます
  serverModuleFormat: "cjs", // 1.xではデフォルト値、アップグレード前に追加
  serverPlatform: "node", // デフォルト値、削除できます
};

serverModuleFormat

デフォルトのサーバーモジュール出力形式が cjs から esm に変更されました。v2 でも引き続き CJS を使用できますが、アプリケーション内の多くの依存関係が ESM と互換性がない可能性があります。

remix.config.js では、既存の動作を維持するには serverModuleFormat: "cjs" を、新しい動作を選択するには serverModuleFormat: "esm" を指定する必要があります。

browserNodeBuiltinsPolyfill

Node.js組み込みモジュールのポリフィルは、デフォルトではブラウザで提供されなくなりました。Remix v2では、必要に応じてポリフィル(または空のポリフィル)を明示的に再導入する必要があります。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  browserNodeBuiltinsPolyfill: {
    modules: {
      buffer: true,
      fs: "empty",
    },
    globals: {
      Buffer: true,
    },
  },
};

一部のポリフィルは非常に大きくなる可能性があるため、ブラウザバンドルで許可されるポリフィルを明示的に指定することをお勧めしますが、次の設定を使用すると、Remix v1からの完全なポリフィルセットを簡単に復元できます。

remix.config.js
const { builtinModules } = require("node:module");
 
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  browserNodeBuiltinsPolyfill: {
    modules: builtinModules,
  },
};

serverNodeBuiltinsPolyfill

Node.js組み込みモジュールのポリフィルは、Node.js以外のサーバープラットフォームではデフォルトで提供されなくなりました。

Node.js以外のサーバープラットフォームをターゲットにしており、v1の新しいデフォルト動作を採用したい場合は、remix.config.jsserverNodeBuiltinsPolyfill.modulesに空のオブジェクトを明示的に指定して、すべてのサーバーポリフィルを削除する必要があります。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverNodeBuiltinsPolyfill: {
    modules: {},
  },
};

その後、必要に応じてポリフィル(または空のポリフィル)を再導入できます。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverNodeBuiltinsPolyfill: {
    modules: {
      buffer: true,
      fs: "empty",
    },
    globals: {
      Buffer: true,
    },
  },
};

参考として、v1からのデフォルトポリフィルの完全なセットを手動で指定する方法は次のとおりです。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  serverNodeBuiltinsPolyfill: {
    modules: {
      _stream_duplex: true,
      _stream_passthrough: true,
      _stream_readable: true,
      _stream_transform: true,
      _stream_writable: true,
      assert: true,
      "assert/strict": true,
      buffer: true,
      console: true,
      constants: true,
      crypto: "empty",
      diagnostics_channel: true,
      domain: true,
      events: true,
      fs: "empty",
      "fs/promises": "empty",
      http: true,
      https: true,
      module: true,
      os: true,
      path: true,
      "path/posix": true,
      "path/win32": true,
      perf_hooks: true,
      process: true,
      punycode: true,
      querystring: true,
      stream: true,
      "stream/promises": true,
      "stream/web": true,
      string_decoder: true,
      sys: true,
      timers: true,
      "timers/promises": true,
      tty: true,
      url: true,
      util: true,
      "util/types": true,
      vm: true,
      wasi: true,
      worker_threads: true,
      zlib: true,
    },
  },
};

installGlobals

Node組み込みのfetch実装を使用するための準備として、fetchグローバルのインストールは現在アプリサーバーの責任となっています。remix-serveを使用している場合は、何もする必要はありません。独自のアプリサーバーを使用している場合は、自分でグローバルをインストールする必要があります。

server.ts
import { installGlobals } from "@remix-run/node";
 
installGlobals();

エクスポートされたポリフィルの削除

Remix v2では、@remix-run/nodeからこれらのポリフィル実装をエクスポートしなくなりました。代わりに、グローバル名前空間にあるインスタンスを使用する必要があります。変更が必要になる可能性が高い場所の1つは、app/entry.server.tsxファイルです。ここでは、NodeのPassThroughをWebのReadableStreamcreateReadableStreamFromReadableを使用して変換する必要もあります。

app/entry.server.tsx
  import { PassThrough } from "node:stream";
  import type { AppLoadContext, EntryContext } from "@remix-run/node"; // or cloudflare/deno
- import { Response } from "@remix-run/node"; // or cloudflare/deno
+ import { createReadableStreamFromReadable } from "@remix-run/node"; // or cloudflare/deno
  import { RemixServer } from "@remix-run/react";
  import { isbot } from "isbot";
  import { renderToPipeableStream } from "react-dom/server";
 
  const ABORT_DELAY = 5_000;
 
  export default function handleRequest({ /* ... */ }) { ... }
 
  function handleBotRequest(...) {
    return new Promise((resolve, reject) => {
      let shellRendered = false;
      const { pipe, abort } = renderToPipeableStream(
        <RemixServer ... />,
        {
          onAllReady() {
            shellRendered = true;
            const body = new PassThrough();
 
            responseHeaders.set("Content-Type", "text/html");
 
            resolve(
-             new Response(body, {
+             new Response(createReadableStreamFromReadable(body), {
                headers: responseHeaders,
                status: responseStatusCode,
              })
            );
 
            pipe(body);
          },
          ...
          onShellError(error: unknown) { ... }
          onError(error: unknown) { ... }
        }
      );
 
      setTimeout(abort, ABORT_DELAY);
    });
  }
 
  function handleBrowserRequest(...) {
    return new Promise((resolve, reject) => {
      let shellRendered = false;
      const { pipe, abort } = renderToPipeableStream(
        <RemixServer ... />,
        {
          onShellReady() {
            shellRendered = true;
            const body = new PassThrough();
 
            responseHeaders.set("Content-Type", "text/html");
 
            resolve(
-              new Response(body, {
+              new Response(createReadableStreamFromReadable(body), {
                headers: responseHeaders,
                status: responseStatusCode,
              })
            );
 
            pipe(body);
          },
          onShellError(error: unknown) { ... },
          onError(error: unknown) { ... },
        }
      );
 
      setTimeout(abort, ABORT_DELAY);
    });
  }

source-map-support

ソースマップのサポートは、現在アプリサーバーの責任です。remix-serve を使用している場合は、何も必要ありません。独自のアプリサーバーを使用している場合は、source-map-support を自分でインストールする必要があります。

npm i source-map-support
server.ts
import sourceMapSupport from "source-map-support";
 
sourceMapSupport.install();

Netlify アダプター

@remix-run/netlify ランタイムアダプターは、@netlify/remix-adapter@netlify/remix-edge-adapter に置き換えられたため、Remix v2 から削除されました。@remix-run/netlify のすべてのインポートを @netlify/remix-adapter に変更してコードを更新してください。

@netlify/remix-adapter@netlify/functions@^1.0.0 を必要とすることに注意してください。これは、@remix-run/netlify で現在サポートされている @netlify/functions のバージョンと比較して破壊的変更です。

このアダプターの削除に伴い、Netlify テンプレート も削除され、公式Netlifyテンプレート に置き換えられました。

Vercel アダプター

@remix-run/vercel ランタイムアダプターは、Vercel の標準機能に置き換えられたため、Remix v2 より廃止され、削除されました。package.json から @remix-run/vercel@vercel/node を削除し、server.js/server.ts ファイルを削除し、remix.config.js から serverserverBuildPath オプションを削除することで、コードを更新してください。

このアダプターの削除に伴い、Vercel テンプレート も削除され、公式 Vercel テンプレート に置き換えられました。

vercel-template: // vercel-templateへのリンクをここに挿入 official-vercel-template: // official-vercel-templateへのリンクをここに挿入

組み込みPostCSS/Tailwindサポート

v2では、プロジェクト内にPostCSSと/またはTailwindの設定ファイルが存在する場合、これらのツールはRemixコンパイラ内で自動的に使用されます。

v2への移行時に、Remix以外でカスタムPostCSSと/またはTailwindの設定を維持したい場合は、remix.config.jsでこれらの機能を無効にできます。

remix.config.js
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
  postcss: false,
  tailwind: false,
};

トラブルシューティング

ESM/CommonJS エラー

"SyntaxError: Named export '<something>' not found. The requested module '<something>' is a CommonJS module, which may not support all module.exports as named exports."

serverModuleFormat セクションを参照してください。