Remixでfetcher.submitの完了と同時に画面が再読み込みされてしまう問題を解決する
今回、Remix Stacksが登場する以前にnpx create-remix
で作成したプロジェクトをRemix Blues Stackに移行させる過程で発生した問題についての対処法をまとめておく。
TL;DR
問題が発覚するまでの時系列
現在開発中のプロジェクトを立ち上げたときは、json
メソッドをremix
パッケージからインポートしていた。
しかし、remix-blues-stack
のpackage.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_createFileUploadHandler
とunstable_parseMultipartFormData
を呼び出している箇所を*.server.ts
ファイルに切り出す。
// 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.