Gemcook Tech Blog
📁

Collocationを意識したRemixのディレクトリ構成

2024/06/28に公開

はじめに

こんにちは!
今回はRemixのディレクトリ構成のお話です。筆者自身、Remixを触り始めて少しずつRemixの特徴を掴めてきたところで以下のような気持ちが出てきました。

actionとloaderってどこか別のファイルに書けないの...?🤔

RemixはRouteファイルの中にサーバー処理のloaderとactionを書くことができます。ただ、一つのRouteファイル内にloaderとaction、UI部分を実装すると、コードが肥大化してしまいます。そこで、loaderとactionを別ファイルに書く方法を探していくうちに以下の記事に出会いました。

https://blog.tomoya.dev/posts/my-best-remix-directory-structure/

この記事によると、Remix Viteでは.serverディレクトリを使ってloaderとactionを管理できるようです。これは便利だと思い、記事の通りに.serverディレクトリにloaderとactionを管理するようにしました。しかし、ここで再びこんな疑問が湧いてきました。

.serverディレクトリとRouteファイルが離れていて関係性が把握しにくい...?

Remix自体がCollocationを意識したフレームワークかなと感じていて、もう少しRouteファイルの近くに置けないかなというちょっとしたモヤっと感を解消するため、試行錯誤しました。

この経緯から筆者が考えたディレクトリ構成を今回ご紹介します。

Collocationを意識したディレクトリ構成

以下のようなディレクトリ構成にしました。

app/
├── routes/
│   ├── _index.tsx
│   ├── contacts.$contactId/
│   │   ├── index.tsx
│   │   ├── loader.ts
│   │   └── action.ts
│   ├── contacts.$contactId_.edit/
│   │   ├── index.tsx
│   │   ├── loader.ts
│   │   └── action.ts
│   └── ...
├── components/
...

ポイントとなるのはルーティングのエントリーポイントを、ファイル名ではなくディレクトリ名で宣言しているということです。そして、それぞれのRouteディレクトリ以下にUI部分を示すindex.tsxloader.ts action.tsを配置するようにしました。こうすることにより、actionとloaderを別ファイルで管理しつつも、どのRouteに紐づくのかが一目で分かるようになります。

loaderとactionの使用例

loaderとactionの使用例が以下になります。loader.tsaction.tsで宣言した関数をindex.tsxでインポートし、loaderactionとして使用できます。非常にシンプルですね。

loader.ts
import { LoaderFunctionArgs, json } from "@remix-run/node";

export const awesomeloader = async ({ params }: LoaderFunctionArgs) => {
  // 省略
  return json({ contact });
};
action.ts
import { ActionFunctionArgs, redirect } from "@remix-run/node";

export const awesomeAction = async ({
  params,
  request,
}: ActionFunctionArgs) => {
  // 省略
  return redirect("/");
};
index.tsx
import { Form, useLoaderData } from "@remix-run/react";
import { awesomeloader } from "./loader";
import { awesomeAction } from "./action";

export const loader = awesomeloader;
export const action = awesomeAction;

export default function Index() {
  const { contact } = useLoaderData<typeof loader>();

  return (
    <div id="contact">
          {/** 省略 */}
        <div>
          <Form method="post">
            <button type="submit">Edit</button>
          </Form>
      </div>
          {/** 省略 */}
    </div>
  );
}

一つのRouteで複数のactionを持つ場合

Remixのチュートリアルでは、一つのRouteで複数のactionを持つ場合は、actionのみを持つRouteを追加する方法が紹介されていました。しかし、上記のディレクトリ構成の場合、再びRouteとactionの紐付きが分かりにくくなってしまうため、以下のような方法を取りました。

index.tsx
export default function Index() {
  // 省略

  return (
    <div id="contact">
          {/** 省略 */}
        <div>
          <Form method="post">
            <button type="submit" name="_action" value="edit">
             Edit
            </button>
          </Form>
          <Form method="post">
            <button type="submit" name="_action" value="delete">
              Delete
            </button>
          </Form>
      </div>
          {/** 省略 */}
    </div>
  );
}
action.ts
import { ActionFunctionArgs } from "@remix-run/node";

export const awesomeAction = async ({
  params,
  request,
}: ActionFunctionArgs) => {
  // 省略
  const formData = await request.formData();
  const { _action } = Object.fromEntries(formData);

  switch (_action) {
    case "edit": {
      // edit時の処理
    }
    case "delete": {
      // delete時の処理
    }
  }
};

actionとしては一個として管理し、action.ts内で条件分岐させることによって対応させています。Remixの公式でも紹介されているため、詳しくは以下の動画をご確認ください。

https://www.youtube.com/watch?v=w2i-9cYxSdc&list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6

最後に

今回はactionとloaderを別ファイルで管理しつつも、Collocationを意識したディレクトリ構成をご紹介しました。ディレクトリ構成はRemixに限らず、エンジニアとして常に向き合う課題です。正解はなく、プロジェクトやチームのニーズに応じて最適な構成は異なると思います。ぜひ皆さんのご意見をお聞かせください。💬

また、皆さんの渾身のディレクトリ構成も教えていただけると、筆者はとても喜びます!🙌

最後までお読みいただき、ありがとうございました!!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion