【React × TypeScript】フロントバリデージョンを実装してみた

Reactを使ってフロントバリデージョンを実装する機会があったので、アウトプットします。

## 今回の目的
入力された値を検証し、エラーメッセージを表示させる関数を作成すること

## 目次

 1. 送信フォームを作成
 2. 値を検証する関数を作成 
 3. 送信ボタンをクリックできなくする
 4. カスタムフック化

## 1. 送信フォームを作成
今回は、以下のフォームをベースに実装しております。

- 入力されたフォームの値を管理するStateを作成
- 入力された時、値が更新されるonchangeイベントを実装
- 送信ボタンをクリックした時、入力している値がアラートされる
- CSSフレームワークとして、chakra-uiを採用

<img width="1136" alt="送信フォームベース" src="https://user-images.githubusercontent.com/64491435/134503671-42d81613-18e6-40b0-929b-30299557dadb.png">


```jsx:App.tsx
import React, { ChangeEvent, useState } from "react";
import { ChakraProvider, Input, Button, Box } from "@chakra-ui/react" ;

export default function App() {
  const [inputPost, setInputPost] = useState<string>("")

  const onChangePost = (e: ChangeEvent<HTMLInputElement>) => {
    const post = e.target.value;
    setInputPost(post)
  }

  const onClickSubmit = () => {
    alert(inputPost)
  }

  return (
    <ChakraProvider>
      <Box m={16}>
        <Input value={inputPost} onChange={onChangePost}  w="sm" />
        <Button onClick={onClickSubmit} bg="blue.300" color="white">送信する</Button>
      </Box>
    </ChakraProvider>
  );
}

```



これで、入力した値がアラートされるフォームができました。

ただ、現状では以下の問題点があります。

- 空文字でも送信できる
- 文字数も無制限に入力できる

なので、これを制限していきたいと思います。

## 2.値を検証する関数を作成

検証用の関数(validateInputPost)を作成して、検証に引っ掛かった場合、エラーメッセージ を表示させます

### 検証内容

今回の検証内容は、以下の通りです。

1. タイトルが空でないか
2. タイトルが文字数を超えていないか(今回は8文字)

上記に引っかかる場合、エラー文字を表示させていきます。

### プログラムの流れ

想定している流れは、以下の通りです。

- onChangeでstateの値が更新される
- 更新された値を検証する
- 値が想定外だった場合、エラーメッセージを返す
- エラーメッセージ がある場合、それを表示する

### 実装

1.エラーを保持するstateを作成

```jsx:App.tsx
const [error, setError] = useState<string>("");
```

2.検証する関数を作成

```jsx:App.tsx
const validateInputPost = (inputPost: string) => {
    const maxPostLength = 8;
    let error = "";
    //　制限1.空の場合
    if (!inputPost) {
      error = "投稿を入力してください";
    //　制限2.文字数が8文字より多い場合
    } else if (inputPost.length > maxPostLength) {
      error = `投稿は${maxPostLength}文字以内で入力してください`;
    }
    //検証に引っ掛かった時、エラーを保持するstateを更新
    setError(error);
  };
```

3.onCahgeイベント時に、検証する関数を実行

```jsx:App.tsx
const onChangePost = (e: ChangeEvent<HTMLInputElement>) => {
    const post = e.target.value;
    setInputPost(post);
    validateInputPost(post) //これ
  };
```

4.エラーを保持するstateがエラー内容を持っている場合、エラー文字を表示させる

以下のように実装すると、エラー内容を保持している場合のみ、赤文字でエラー文が表示されます。

```jsx:App.tsx

//エラーの場合: errorにエラー内容が入る
//エラーでない場合: ""が入っている = 表示されない 

  { error && 
　　　<Text color="red">{error}</Text>
　 }

```

全体

```jsx:App.tsx
import React, { ChangeEvent, useState } from "react";
import { ChakraProvider, Input, Button, Box, Text } from "@chakra-ui/react";

export default function App() {
  const [inputPost, setInputPost] = useState<string>("");
  const [error, setError] = useState<string>(""); // 1

  const onChangePost = (e: ChangeEvent<HTMLInputElement>) => {
    const post = e.target.value;
    setInputPost(post);
    validateInputPost(post);   // 3
  };

  const onClickSubmit = () => {
    alert(inputPost);
  };

  // 2
  const validateInputPost = (inputPost: string) => {
    const maxPostLength = 8;
    let error = "";
    if (!inputPost) {
      error = "投稿を入力してください";
    } else if (inputPost.length > maxPostLength) {
      error = `投稿は${maxPostLength}文字以内で入力してください`;
    }
    setError(error);
  };

  return (
    <ChakraProvider>
      <Box m={16}>
        <Input value={inputPost} onChange={onChangePost} w="sm" />
        <Button onClick={onClickSubmit} bg="blue.300" color="white">
          送信する
        </Button>
        {error && <Text color="red">{error}</Text>}  // 4
      </Box>
    </ChakraProvider>
  );
}

```

これで、エラー文字が表示されるようになります。

<img width="1136" alt="エラーメッセージ (空)" src="https://user-images.githubusercontent.com/64491435/134503718-37c107d2-1ea2-45c1-bb85-4b89956e24f2.png">
<img width="1136" alt="エラーメッセージ (文字数制限)" src="https://user-images.githubusercontent.com/64491435/134503726-cc086ff2-82f5-4c17-b403-51772fe004b1.png">


## 3.送信ボタンをクリックできなくする

- 現状だと、エラーメッセージ を表示させているだけで送信できてしまいます。

<img width="1136" alt="isDisabled前1" src="https://user-images.githubusercontent.com/64491435/134503772-95f7ea62-6237-4312-a60e-eee10a9e7b1d.png">
<img width="1136" alt="isDisabled前2" src="https://user-images.githubusercontent.com/64491435/134503781-0f40c426-b675-4180-8c34-ede39851be89.png">

- これでは意味がないので、送信ボタンをクリックできないようにします。今回は`isDisabled`を使用します。(trueの場合、ボタンがクリックできなくなります。)

- isDisabledは、trueであればボタンがクリックできなくなります。なので、`error`に文字が入っている場合、trueになるため、送信することができなくなります。

```jsx:App.tsx
<Button onClick={onClickSubmit} isDisabled={error} bg="blue.300" color="white">
  送信する
</Button>
```

これでokです！
ボタンが少し透明になって、クリックできなくなります。

実装後

<img width="1136" alt="isDisabled後1" src="https://user-images.githubusercontent.com/64491435/134503818-61ba3d2f-7255-46de-b0ae-6ddd0a8f6a2e.png">
<img width="1136" alt="isDisabled後2" src="https://user-images.githubusercontent.com/64491435/134503825-f83b296f-9aba-42e1-b93f-17ef81d3d945.png">




- 最終コード

```jsx:App.tsx
import React, { ChangeEvent, useState } from "react";
import { ChakraProvider, Input, Button, Box, Text } from "@chakra-ui/react";

export default function App() {
  const [inputPost, setInputPost] = useState<string>("");
  const [error, setError] = useState<string>("");

  const onChangePost = (e: ChangeEvent<HTMLInputElement>) => {
    const post = e.target.value;
    setInputPost(post);
    validateInputPost(post);
  };

  const onClickSubmit = () => {
    alert(inputPost);
  };

  const validateInputPost = (inputPost: string) => {
    const maxPostLength = 8;
    let error = "";
    if (!inputPost) {
      error = "投稿を入力してください";
    } else if (inputPost.length > maxPostLength) {
      error = `投稿は${maxPostLength}文字以内で入力してください`;
    }
    setError(error);
  };

  return (
    <ChakraProvider>
      <Box m={16}>
        <Input value={inputPost} onChange={onChangePost} w="sm" />
        <Button onClick={onClickSubmit} isDisabled={error} bg="blue.300" color="white">
          送信する
        </Button>
        {error && <Text color="red">{error}</Text>}
      </Box>
    </ChakraProvider>
  );
}
```

## 4.カスタムフック化

最後に、検証用の関数をカスタムフックとして切り出します



```jsx:App.tsx
import React, { ChangeEvent, useState } from "react";
import { ChakraProvider, Input, Button, Box, Text } from "@chakra-ui/react";

import { usePostValidate } from "./useValidate";

export default function App() {
  const [inputPost, setInputPost] = useState<string>("");
  const { error, validateInputPost } = usePostValidate();

  const onChangePost = (e: ChangeEvent<HTMLInputElement>) => {
    const post = e.target.value;
    setInputPost(post);
    validateInputPost(post);
  };

  const onClickSubmit = () => {
    alert(inputPost);
  };

  return (
    <ChakraProvider>
      <Box m={16}>
        <Input value={inputPost} onChange={onChangePost} w="sm" />
        <Button
          onClick={onClickSubmit}
          isDisabled={error}
          bg="blue.300"
          color="white"
        >
          送信する
        </Button>
        {error && <Text color="red">{error}</Text>}
      </Box>
    </ChakraProvider>
  );
}
```

```jsx:useValidate.ts
import { useState } from "react";

export const usePostValidate = () => {
  const [error, setError] = useState<string>("");

  const validateInputPost = (inputPost: string) => {
    const maxPostLength = 8;
    let error = "";
    if (!inputPost) {
      error = "投稿を入力してください";
    } else if (inputPost.length > maxPostLength) {
      error = `投稿は${maxPostLength}文字以内で入力してください`;
    }
    setError(error);
  };
  return { validateInputPost, error };
};
```

同じ挙動になるとOKです。

以上になります。

## 参考資料
- [Form Control](https://chakra-ui.com/docs/form/form-control#usage-with-form-libraries)