いい感じのRemixのディレクトリ構成

2024-04-10T22:07:49+0900
Remix

2022年の末ごろからRemixを使って小さめのアプリケーションを開発をしています。2023年の3月ごろにプロダクション投入したので、早いもので稼動してからもう1年が経過しました。

その間、大きな機能追加もなかったので基本的に放置していたのですが、問題なく稼動していて要件にあったいいスタックで開発ができたと自我自賛していました。

そんな中、今年に入ってわりと大きめな機能を追加することになり、久しぶりにRemixのコードを書いているのですが、当時のように手探りで行っていた開発ではなく、経験を積んだ分だけ前よりももう少しいい感じのコードが書けそうだったので、今回の開発にあわせて全面的なコードのリファクタリングも行いました。

loaderモジュールとactionモジュールをどう書くか #

RemixのページはRouteファイルを作成します。Routeファイルには、サーバー側で実行されるコードとクライアント側で実行されるコードの両方が書けますが、とりわけサーバー側で実行されるloaderモジュールとactionモジュールは肥大化しがちです。

そうなってくると、中の処理を関数に切り出したりするわけですが、それならば、そもそも両モジュールを別ファイルに書きたいよねとなってきます。

そんなわけで、必然的にloaderモジュールとactionモジュールは別ファイルに書いて、Routeファイルからexportする構成になりました。

いい感じのディレクトリ構成 #

具体的なディレクトリ構成はこんな感じです。

./app
├── .server     # Remix Viteでは.serverディレクトリのみサポートしている
│  ├── actions # action用のコードを配置するディレクトリ
│  │  ├── index.ts
│  │  ├── ...色々なaction
│  │  └── indexAction
│  │     ├── index.ts
│  │     └── index.test.ts
│  └── loaders # loader用のコードを配置するディレクトリ
│     ├── index.ts
│     ├── ...色々なloaderファイル
│     ├── indexLoader
│     │  ├── index.ts
│     │  └── index.test.ts
│     └── rootLoader
│        ├── index.ts
│        └── index.test.ts
├── components  # Reactコンポーネントを配置するディレクトリ
│  ├── ...色々なコンポーネント
│  └── index.ts
├── entry.client.tsx
├── entry.server.tsx
├── root.tsx
└── routes      # route用のコードを配置するディレクトリ
   ├── ...色々なrouteファイル
   └── _index.tsx

Remixでは、サーバー側のコードは.serverモジュールという仕組みを使ってクライアントコードへの混入を防ぐ仕組みがあります。

Remix Viteからは、サーバー.server ディレクトリのみサポートするということなので、サーバー側で実行するloaderとactionは .server ディレクトリ配下に作成した actionsloaders というディレクトリで管理します。

actionsloaders ディレクトリの下には各Routeファイル用のモジュールを配置することで、各モジュールのテストコードも自然と同じ場所に置けるようになります。

loaderモジュールの利用方法 #

例えば、Routeファイル _index.tsx で使うloaderモジュール(.server/loaders/indexLoader/index.ts)を次のように書いて作成します。

import type { LoaderFunctionArgs } from "@remix-run/node";

export async function indexLoader({ request }: LoaderFunctionArgs) {
  // 色々なコードを書く
  return {
    // 色々なものを返す
  };
}

// loaderの戻り値の型も一緒に返しておくとuseLoaderDataで便利に使える
export type IndexLoader = typeof indexLoader;

あわせてテストコードを次のように書きます

import { indexIndexLoader } from ".";
import type { LoaderFunctionArgs } from "@remix-run/node";

test("return valid data", async () => {
  const loaderFunctionArgs: LoaderFunctionArgs = {
    request: new Request("https://example.com", {
      method: "GET",
    }),
    params: {},
    context: {},
  };
  const loaderData = await indexIndexLoader(loaderFunctionArgs);
  const res = await loaderData.json();
  expect(res).toEqual({
    // loaderが返すデータ
  });
});

最後に .server/loaders/index.ts からexportしておきます。

export * from "./indexLoader";

このようにして作成したloaderモジュールは、Routeファイル _index.tsx で次のようにして利用します。

import { useLoaderData } from "@remix-run/react";
import type { IndexIndexLoader } from "~/.server/loaders";
// Routeファイルからas loaderとしてexportすれば、loaderモジュールとして機能する
export { indexIndexLoader as loader } from "~/.server/loaders";

export default function Index() {
  // IndexIndexLoaderを指定することで、dataに型情報がつく
  const data = useLoaderData<IndexIndexLoader>();
  return (
    // クライアントコンポーネントでdataを利用する
  )
}

このようにすれば、安全に.serverモジュールを使いつつ、ある程度の行数のあるloaderやactionモジュールがあっても、Routeファイルをすっきりした状態が保てます。

actionモジュールの利用方法 #

actionモジュールもloaderモジュールと同じように書きます。違いがあるとすれば、テストコードくらいでしょうか。

import { indexAction } from ".";
import type { ActionFunctionArgs } from "@remix-run/node";

test("return error if formData is blank", async () => {
  const formData = new FormData();
  // フォームの値があれば追加する
  // formData.append("name", "value");
  const actionFunctionArgs: ActionFunctionArgs = {
    request: new Request("https://example.com", {
      method: "POST",
      body: formData,
    }),
    params: {},
    context: {},
  };
  const actionData = await indexAction(actionFunctionArgs);
  const res = await actionData.json();
  expect(res).toEqual({
    // エラーのときの戻り値
  });
});

フォームデータを使うactionモジュールのテストコードはこのように書けば大丈夫です。

まとめ #

というわけで、簡単ですが現状で自分が使っているRemixのディレクトリ構成を紹介しました。

Remixは案外いいやつで、v1からv2の移行もかなりスムーズにいきました。Viteへの移行はPrismaがESMではないためちょっとハマっていてまだ様子見しているのですが、そのうちやる予定です。

他にはConformというフォームライブラリ(@techtalkjpさんがドキュメントの日本訳を用意してくれました🎉)がなかなか良かったり(@conform-to/zodにあるparseWithZodが便利)、AWS SDKもあたり前のように使えたりと、慣れるとサクっとアプリケーションを作れるようになります。

SPAモードも入って、とりあえずAPIさえ叩ければいいんやというアプリケーションも任せらるようになったので、ますます選択しやすくなったかと思います。

機能的にだいぶ揃ってきたと思うので、今後どのように進化していくのか楽しみです。