たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

Remixでfetcher.submitの完了と同時に画面が再読み込みされてしまう問題を解決する

今回、Remix Stacksが登場する以前にnpx create-remixで作成したプロジェクトをRemix Blues Stackに移行させる過程で発生した問題についての対処法をまとめておく。

TL;DR

github.com

問題が発覚するまでの時系列

現在開発中のプロジェクトを立ち上げたときは、jsonメソッドをremixパッケージからインポートしていた。

しかし、remix-blues-stackpackage.jsonを見ていただくとわかるように、remixパッケージはインストールされていない。 代わりに@remix-run/react@remix-run/nodeといった個別のパッケージがインストールされており、これらから必要な関数等をインポートする形式を採用している。

リポジトリを確認してみると、jsonメソッドを@remix-run/nodeからインポートしているようなので、何も考えずにリソースルート内でも@remix-run/nodeからjsonメソッドをインポートするようにした。

// app/routes/api/new.tsx

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

export type ActionData = {
  message: string;
  received: boolean;
};

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const country = formData.get("country");
  if (!country) {
    return json<ActionData>(
      {
        message: "bad request",
        received: false,
      },
      400
    );
  }
  const data: ActionData = {
    message: `your country is ${country.toString()}`,
    received: true,
  };
  return json<ActionData>(data, 400);
};

基本的な実装方法として、UIのルートモジュールに設置したaction関数ではなく、リソースルートに設置したaction関数に対してUIを接続させたい場合はuseFetcherを使っている。

// app/routes/index.tsx
import { useFetcher } from "@remix-run/react";

export default function Index() {
  const fetcher = useFetcher();

  return (
    <main className="relative min-h-screen bg-white sm:flex sm:items-center sm:justify-center">
      <div className="relative sm:pb-16 sm:pt-8">
       <div className="flex flex-col mb-4 items-center">
         <h1 className="text-lg font-bold">Message</h1>
         <p>{ fetcher.data ? fetcher.data.message : "none"}</p>
       </div>
        <div className="flex w-full flex-row justify-center">
          <fetcher.Form method="post" action="/api/new" reloadDocument={false}>
            <input type="hidden" name="country" value={"japan"} />
            <button className="border rounded-md px-2 py-1" type="submit">送信</button>
          </fetcher.Form>
        </div>
      </div>
    </main>
  );
}

しかし、fetcher.Formから送信されたpostメソッドはaction関数に届いており、正常に実行されるものの、return json({}, 200)を実行した直後に画面全体が再リロードされてしまう問題に直面した。

この問題が起きた場合、例えばfetcher.Formを通して送信した直後、全体が再レンダリングされてしまうので、上記の内容だとfetcher.dataを通してmessageを画面上に表示できないなどの不具合が発生する。

解決策

その場合の解決方法として、@remix-run/server-runtimeからjsonメソッドをインポートすることで問題が解決する。

// app/routes/api/new.tsx
import type { ActionFunction } from "@remix-run/node";
import { json } from "@remix-run/server-runtime";

...

ところが、これで解決しない問題も

リソースルートのaction関数内で、unstable_createFileUploadHandler関数を使用した場合、上記の解決方法を用いてjsonをインポートしたとしても再レンダリングが発生してしまう。

export const action: ActionFunction = async ({ request }) => {
  let uploadHandler = unstable_createFileUploadHandler({
    maxFileSize: 5_000_000,
    file: ({ filename }) => filename,
  });
  let form: FormData;
  try {
    form = await unstable_parseMultipartFormData(request, uploadHandler);
  } catch (error) {
    console.error(error);
    throw badRequest<ActionData>({
      formError: "formData() not resolved",
    });
  }
  const dataURL = form.get("dataURL");

解決策として、このunstable_createFileUploadHandlerunstable_parseMultipartFormDataを呼び出している箇所を*.server.tsファイルに切り出す。

github.com

// app/utils/parseMultipleFormData.server.ts
import { unstable_createFileUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";

export async function parseMultipleFormData(request: Request) {
  let uploadHandler = unstable_createFileUploadHandler({
    maxFileSize: 5_000_000,
    file: ({ filename }) => filename,
  });
  let form: FormData;
  try {
    form = await unstable_parseMultipartFormData(request, uploadHandler);
    return form;
  } catch (error) {
    console.error(error);
    return undefined;
  }
}

そして、切り出した関数を該当のaction関数内で呼び出すように修正する。

export const action: ActionFunction = async ({ request }) => {
...
  const form = await parseMultipleFormData(request);
  if (!form) {
    console.error("formData() not resolved");
    throw badRequest<ActionData>({
      formError: "formData() not resolved",
    });
  }
...

まとめ

もし、同じようにRemix Stackに移行をしている最中で、jsonメソッドを全て@remix-run/nodeからインポートするように置き換えてしまっている場合は、まずはリソースルートでのみ、import { json } from “@remix-run/server-runtimeに置き換えることをお勧めする。

次の文に書かれているように、サーバーサイドでしか使用しない処理に関しては*.server.tsもしくは*.server.tsxファイルに切り出して、コンパイラに対してブラウザ向けのビルド時に含めないように明示的に記しておく方が無難だと思われる。

Adding .server to the filename is a hint to the compiler to not worry about this module or its imports when bundling for the browser.

remix.run