🐢

Jotaiを使って入力フォームのデータをリロード後も保持する

2025/01/25に公開

これは何?

  • 入力フォームのデータをブラウザバックやブラウザリロード時も保持する方法について記載します
  • 環境
    • Next.js 15.1.2(App Router)
    • React 19.0.0
    • Jotai 2.11.0
    • Conform 1.2.2

ユースケース

  • 入力フォームは、入力画面 => 確認画面 => サンクスページとします。
  • サンクスページに遷移したら入力データがクリアされます。
  • サンクスページに移動しない限り、ブラウザを閉じたりリロードしたりブラウザバックしても入力データは保持されます。

実装方針

  • JotaiatomWithStorageを使います。
  • ストレージはローカルストレージを使います。

コード

atom定義

app/lib/atoms/atoms.tsx
import { atomWithStorage } from "jotai/utils";

export const emailAtom = atomWithStorage<string>("emailAtom", "", undefined, {
  getOnInit: true,
});

export const nameAtom = atomWithStorage<string>("nameAtom", "", undefined, {
  getOnInit: true,
});

入力フォーム

app/ui/use-conform/create/form.tsx
"use client";
# ...snip
import { emailAtom, nameAtom } from "@/app/lib/atoms/atoms";
import { useAtom } from "jotai/react";

export default function Form() {
  const form = useFormMetadata();
  const [email] = useField<string>("email");
  const [name] = useField<string>("name");
  const [emailState, setEmail] = useAtom(emailAtom);
  const [nameState, setName] = useAtom(nameAtom);

  return (
    <form id={form.id} onSubmit={form.onSubmit} noValidate>
# ...snip
              <Input
                type="email"
                key={email.key}
                name={email.name}
                defaultValue={emailState || email.value}
                placeholder="sample@example.com"
                onChange={(e) => setEmail(e.target.value)}
              />
# ...snip
              <Input
                type="text"
                key={name.key}
                name={name.name}
                defaultValue={nameState || name.value}
                placeholder="John Doe"
                onChange={(e) => setName(e.target.value)}
              />
# ...snip
    </form>
  );
}

useAtomで定義したステートデータをフォームのdefaultValueに定義し、ステートデータが存在したら初期表示に使います。

サンクスページ

app/thanks/page.tsx
"use client";

import { emailAtom, nameAtom } from "@/app/lib/atoms/atoms";
import { useSetAtom } from "jotai/react";
import { RESET } from "jotai/utils";

export default function Page() {
  useSetAtom(emailAtom)(RESET);
  useSetAtom(nameAtom)(RESET);

  return <p>thanks!</p>;
}

ページ遷移したら入力フォームに設定したatomをローカルストレージから削除します。
もし、汎用的なページ(たとえばデータ一覧ページ)に遷移したときに消したい場合、データ保存に成功したらサーバ側でワンタイムクッキーを定義して、遷移先のページでクッキー判定して削除ロジックを起動させると良さそうです。

以下、ワンタイムクッキーの設定例です:

app/lib/actions.ts
"use server";

import { cookies } from "next/headers";

export async function storeAction() {
  await dataStore();
  (await cookies()).set("successStoreData", "true", { maxAge: 0 });
  revalidatePath("/dashboard/datas");
  redirect("/dashboard/datas");
}

備考

  • 個人的にはローカルストレージよりセッションストレージを使いたかったのですが、うまく動かなかったのでローカルストレージにしてます(似たissue: https://github.com/pmndrs/jotai/issues/2581)
  • 以前はlocation-stateを使用して状態保存していました。状態管理全般にJotaiを使いたかったので、保存のためだけに違うライブラリを使うのもなんだか非効率に思い、location-stateは撤去しました(location-stateのほうが状態保存には軽量でシンプルなので、もしかしたら共存させるかもしれません。)

Discussion