Next.jsでフォームの値を復元するには


React RouterとNext.jsの違い

次のようなフォームの機能を考える。

  1. フォーム入力
  2. フォーム確認画面
  3. フォーム入力に戻る。このとき入力したものは残る。

2→3においてReact Routerを使う場合はlocation.stateに入力した値を保存して取りまわせるが、Next.jsでは類似の機能がないのでクエリパラメータに格納する。

以下、概要のコード:

カスタムフック

useQueryState.ts
const hasKeys = (object: Record<string, unknown>, keys: string[]) => {
  for (const key of keys) {
    const valid = Object.prototype.hasOwnProperty.call(object, key);
    if (!valid) return false;
  }

  return true;
};

/**
 * Set initial state from query object.
 *
 * @param defaultState Default state when empty or invalid query.
 */
const useQueryState = <T extends Record<string, string>>(defaultState: T) => {
  const { query } = useRouter();
  const [values, setValues] = useState<T>(defaultState);

  useEffect(() => {
    if (hasKeys(query, Object.keys(defaultState))) {
      setValues(query as T);
    }
  }, [defaultState, query]);

  return [values, setValues] as const;
};

export default useQueryState;

stateをrouter.queryと紐付ける。指定のキーがあればそれを適用、なければデフォルト値にセット。

フォーム

type Values = {
  username: string;
  comment: string;
};

const initialValues: Values = {
  username: "",
  comment: "",
};

const Home: NextPage = () => {
  const router = useRouter();
  const [values, setValues] = useQueryState(initialValues);

  const handleChange = (e: React.ChangeEvent<any>) => {
    setValues((prevValues) => ({
      ...prevValues,
      [e.target.name]: e.target.value,
    }));
  };

  const handleSubimit = (e: React.FormEvent<any>) => {
    e.preventDefault();

    router.push({
      pathname: "/confirm",
      query: values,
    });
  };

  return (
    <div>
      <Head>
        <title>Post</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h1>Post</h1>

        <form onSubmit={handleSubimit}>
          <div>
            <label htmlFor="username">username</label>
            <input
              type="text"
              id="username"
              name="username"
              value={values.username}
              onChange={handleChange}
            />
          </div>

          <div>
            <label htmlFor="comment">comment</label>
            <input
              type="text"
              id="comment"
              name="comment"
              value={values.comment}
              onChange={handleChange}
            />
          </div>

          <button>Post</button>
        </form>
      </main>
    </div>
  );
};

export default Home;

フォーム確認画面

const Confirm: NextPage = () => {
  const router = useRouter();

  const goBack = () => {
    router.push({
      pathname: "/",
      query: router.query,
    });
  };

  return (
    <div>
      <Head>
        <title>Confirm</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <h1>Confirm</h1>

        <ul>
          <li>username: {router.query.username}</li>
          <li>comment: {router.query.username}</li>
        </ul>

        <button onClick={goBack}>Back</button>
      </main>
    </div>
  );
};

export default Confirm;