💡

Remixでフォームを扱う(Remix Validated Form)

2022/08/12に公開

0. Remixの準備

以下のコマンドを実行してRemixのアプリを起動

https://remix.run/docs/en/v1/tutorials/blog#creating-the-project

$ npx create-remix --template remix-run/indie-stack blog-tutorial

Remixでのインストール可能なNode.jsのバージョン

⠋ 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を実行して以下のような画面が表示されたら準備完了です。

Remixでの初期表示画面

※ 今回使用するパッケージについては執筆時点(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を使用していきます。

https://www.remix-validated-form.io/

Remix Validated Form以外で、Reactでフォームを扱う際の選定の場合は以下の記事が参考になります。

https://dev.to/pmbanugo/looking-for-the-best-react-form-library-in-2021-it-s-probably-on-this-list-e2h

Remix Validated Formの選定理由

    1. 利用率が高い

Remixを利用しているユーザーの約半数ほどがRemix Validated Formを利用している。

https://npmtrends.com/remix-vs-remix-validated-form

Remixの利用率とremix-validated-formの利用率の割合
(2022年8月時点)

参考までにReactの主要ライブラリと比較すると以下のようになります。

https://npmtrends.com/final-form-vs-formik-vs-react-hook-form-vs-remix-validated-form

Remixの利用率とremix-validated-formの利用率の割合
(2022年8月時点)

    1. Remixとの相性がよい

以下のことに対応しているため、Remixで何か行いたいときにも対応してくれそう

https://www.remix-validated-form.io/

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

https://github.com/jquense/yup

https://github.com/airjp73/remix-validated-form/tree/main/packages/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の方を使用したくない場合は以下のようなイメージで設定をすることもできます。

https://bobbyhadz.com/blog/react-set-default-value-of-input

ページ側

詳細を開く

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>
  )
}

参考リンク

https://www.remix-validated-form.io/integrate-your-components

https://www.remix-validated-form.io/default-values

https://www.remix-validated-form.io/repeated-field-names

https://www.remix-validated-form.io/use-outside-forms

3. APIについて

<ValidatedForm />

https://www.remix-validated-form.io/reference/validated-form

validator

フォームが正しく入力されているかのValidatorオブジェクトを使用して、送信ボタンが押されたときなどに形式が正しく入力されているか検証を行う時に使用。

defaultValues

フォームの初期値を設定したい場合に使用。

resetAfterSubmit

フォームの送信が完了した後にデフォルトの値にリセットするか。

onSubmit

フォームが送信される時に行いたい関数を記載。

disableFocusOnError

バリデーション失敗時に、最初のエラーのフォームに行きますが、falseに設定することで、エラーのフォームに行くのを防ぐか。

useField

https://www.remix-validated-form.io/reference/use-field

error

バリデーションのエラーメッセージがある場合、エラーメッセージの表示。

getInputProps

入力のバリデーションに必要なすべてのプロップを取得するために使用。validationBehaviorオプションに基づいたバリデーション動作を自動的に設定。

https://www.remix-validated-form.io/reference/use-field#validationBehavior

touched

フォームがタッチされた場合、trueに設定。

4. バリデーションライブラリと併用する

yupとzod自体の比較については以下の記事が参考になると思います。

https://blog.logrocket.com/comparing-schema-validation-libraries-zod-vs-yup/

https://www.libhunt.com/compare-yup-vs-zod

yupの場合

1. パッケージのインストール

$ npm i @remix-validated-form/with-yup
// or
$ yarn add @remix-validated-form/with-yup

https://www.npmjs.com/package/@remix-validated-form/with-yup

2. withYupの処理について

https://github.com/airjp73/remix-validated-form/blob/main/packages/with-yup/src/index.ts

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自体の使い方について以下を参照してください。

https://github.com/jquense/yup

zodの場合

1. パッケージのインストール

$ npm i @remix-validated-form/with-zod
// or
$ yarn add @remix-validated-form/with-zod

https://www.npmjs.com/package/@remix-validated-form/with-zod

2. withZodの処理について

https://github.com/airjp73/remix-validated-form/blob/main/packages/with-zod/src/index.ts

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自体の使い方について以下を参照してください。

https://github.com/colinhacks/zod

5. JavaScript無効の状態でフォームを動かす

Rimixの場合以下の方法を用いることでJavaScriptの出力を無効にすることができます。

https://remix.run/docs/en/v1/guides/disabling-javascript

Remix Validated FormでもRemixでJavaScriptを無効にした場合の方法も記載されているため、参考にしてください。

https://www.remix-validated-form.io/supporting-no-js

最後に

Remixではデフォルトで<Form>のサポートはありますが、Remix Validated Formのライブラリを使用することでより簡単にフォームを扱うことができるため、ぜひご利用ください。

Discussion