Pathee engineering blog

世界をしなやかに変えるエンジニアたちのブログ

How to use React Hook Form at Next.js | Pathee Advent Calendar 2021 Day 25

これはPathee Advent Calendarの25日目の記事です

こんにちは。土田です。
気持ちをリフレッシュさせるために、タイトルを英語にしてみました! でも全然リフレッシュしなかったあげく、5日目の予定だった記事を25日目にしてしまいました!! 圧倒的敗北、無念!!!

・・・今回はSTORECAST Managerで採用したReact Hook Formについて、Next.js基準で利用方法を書いていきます。

Formライブラリがなぜ必要か?

「Form値の管理が面倒臭いからです!」

身も蓋もないですが、Formライブラリの導入を考えたのはこの理由です。
今回はReac Hook Formを利用してみることにしました。

導入

これも導入が大変だったかな?と思い、今回のお題にしたのですが、導入自体はyarn add react-hook-formで済んでいました。

なので、いきなりタイトル詐欺告発になるのですが、Next.jsだからといって苦労したところはないかもしれません。。。

とはいっても実際どうなのかはなんともなので、Next.jsで実装する際の流れを書いていきたいと思います。

React Hook Formの利用バージョンは7.22.3です

実装フロー

Formの実装は以下のように行うと良さそうでした。

  1. Formのtypeを宣言
  2. 初期値の設定処理を実装
  3. useFormの定義
  4. 各formの作成
  5. handleSubmitの実装
  6. エラー処理
  7. その他諸々

順に追っていきます。

1. Formのtypeを宣言

TypeScript前提なのでformの型を宣言しておきます(JSだと不要です)。

利用するformの属性値を型と一緒に宣言してください。 属性の型にはstring、number、DataのようにTypeScript標準の型を利用するのが安定です。 もし独自の型を設定したい場合はNestedValue<利用したい型>のように、NestedValueを利用してください。

ただ、サンプルのコメントにもありますが、input typeがdateのフィールドでも値はstringで扱われるため、NestedValueは利用せずに実装できる気がします

type SampleFormType = {
    id: number;
    note: string;
    nankanoPeriod: string; // Date系の型はinput type="date"で利用できない
  };

2. 初期値の設定処理を実装

1で宣言した型の変数を定義します(以下の例だとformDefaultValues)。
大体がAPIのレスポンスを受け取ってForm値を初期設定することになると思うので、設定処理を関数化しておくと、submit後の再描画にも利用できて便利です。

const [formSample, setFormSample] = useState<ServerResponseType>(sampleData);

// サーバから返却されたデータをForm値に変換する
const getFormData = (data: ServerResponseType): SampleFormType => {
  return {
    id: data.id,
    note: data.note,
    nankanoPeriod: data.nankanoPeriod ?
      dayjs(data.nankanoPeriod).format('YYYY-MM-DD') : '',
  };
};
const formDefaultValues = getFormData(formSample);

3. useFormの定義

useFormはいろいろなパラメータがあるので、ドキュメントを参考にしながら実装してください。
※ドキュメントへのリンクは日本語、英語の両方を載せておきます

例えば以下のように定義します。

const {
  register,
  setValue,
  watch,
  handleSubmit,
  formState: { errors, isSubmitting, isSubmitSuccessful, dirtyFields },
  reset,
} = useForm<SampleFormType>({ mode: 'onBlur', defaultValues: formDefaultValues });

formの値を取得するにはwatchとgetValuesが利用できますが、getValuesではblurのタイミングを検知しない時があるので、watchを使っています。 watchで処理が重い場合はgetValuesに切り替えてみるのもいいかもしれません。

4. 各formの作成

useFormの各変数を作成後は、registerなどを使って各formを実装していきます。

ただ、現状useFormに用意されているdirtyFieldsだけでは、変更後に同じ値に戻した場合などを判断できないので、 formの編集状態をチェックするためには以下のような独自実装が必要です。

const checkDirty = (before, after) => {
  if (before) {
    return before != after ? true : false;
  } else {
    return after ? true : false;
  }
};

const isDirtyForm = (name: keyof SampleFormType): boolean => {
  return dirtyFields[name] && checkDirty(sampleData[name], watch(name));
};

const isDirtyNote = isDirtyForm('note');

〜〜〜

// JSX部分
<TextareaInput
  name="note"
  label="備考"
  isDirty={isDirtyNote}
  errorMessage={errors?.note?.message}
  register={register}
/>

例で載せているTextareaInputについてはちょっと説明が難しいので、Componentをまるまる載せておきます(CoreUIを利用するサンプルにもなるかもしれません)。

import { CFormFeedback, CFormLabel, CFormTextarea } from '@coreui/react';
import { UseFormRegister } from 'react-hook-form';

interface Props {
  name: string;
  label: string;
  placeholder?: string;
  required?: boolean;
  disabled?: boolean;
  isDirty?: boolean;
  errorMessage?: string;
  register: UseFormRegister<any>;
}

export const TextareaInput: React.FC<Props> = ({
  name,
  label,
  placeholder,
  required,
  disabled,
  isDirty,
  errorMessage,
  register,
}) => {
  const validCondition = required ? { required: '必須項目です' } : {};

  return (
    <div className="mb-3">
      <CFormLabel>{label}</CFormLabel>
      {!disabled ? (
        <CFormTextarea
          placeholder={placeholder}
          required={required}
          invalid={errorMessage ? true : false}
          valid={isDirty}
          {...register(name, validCondition)}
        />
      ) : (
        <CFormTextarea placeholder={placeholder} required={required} disabled={disabled} {...register(name)} />
      )}
      <CFormFeedback invalid>{errorMessage}</CFormFeedback>
    </div>
  );
};

5. handleSubmitの実装

formタグのonSubmitに設定する関数(ここではupdateSample)をhandleSubmitを使って実装します。

今回は実施していませんが、項目の相関チェックやサーバから返却されるWarningメッセージの設定などもこのタイミングで行うのが良いかと思います。

const updateSample = handleSubmit(async (data: SampleFormType) => {
  // TODO: クライアント側で項目の相関チェックを行う場合はここに入れてください

  const headers = {
    'Content-Type': 'application/json',
  };
  const body = { sample: data };
  const res = await fetch(`/api/samples/${data.id}`, {
    method: 'PUT',
    headers: headers,
    body: JSON.stringify(body),
  });

  const result: ApiResponseType<ServerResponseType> = await res.json();
  if (res.ok) {
    // form値を更新し、画面をリセット
    reset(getFormData(result.data));
    setServerErrors([]); // TODO: Warningメッセージなどを利用する場合は、別途処理を作ってください
  } else {
    // エラー処理
    setServerErrors(result.errors);
  }
});

6. エラー処理

5に若干含まれていますが、必要なレベルでエラー処理を実装します。 例えば返却されたエラーメッセージを画面に表示する、エラー項目をマークするなどです。

Submitがうまく行った場合の挙動も、エラー処理と一緒に考えて実装するのが良いかと思います。 例えば、保存しましたメッセージを表示するためには、以下のようにしてuseStateの変数に値を設定します。

useEffect(() => {
  if (isSubmitting) {
    setMessage(null);
    return;
  }
  if (isSubmitSuccessful && !serverErrors.length) {
    setMessage('保存しました。');
  }
}, [isSubmitSuccessful, isSubmitting]);

7. その他諸々

あとはスタイルの調整やらなんやら、画面に合わせた処理を行って終了です。

ちなみに今回、4でフィールドの編集状態(isDirtyNote)をわざわざ個別に定義したのは、 form全体の編集状況でSubmitボタンを制御するように実装していたからです。

まとめ

ざっくりですがNext.jsのアプリケーションでReact Hook Formを使ったform実装の流れは以上になります。

若干痒いところに手が届かなかったりしましたが、とても便利なライブラリだったので、 もっと利用例が出てくると嬉しいなと思い、記事のネタにしました。

まだエラー側の例が弱いので、かっこいいサンプルが上がってくるのをほんのり期待して、今年のAdvent Calendarの終わりにしたいと思います。

メリークリスマス!