Remixでaction内が煩雑になるのを防ぐには

趣味の開発でremixを利用してwebアプリケーションを作成しています
その際、1ページに複数のformを設置したい場合、actionの中に複数の処理を書かないといけなくなり、ごちゃごちゃしてしまいます😥
単一責任の原則に則り、1action1処理にする方法をメモ

:::note info
以下の方法でactionごとに処理の記載が可能
1. actionのみのrouteを用意
1. `fetcher`を使用したformで1のrouteにポスト
1. `fetcher.data`から戻り値を取得して画面に反映
:::

## 困りがちな事例

ブログを例に考えてみた場合、記事にコメントを行う際に以下の処理を行うものとします
1. 記事に"いいね"をつける
1. 記事にコメントを付ける

上記をコードにしてみると、以下のようにaction内でformのtypeによって分岐することになると思います
これにより、actionの中が煩雑になって見づらくなってしまうのが個人的には結構ツラいです🫨
分岐ごとにserviceクラスへ処理を委譲するのもよいと思いますが、actionを複数にすることで対応することも可能でしたので例を後述します

```tsx
// Article.tsx
export async function action({request}: ActionFunctionArgs) {
        const formData = Object.fromEntries(await request.formData().entries());
        
        if (formData.type === "good-form") {
            // いいねを行う処理
        else if (formData.type === "comment-form") {
            // コメントをつける処理
        }
        ...
}

export default function Article() {
    const [good, setGood] = useState(false)
    return (
        // 記事内容の表示コード
        ...
        // いいねをするフォーム
        <Form id="good" method="post">
            <input name="type" type="hidden" value="good-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <button type="submit" name="good" value={good ? "true" : "false"}>
              いいね
            </button>
        </Form>
        // コメントするフォーム
        <Form id="comment" method="post">
            <input name="type" type="hidden" value="comment-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <input name="content" type="text" value="~~~"/>
            <button type="submit">送信</button>
        </Form>
    );
}

```

## 対策
### 1. actionのみのrouteを用意

困りがちな事例に倣って`routes`の構成を考え、いいねとコメントのrouteを追加します
```diff
// 記事のroute
  articles.tsx

// いいねのroute
+ good.tsx

// コメントのroute
+ comment.tsx
```

それぞれ以下のようなactionのみを定義します

```tsx
// good.tsx
export async function action({request}: ActionFunctionArgs) {
        const formData = Object.fromEntries(await request.formData().entries());
        
        // いいねを行う処理
        ...
}
```

```react
// comment.tsx
export async function action({request}: ActionFunctionArgs) {
        const formData = Object.fromEntries(await request.formData().entries());
        
        // コメントを行う処理
        ...
}
```

### 2. `fetcher`を使用したformで1のrouteにポスト

困りがちな事例のコードに戻り、`Form`を`fetcher(Good|Comment).Form`に変更します
これでページ遷移やURLの書き換えが発生せずに対象のrouteにリクエストが行えます
（通常のFormでのリクエストではuseActionDataでの戻り値取得も不可だったはず、、なのでfetcher使うしかない）

```diff_tsx
// Article.tsx
- export async function action({request}: ActionFunctionArgs) {
-         const formData = Object.fromEntries(await request.formData().entries());
-         
-         if (formData.type === "good-form") {
-             // いいねを行う処理
-         else if (formData.type === "comment-form") {
-             // コメントをつける処理
-         }
-         ...
- }

export default function Article() {
+   const fetcherGood = useFetcher();
+   const fetcherComment = useFetcher();
    const [good, setGood] = useState(false)
    return (
        // 記事内容の表示コード
        ...
        // いいねをするフォーム
+       <fetcherGood.Form id="good" method="post" action="/good">
            <input name="type" type="hidden" value="good-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <button type="submit" name="good" value={good ? "true" : "false"}>
              いいね
            </button>
+       </fetcherGood.Form>
        // コメントするフォーム
+       <fetcherComment.Form id="comment" method="post" action="/comment">
            <input name="type" type="hidden" value="comment-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <input name="content" type="text" value="~~~"/>
            <button type="submit">送信</button>
 +      </fetcherComment.Form>
    );
 }
 ```

### 3. `fetcher.data`から戻り値を取得して画面に反映

さらに上記のコードに`useEffect()`で`fetcher.data`を監視して変更時にいいねを反映するなり、コメント一覧を更新するなりで完了です

```diff_tsx
// Article.tsx
export default function Article() {
   const fetcherGood = useFetcher();
   const fetcherComment = useFetcher();
   const [good, setGood] = useState(false)
+   useEffect(() => {
+     if (fetcherGood.state === "idle" && fetcherGood.data != null) {
+       // いいね戻り値に応じた処理
+       ...
+     }
+   }, [fetcherGood.data]);
+   useEffect(() => {
+     if (fetcherComment.state === "idle" && fetcherComment.data != null) {
+       // コメント戻り値に応じた処理
+       ...
+     }
+   }, [fetcherComment.data]);
    return (
       // 記事内容の表示コード
       ...
       // いいねをするフォーム
       <fetcherGood.Form id="good" method="post" action="/good">
            <input name="type" type="hidden" value="good-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <button type="submit" name="good" value={good ? "true" : "false"}>
              いいね
            </button>
       </fetcherGood.Form>
       // コメントするフォーム
       <fetcherComment.Form id="comment" method="post" action="/comment">
            <input name="type" type="hidden" value="comment-form"/>
            <input name="articleId" type="hidden" value="1"/>
            <input name="content" type="text" value="~~~"/>
            <button type="submit">送信</button>
      </fetcherComment.Form>
   );
}
```

### さいごに

上記のやり方の問題点としてはパスを使ってしまう点ですが、
パス設計をしっかりやれば問題にはならないかな？と思います
ほかに見通しよく作る方法があればまたメモしてみようと思います