Remixでは簡単にバリデーションできます😎

## はじめに
Webアプリを開発する際は, 入力フォームとそのバリデーションを実装することが非常に多いと思います. 皆さんも必要なプロパティが存在するか, 値が適切な範囲に収まっているか, 目的の型にキャストできるかなどをチェックするコードをたくさん書いてきたのではないでしょうか？🤔


一般的にバリデーションは, **クライアントサイドでのバリデーション**と**サーバーサイドでのバリデーション**とで別々に実装する必要があるため, バリデーションルールを表現するコードがDRYにならないという問題があります. しかも二度手間で面倒です😱

こういった煩雑さから解放させてくれるのが, 今回紹介する[remix-validated-form](https://www.remix-validated-form.io/)というライブラリです✨
`remix-validated-form`は, 最近流行りのフルスタックフレームワークである`Remix`と組み合わせて利用するライブラリであり, クライアントサイドとサーバーサイドでの**バリデーションを一本化**してくれる優れものです🔥
`zod`や`yup`といった有名なバリデーションライブラリと連携して利用することも可能です. `zod`を用いる場合は以下のコマンドでインストールできます. 

```shell
> npm install remix-validated-form @remix-validated-form/with-zod
```

## 使い方
今回は以下のような「新規ユーザーの登録」を例として, `remix-validated-form`の使い方を紹介していきます. (バリデーションには`zod`を使います)
![前提.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/d3302559-3a4e-cfe4-95f0-6d633b3c082e.png)


### バリデーターの作成とフォームの実装
`remix-validated-form`を使うには, まずバリデーターを作成する必要があります. 
```typescript:userRegisterValidator.ts
import { withZod } from "@remix-validated-form/with-zod";
import { z } from "zod";

export const userRegisterValidator = withZod(
  z.object({
    age: z.coerce
      .number({ invalid_type_error: "年齢は数字で入力してください" })
      .safe(),
    email: z
      .string()
      .email({ message: "メールアドレスを正しい形式で入力してください" }),
    name: z.string().min(1, { message: "名前が空になっています" }),
  })
);
```

次に, フォームを実装したいページで`ValidatedForm`コンポーネントを利用します. 
このコンポーネントは`validator`が必須`Prop`になっており, フォームに適用したいバリデーターを渡します. 
これだけで, フォームに入力中のデータに対して`Props`で渡したバリデーターによるクライアントサイドでのバリデーションが実装完了です✨
```tsx:register.tsx
import { Button, Text, VStack } from "@chakra-ui/react"; // ← スタイリングにChakra UIを使っています

import { ValidatedForm } from "remix-validated-form";
import { userRegisterValidator } from "./userRegisterValidator";

...
const RegisterPage = () => {
  return (
    <>
        ...
        <Text fontSize="lg" fontWeight="bold" textDecoration="underline">
          ユーザ登録
        </Text>
        <ValidatedForm
          validator={userRegisterValidator} // ← バリデーターを設定(必須)
          action="/register"
          method="POST"
        >
          <VStack>
            <InputFields /> {/* ← 入力欄をまとめたコンポーネント(ユーザー定義) */}
            <Button type="submit" colorScheme="purple">
              登録する
            </Button>
          </VStack>
        </ValidatedForm>
      </VStack>
    </>
  );
};
export default RegisterPage;
```

フォームから送信されたデータに対するサーバーサイドでのバリデーションは, `validator.validate()`メソッドを実行するだけで簡単に行うことができます. 
```typescript:register.tsx
import { validationError } from 'remix-validated-form'
...

export const action = async ({ request }: ActionArgs) => {
  const formData = await userRegisterValidator.validate(
    await request.formData()
  );
  if (formData.error) {
    console.log(formData.error);
    return validationError(formData.error);
  }
  const { name, age, email } = formData.data; // ← zodでバリデーション済みのデータを簡単に取得できます

  return json({ name, age, email });
};
```
(※) `register.tsx`内の`action`関数はこのエンドポイント（=/register）にリクエストが送信されたときにサーバーサイドで実行される関数です. 


### バリデーションエラーの取得方法
サーバーサイドでのバリデーションエラーは, `formData.error`で簡単に取得できます. 
また, `validationError`関数を`formData.error`に使うことで簡単にエラーレスポンスを生成することができます. 
一方で, クライアントサイドのバリデーションエラーは, `remix-validated-form`で定義されている`useField`というフックを利用することで取得することができます. 
例として, `useField`によるエラーの取得とアラートの表示は以下のように実装できます. 
```tsx:inputFields.tsx
import { Alert, AlertIcon, HStack, Input,vText } from "@chakra-ui/react";

import { useField } from "remix-validated-form";

export const InputFields = () => {
  const nameError = useField("name");
  
  ...
  return (
      ...
      <HStack>
        <Text mr={4}>名前　:</Text>
        <Input
          name="name"
          width="15vw"
          backgroundColor="white"
          border="1px solid black"
        />
      </HStack>
      {nameError.error && ( // ← エラーがある場合は, アラートコンポーネントを表示する
        <Alert status="error" width="20vw">
          <AlertIcon />
          {nameError.error}
        </Alert>
      )}
    ...
  );
};
```

## バリデーション実行例
### クライアントサイド
実際にバリデーションエラーが起きる例として, メールアドレスに不適切な値を入力してみました. 
その結果, ブラウザ上では下の画像のように適切なエラーメッセージが表示されました. 
![csv1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/06405d59-8140-4622-9c13-f8fe878a2a93.png)


一方で, ターミナルで`Remix`サーバの出力を見てもフォーム送信のログが出力されていないことから, クライアントサイドでのバリデーションが実行され, 通過しなかったためにリクエストが送信されていないことが確認できます✨
)![csv2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/8bf5bab2-6dc1-fd46-5dd1-b6b9741414e2.png)

※ちなみに, バリデーションをパスしたデータがフォームによって送信された場合は以下のような出力になります. 
![csv3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/2fe9c2f7-51a8-6f0f-6b8a-5063f6dc7955.png)



### サーバーサイド
サーバーサイドでのバリデーションを確認するため, 先ほどと同じエラーが起きるデータを`curl`を使って送信しました. 
![ssv1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/a0047250-c9b4-d5bb-7972-bfbc10878833.png)

同様にターミナルで`Remix`サーバの出力を確認すると, クライアントサイドで表示されたエラーメッセージと同じエラーが出力されていることがわかります. 
レスポンスのステータスコードは[422](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/422)なのですが, このステータスコードのレスポンスメッセージは`Unprocessable Entity`なので, 送信された内容の理解はできるものの正しく処理できなかったことを表しています！！
![ssv2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3621335/cc5d6e23-44b3-69fd-29a3-861464f43959.png)


## まとめ
バリデーターを一つ定義するだけでクライアントサイドとサーバーサイドの両方で一貫したバリデーションを行うことが`remix-validated-form`を使えば簡単に実現できます. 
また, `zod`や`yup`といった有名なバリデーションライブラリとの連携を想定して作られているので, 利用経験のあるバリデーションライブラリをそのまま使うことができます. 

さらに, クライアントサイドでは`ValidatedForm`にバリデーターを`Props`として渡すだけでフォームにバリデーションを設定でき, `useField`を使って直感的に対象のフィールドのエラーを取得できます. サーバーサイドでは`validator.validate()`を実行するだけでバリデーションを実行できるうえに, `validationError`関数を`formData.error`に使うことで簡単にエラーレスポンスを生成できます. こういった諸々の操作が簡単であることも魅力に感じています. 

`Remix`を使ってWebアプリを開発している方や`Remix`に興味があってこれから触ってみたい方に是非おすすめしたいライブラリです！！
何か不明な点などがあれば, 遠慮なくコメントしてください😊

### 参考文献
- [公式ドキュメント](https://www.remix-validated-form.io/)
- [remix-validated-form](https://www.npmjs.com/package/remix-validated-form)
- [remix-validated-form/with-zod](https://www.npmjs.com/package/@remix-validated-form/with-zod)
- [remix-validated-form/with-yup](https://www.npmjs.com/package/@remix-validated-form/with-yup)
- [ステータスコード422](https://developer.mozilla.org/ja/docs/Web/HTTP/Status/422)