4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

以下の方法でactionごとに処理の記載が可能

  1. actionのみのrouteを用意
  2. fetcherを使用したformで1のrouteにポスト
  3. fetcher.dataから戻り値を取得して画面に反映

困りがちな事例

ブログを例に考えてみた場合、記事にコメントを行う際に以下の処理を行うものとします

  1. 記事に"いいね"をつける
  2. 記事にコメントを付ける

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

// 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を追加します

// 記事のroute
  articles.tsx

// いいねのroute
+ good.tsx

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

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

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

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

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

// 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を監視して変更時にいいねを反映するなり、コメント一覧を更新するなりで完了です

// 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>
   );
}

さいごに

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

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?