🚀

Remixでv2に移行する

2023/07/09に公開

はじめに

今回の記事ではRemixをv1からv2に移行する方法をまとめていきます。
それでは早速見ていきましょう。

CHANGELOG

2023/10/08

  • v2リリースに伴い、future系のフラグが必要なくなったので記事から削除
  • LoaderArgsLoaderFunctionArgs に変更された為それについて記述
  • Remix関係のライブラリについて記述

loader や actionの型が変更

前まで v2から
LoaderArgs LoaderFunctionArgs
ActionArgs ActionFunctionArgs

v2 route

これは routes フォルダに関する変更です。ファイルなどが多いと結構大変かもしれませんが、頑張ってやっていきましょう。

基本ルート

v1の頃 routes/index.ts だったものが routes/_index.ts になります。

リンク 一致するルート
/ _index.tsx

フラットルートが実装されました

実装されたというより、今後はこれがメインに近いと思った方がいいと思います。実際にどんな感じかというと以下の例が分かりやすいです。

これが

v1
routes
├── users
│   ├── index.tsx  # users直下のページ(例としてはユーザー一覧など
│   └── profile.tsx  # プロフィール
└── users.tsx  # レイアウト ここに Outletを書く

こうなります

v2
routes
├── users._index.tsx  # users直下のページ(例としてはユーザー一覧など
├── users.profile.tsx  # プロフィール `users.tsx` のレイアウトが適応されない!
└── users.tsx # レイアウト ここに Outletを書く

ディレクトリ内に route.tsxindex.tsx がある場合は、他のモジュールはルートファイルとして認識されなくなります。つまり、 routes フォルダにページに関連するコンポーネントやデータベースへのアクセスなどを集めることが可能になりました。これに関しては後述するので、とりあえずそういうのがあるという風に覚えておいてください。

また、ここで大事なのは users.tsxusers._index.tsx の機能がごちゃ混ぜにならないことです。今回はコメントをファイルの横に書いているのでそちらを読んでいただければわかりやすいかなと思います。

よりv2の構造が分かりやすくなるようにするため、表にしてみましょう。
※ルートというのは今回ファイルのことを指します

URL 一致するルート レイアウト
/users users._index.tsx users.tsx
/users/profile users.profile.tsx users.tsx

URLはネストするがレイアウトはネストしないということが可能に

「おまえは何を言っているんだ」と思った人もいるかもしれませんが、今から説明するので落ち着いて聞いてください。

これはURLとしては /users/profile のようにネストさせたいが、 profileusers のレイアウトを適応したくない場合に役に立つ機能です。
使用する場合は、親セグメントの末尾にアンダースコアをつけることで使用できます。
以下に実際に使用する場合のファイル名を記載します

v2
routes
├── users._index.tsx  # users直下のページ(例としてはユーザー一覧など
├── users_.profile.tsx  # プロフィール `users.tsx` のレイアウトが適応されない!
└── users.tsx # レイアウト ここに Outletを書く

今回はレイアウトを適応しないルートのファイル名はusers_.profile.tsx としています。
ちなみにこの機能を使用してレイアウトを適応しなかった場合は root.tsx のレイアウトが適応されます。

一応表にして記載すると以下のようになります。

URL 一致するルート レイアウト
/users users._index.tsx users.tsx
/users/profile users_.profile.tsx root.tsx

URLではネストしないが、ネストしたレイアウトが可能に

これもまた言葉では表しにくいのですが、まずは以下のディレクトリ構造を見てください。

v2
app/
├── routes/
│   ├── _auth.login.tsx
│   ├── _auth.register.tsx
│   ├── _auth.tsx
│   ├── _index.tsx
│   ├── concerts.$city.tsx
│   └── concerts.tsx
└── root.tsx

これは以下のような表に直せます。

リンク 一致するルート レイアウト
/ _index.tsx root.tsx
/login _auth.login.tsx _auth.tsx
/register _auth.register.tsx _auth.tsx
/concerts/salt-lake-city concerts.$city.tsx concerts.tsx

今回の例で言うなら、 本来 auth/login などになる物を /login にした状態で _auth.tsx のレイアウトを適応するというものです。これはどのように使用するかというと、対象としたいルートファイル名の先頭にアンダースコアをつけることで使用できます(今回なら_auth

フォルダを使用したルート

前からRemixを触ってた人はこう思ったのではないでしょうか、「え?フォルダ無いの?」と、安心してください!入ってますよ!

それではディレクトリ構造を先にご覧ください。

v2
routes
├── users._index
│   └── route.tsx  # users直下のページ(例としてはユーザー一覧など
├── users.profile
│   ├── get-profile-data.server.tsx  # route.tsxで使う関数が入っている
│   └── route.tsx  # プロフィールページ
└── users.tsx  # レイアウト

さて、users.profileusers/profile が出来るのはもう先ほどの流れからなんとなくわかっている方も多いと思います。ここで、フラットルートの最初の方で触れた route.tsx の機能を改めて説明します。

route.tsx がある場合そのディレクトリ内のファイルはルートファイルとして認識されないと先ほど説明しましたが、これにより今回は get-profile-data.server.tsx というファイルが新しく増えています。

これにはデータベースにアクセスしユーザーのプロフィールを取得するための関数などが入っています。v1の頃では loader の中でやるか、 routes フォルダの外で作成するしか無かったものが、関連性の高いものに纏められるようになったという認識で大丈夫です。

また、念のために説明しておくと route.tsx はv1の頃にフォルダを使用した際に使用していた index.tsx と同じものだと思えば大丈夫です。ただ、この場合はネストしたページだとしてもフォルダを分ける必要があります。それがv1とv2の大きな違いです。

Meta

metaの記述方式が変わります。v1ではオブジェクトにキーと値を入れる形でしたが、v2ではarrayにそれぞれ一つずつオブジェクトを入れる形になります

v1.tsx
export const meta: MetaFunction = () => {
  return {
    title: "...",
    description: "...",
    "og:title": "...",
  }
};
v2.tsx
export const meta: MetaFunction = () => {
  return [
    { title: "..." },
    { name: "description", content: "..." },
    { property: "og:title", content: "..." },
};

また、v1の記述のまま内部的にv2にしたいという場合は @remix-run/v1-meta パッケージの metaV1 という関数が使用可能です。一応これで延命が可能ですが、個人的にはパッケージを追加する手間などを考慮すると素直に新しい記述にした方がいいと思います。

v2.tsx
import { metaV1 } from "@remix-run/v1-meta";
export const meta: MetaFunction = (args) => {
  return metaV1(args, {
    title: "...",
    description: "...",
    "og:title": "...",
  });
}

matches

どうもv1では nestされたrouteのオブジェクトはすべてマージされていたようで、v2では自身でマージを管理する必要があるようです。いまいちこれに関しては自分もよく分かってないので詳しくは公式ドキュメントのこちらを見てみてください。

v2.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: "..." },

    // 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",
      },
    },
  ];
}

CatchBoundaryErrorBoundary

v1ではスローされた例外は最も近い物をレンダリングし、他の処理されなかった例外は ErrorBoundary 等でレンダリングされていましたが、v2ではそれらが無くなり、ひとまとめになっています。更に、エラーは props で渡されなくなり、 useRouteError hook で取得できるようになりました。

v1.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>
  );
}
v2.tsx
import {
  useRouteError,
  isRouteErrorResponse,
} from "@remix-run/react";

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

  // trueの場合 v1の頃の `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

これは結構シンプルな変更で useNavigation hookなどにある formMethod の中身である postget などといったフォームのメソッドが POSTGET といった風にアッパーケースになるという変更です。useNavigation に限らず、 formMethod そのものの仕様が変わるという認識です。

useTransition

これは最近のReact hookとの混同を避けるために非推奨になっています。今後は useNavigation hookを使いましょう。また、フィールドがなくなり、オブジェクトがオブジェクトそのものにフラット化されました。

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

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

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

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

  // this key is removed
  navigation.type;
}

フラット化は v2では submission を間に挟まなくてもよくなったという所だと思います。

useFetcher

こちらに関しては useTransition と同じようにフィールドが削除され、フラット化されています。

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

function SomeComponent() {
  const fetcher = useFetcher();
  fetcher.submission.formData;
  fetcher.submission.formMethod;
  fetcher.submission.formAction;
  fetcher.type;
}
v2.tsx
import { useFetcher } from "@remix-run/react";

function SomeComponent() {
  const fetcher = useFetcher();

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

  // this key is removed
  fetcher.type;
}

v1ではlowercaseが許容されていましたが、v2からはcamelCaseを使用する必要があります。

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

型も LinksFunction から V2_LinksFunction に変わっています。

v2.tsx
export const links: V2_LinksFunction = () => {
  return [
    {
      rel: "preload",
      as: "image",
      imageSrcSet: "...",  // camelCaseになっている
      imageSizes: "...",  // camelCaseになっている
    },
  ];
};

remix.config.jsに関する変更

先に軽くまとめておきます。

変更前 変更後 備考
browserBuildDirectory assetsBuildDirectory
serverBuildDirectory serverBuildPath ディレクトリではなく、モジュールに対するパスを指定する必要があります

serverBuildDirectory

変更前

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",
};

serverBuildTarget

serverBuildTarget を指定する代わりに デプロイ先のサービス名などを指定することで、簡単にそのサーバーが求めるコードを生成できるということのようです。
これに関しては私はあまり使ったことが無いので、詳しく知りたい方は以下のドキュメントをご覧ください。
https://remix.run/docs/en/main/pages/v2#serverbuildtarget

Dev Server

v2のdev サーバーはHMRとHDRなどをサポートしており、非常にクールな機能となっています。今まで開発している最中、フォームのコンポーネントに変更を加えた場合、ページ自体がリロードされフォームの内容などは消えてしまっていましたが、v2からは変更した箇所のみを更新することが可能となっており、フォームのコンポーネントを更新しても入力した内容はそのままというものです。
これは何もフロントだけの話ではなく、 loader 内などで prismaなどを用いてデータ取得している場合、なんとそれもHMRによってページをリロードすることなく、対象の箇所だけ変更可能となっています。個人的に今回紹介する物の中で一番クールな機能だと思います。

より詳しく知りたい!という方は以下のRemix公式の動画でどのような機能か確認しましょう!
https://www.youtube.com/watch?v=2c2OeqOX72s

Remix関連のライブラリについて

remix-utilsremix-auth を使用している場合はバージョンを上げましょう。更新が止まっているものはForkするか更新が再開するまでv2へのアップデートを見送るのがいいと思います。

最後に

ここまでお読みいただきありがとうございました。Remixはまだ日本での情報などが少なく、開発していく中で良く分からないところも出てくるかもしれませんが公式Discordやドキュメントなどを読んでいくことでそれなりにどうにかなるので、ぜひ挑戦してみてください。一応Misskeyもやっているので、分からないことがあって直接聞きたかったりしたらこのアカウントにメンションを飛ばしてくだされば返信できます。
また、過去に私が執筆した記事でv1 routeの時にはなりますが、どのように各種機能などを使うか等をまとめているので、よければそちらもご覧ください。改めてここまで読んでいただきありがとうございました。

リンク集

今回記事の中で紹介したv2 routeに関する公式ドキュメント
https://remix.run/docs/en/main/pages/v2

Remixの公式Discord
https://discord.com/invite/xwx7mMzVkA

私が過去に執筆したRemixの記事
https://zenn.dev/yupix/articles/65dca9fe6eb317
https://zenn.dev/yupix/articles/06a2db733b36f6

Discussion