Remixでフォームを扱う(Remix Validated Form)
0. Remixの準備
以下のコマンドを実行してRemixのアプリを起動
$ npx create-remix --template remix-run/indie-stack blog-tutorial

⠋ Creating your app…🚨 There was a problem fetching the file from GitHub. The request responded with a 403 status. Please try again later.
今回は以下のように設定しました。
$ npx create-remix --template remix-run/indie-stack remix-sample
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`?
Yes
インストールが完了したら、npm run devを実行して以下のような画面が表示されたら準備完了です。

※ 今回使用するパッケージについては執筆時点(2022年8月時点)での以下の最新バージョンになります。
- "@remix-run/node": "^1.6.5"
- "@remix-run/react": "^1.6.5"
- "@remix-run/serve": "^1.6.5"
- "@remix-run/server-runtime": "^1.6.5"
- "react": "^18.2.0"
- "react-dom": "^18.2.0"
- "@remix-run/dev": "^1.6.5"
- "@remix-run/eslint-config": "^1.6.5"
- "vite": "^3.0.0"
1. フォームライブラリの追加
今回はRemixと相性がよいRemix Validated Formを使用していきます。
Remix Validated Form以外で、Reactでフォームを扱う際の選定の場合は以下の記事が参考になります。
 Remix Validated Formの選定理由
- 
- 利用率が高い
 
Remixを利用しているユーザーの約半数ほどがRemix Validated Formを利用している。

(2022年8月時点)
参考までにReactの主要ライブラリと比較すると以下のようになります。

(2022年8月時点)
- 
- Remixとの相性がよい
 
以下のことに対応しているため、Remixで何か行いたいときにも対応してくれそう
Client-side, field-by-field and form-level validation
Re-use validation on the server
Set default values for the entire form in one place
Supports nested objects and arrays
Easily detect if a specific form is being sumitted
Validation library agnostic
Can work without JS
インストール
$ npm install remix-validated-form
// or
$ yarn add remix-validated-form
2. 基本的な使い方
今回使い方で使用するパッケージ周りのインストール
今回の例ではバリデーションライブラリをyupで使用していきます。
yupの使い方に4. バリデーションライブラリと併用する - yupの場合で説明します。
$ npm install yup @remix-validated-form/with-yup
// or
$ yarn add yup @remix-validated-form/with-yup
通常のフォーム
ページ側
詳細を開く
import { ValidatedForm } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
type Input = {
  sample: string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required()
  })
);
export default function FormNormal() {
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
    <ValidatedForm validator={schema} onSubmit={(data) => onSubmit(data)}>
      <MyInput name='sample' label="labelName" />
    </ValidatedForm>
  )
}
コンポーネント側
import { useField } from "remix-validated-form";
type MyInputProps = {
  name: string;
  label: string;
};
export const MyInput = ({ name, label }: MyInputProps) => {
  const { error, getInputProps } = useField(name);
  return (
    <div>
      <label htmlFor={name}>{label}</label>
      <input {...getInputProps({ id: name })} />
      {error && (
        <span className="my-error-class">{error}</span>
      )}
    </div>
  );
};
送信ボタン
ページ側
詳細を開く
以下の+の部分が追記した内容になります。
import { ValidatedForm } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
+ import { MySubmitButton } from "~/components/submit-button";
type Input = {
  sample: string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required()
  })
);
export default function FormNormal() {
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
    <ValidatedForm validator={schema} onSubmit={(data) => onSubmit(data)}>
      <MyInput name='sample' label="labelName" />
+      <MySubmitButton />
    </ValidatedForm>
  )
}
コンポーネント側
import { useIsSubmitting } from "remix-validated-form";
export const MySubmitButton = () => {
  const isSubmitting = useIsSubmitting();
  return (
    <button type="submit" disabled={isSubmitting}>
      {isSubmitting ? "Submitting..." : "Submit"}
    </button>
  );
};
初期値の設定
ページ側
defaultValues={{ sample: "InitialValue" }}の追加
import { ValidatedForm } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
import { MySubmitButton } from "~/components/submit-button";
type Input = {
  sample: string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required()
  })
);
export default function FormNormal() {
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
+    <ValidatedForm validator={schema} defaultValues={{ sample: "InitialValue" }} onSubmit={(data) => onSubmit(data)}>
      <MyInput name='sample' label="labelName" />
      <MySubmitButton />
    </ValidatedForm>
  )
}
初期値の設定(コンポーネント側で設定を行いたい場合)
inputタグにdefaultValueを送ることで設定することもできるため、ValidatedFormのdefaultValueの方を使用したくない場合は以下のようなイメージで設定をすることもできます。
ページ側
詳細を開く
MyInputファイルにdefaultValueの値を送れるように設定(+の部分)
import { ValidatedForm } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
import { MySubmitButton } from "~/components/submit-button";
type Input = {
  sample: string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required()
  })
);
export default function FormNormal() {
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
    <ValidatedForm validator={schema} onSubmit={(data) => onSubmit(data)}>
+      <MyInput name='sample' label="labelName" defaultValue="InitialValue2" />
      <MySubmitButton />
    </ValidatedForm>
  )
}
コンポーネント側
import { useField } from "remix-validated-form";
type MyInputProps = {
  name: string;
  label: string;
+  defaultValue?: string
};
export const MyInput = ({ name, label, defaultValue }: MyInputProps) => {
  const { error, getInputProps } = useField(name);
  return (
    <div>
      <label htmlFor={name}>{label}</label>
+       <input {...getInputProps({ id: name })} defaultValue={defaultValue} />
      {error && (
        <span className="my-error-class">{error}</span>
      )}
    </div>
  );
};
繰り返し
ページ側
フォームグループで同じname属性を使用すること(以下の例の場合はmyCheckboxGroup)で複数のチェックボックを選択している場合配列に、1つ選択している場合には文字列としてデータを扱うことができます。
import { ValidatedForm } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
import { MySubmitButton } from "~/components/submit-button";
+ import { Checkbox } from "~/components/checkbox";
+ import { CheckboxGroup } from "~/components/checkbox-group";
type Input = {
  sample: string
+   myCheckboxGroup: string[] | string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required(),
+     myCheckboxGroup: Yup.lazy(val => (Array.isArray(val) ? Yup.array().of(Yup.string()) : Yup.string()).label("CheckBox Array").required())
  })
);
export default function FormNormal() {
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
    <ValidatedForm validator={schema} onSubmit={(data) => onSubmit(data)}>
      <MyInput name='sample' label="labelName" defaultValue="InitialValue2" />
+       <CheckboxGroup name="myCheckboxGroup" label="checkboxGroup">
+         <Checkbox name="myCheckboxGroup" label="value1" value="value1" />
+         <Checkbox name="myCheckboxGroup" label="value2" value="value2" />
+       </CheckboxGroup>
      <MySubmitButton />
    </ValidatedForm>
  )
}
コンポーネント側
チェックボックス全体
import {FC, ReactNode} from 'react'
import { useField } from "remix-validated-form";
export type CheckboxGroupProps = {
  label: string;
  name: string;
  children: ReactNode
};
export const CheckboxGroup: FC<CheckboxGroupProps> = ({
  label,
  name,
  children,
}) => {
  const { error } = useField(name);
  return (
    <fieldset>
      <legend>{label}</legend>
      {children}
      {error && <p className="myErrorClass">{error}</p>}
    </fieldset>
  );
};
チェックボックス単体
import {FC} from 'react'
import { useField } from "remix-validated-form";
export type CheckboxProps = {
  label: string;
  name: string;
  value?: string;
};
export const Checkbox: FC<CheckboxProps> = ({
  label,
  name,
  value,
}) => {
  const { getInputProps } = useField(name);
  return (
    <div>
      <label>
        {label}
        <input
          {...getInputProps({ type: "checkbox", value })}
        />
      </label>
    </div>
  );
};
フォームの外でhooksを使用する
ページ側
+ import { ValidatedForm, useIsSubmitting } from "remix-validated-form";
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
import { MyInput } from "~/components/normal";
import { MySubmitButton } from "~/components/submit-button";
import { Checkbox } from "~/components/checkbox";
import { CheckboxGroup } from "~/components/checkbox-group";
type Input = {
  sample: string
  myCheckboxGroup: string[] | string
}
const schema = withYup(
  Yup.object({
    sample: Yup.string().label("First Name").required(),
    myCheckboxGroup: Yup.lazy(val => (Array.isArray(val) ? Yup.array().of(Yup.string()) : Yup.string()).label("CheckBox Array").required())
  })
);
export default function FormNormal() {
+   const isSubmitting = useIsSubmitting("myForm");
  const onSubmit = (data: Input) => {
    console.log(data);
  };
  return (
+     <ValidatedForm validator={schema} onSubmit={(data) => onSubmit(data)} id="myForm">
      <MyInput name='sample' label="labelName" defaultValue="InitialValue2" />
      <CheckboxGroup name="myCheckboxGroup" label="checkboxGroup">
        <Checkbox name="myCheckboxGroup" label="value1" value="value1" />
        <Checkbox name="myCheckboxGroup" label="value2" value="value2" />
      </CheckboxGroup>
      <MySubmitButton />
+       <button className="block" disabled={isSubmitting}>
+         {isSubmitting ? "Submitting..." : "Submit"}
+       </button>
    </ValidatedForm>
  )
}
参考リンク
3. APIについて
<ValidatedForm />
validator
フォームが正しく入力されているかのValidatorオブジェクトを使用して、送信ボタンが押されたときなどに形式が正しく入力されているか検証を行う時に使用。
defaultValues
フォームの初期値を設定したい場合に使用。
resetAfterSubmit
フォームの送信が完了した後にデフォルトの値にリセットするか。
onSubmit
フォームが送信される時に行いたい関数を記載。
disableFocusOnError
バリデーション失敗時に、最初のエラーのフォームに行きますが、falseに設定することで、エラーのフォームに行くのを防ぐか。
useField
error
バリデーションのエラーメッセージがある場合、エラーメッセージの表示。
getInputProps
入力のバリデーションに必要なすべてのプロップを取得するために使用。validationBehaviorオプションに基づいたバリデーション動作を自動的に設定。
touched
フォームがタッチされた場合、trueに設定。
4. バリデーションライブラリと併用する
yupとzod自体の比較については以下の記事が参考になると思います。
yupの場合
1. パッケージのインストール
$ npm i @remix-validated-form/with-yup
// or
$ yarn add @remix-validated-form/with-yup
2. withYupの処理について
3. 使い方
import { withYup } from "@remix-validated-form/with-yup";
import * as Yup from "yup";
const schema = withYup(
  // withYupの中身についてはyupの使い方参照
  Yup.object({
    sample: Yup.string(),
  })
);
<ValidatedForm validator={schema}>
  {/* 何かのコンテンツ */}
</ValidatedForm>
yup自体の使い方について以下を参照してください。
zodの場合
1. パッケージのインストール
$ npm i @remix-validated-form/with-zod
// or
$ yarn add @remix-validated-form/with-zod
2. withZodの処理について
3. 使い方
import { withZod } from "@remix-validated-form/with-zod";
import { z } from "zod";
const schema = withZod(
  // withZodの中身についてはzodの使い方参照
  z.object({
    sample: z.string(),
  }
);
<ValidatedForm validator={schema}>
  {/* 何かのコンテンツ */}
</ValidatedForm>
zod自体の使い方について以下を参照してください。
5. JavaScript無効の状態でフォームを動かす
Rimixの場合以下の方法を用いることでJavaScriptの出力を無効にすることができます。
Remix Validated FormでもRemixでJavaScriptを無効にした場合の方法も記載されているため、参考にしてください。
最後に
Remixではデフォルトで<Form>のサポートはありますが、Remix Validated Formのライブラリを使用することでより簡単にフォームを扱うことができるため、ぜひご利用ください。


Discussion