ReactとMaterial UIを使って入力検知を実装する

この記事では、`React`と`Material UI`を用いて作った入力フォームにバリデーションチェックを実装する。

### Reactをインストール

下記コマンドを入力して実行し、適切なプロジェクト名を入力し、ReactとJavaScriptを選択します。
```
npm create vite
```
作成されたフォルダに入り、下記コマンドで必要なパッケージをインストールします。node_moduleフォルダが作成されます。

```
npm install
```

### バリデーションを実装する

下記コマンドを実行し、`http://localhost:5173/`にアクセスするとデフォルトの画面が立ち上がります。
```
npm run dev
```
srcフォルダ内のApp.jsxの中身を下記のように不要なものを一旦削除します。
```javascript:App.jsx
function App() {
  return (
    <>
      
    </>
  )
}

export default App
```

App.cssファイルとindex.cssファイル内の内容をすべて削除します。真っ白な画面になります。

ここで、Material UIパッケージをインストールします。ターミナルをもう一つ立ち上げて、Reactフォルダ内で、Material UIのドキュメントを参考にして下記コマンドを入力し実行します。

https://mui.com/material-ui/getting-started/installation/

```
npm install @mui/material @emotion/react @emotion/styled
```

次に、MaterialドキュメントのTextFieldに関連するページを参考にして、App.jsxにTextFieldを使ってみます。

```javascript:App.jsx
import TextField from '@mui/material/TextField'

function App() {
  return (
    <>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>username: </p>
        <TextField
          error={true}                          // textFieldが赤くなります
          helperText="5~10文字入力してください"   // 文字が表示されます
          size='small'
        />
      </div>
    </>
  )
}

export default App
```

画面が下記のようになります。`<TextField>`のerror属性がtrueの場合、入力欄が赤色になります。また、helperText属性に文字が存在する場合も、ちゃんと表示してくれるようです。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3532036/3406c1ad-e416-e0e2-90bc-5edd34a9b5e0.gif)


ということで、入力した内容により\<TextField>の属性を変化させればよさそうです。
同じように、パスワード入力欄とログインボタンを簡単に作ってみます。

```javascript:App.jsx
import TextField from '@mui/material/TextField'

function App() {
  return (
    <div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>username: </p>
        <TextField
          error={true}                            // textFieldが赤くなります
          helperText="5~10文字入力してください"     // 文字が表示されます
          size='small'
        />
      </div>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>password: </p>
        <TextField
          error={true}                            // textFieldが赤くなります
          helperText="8~16文字入力してください"     // 文字が表示されます
          size='small'
        />
      </div>
      <button style={{marginTop: '30px'}}>ログイン</button>
    </div>
  )
}

export default App
```

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3532036/a3c8891f-e8b4-9341-925f-8e2090f82bf3.gif)

次に入力内容に応じて\<TextField>の属性を変化させる部分について考えます。

まずusernameの入力欄に文字が入力されるたびに条件を満たすかを判定したいので、\<TextField>にonChange属性を追加します。関数名をnameHandleChangeとします。

```javascript:App.jsx
<div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
  <p>username: </p>
  <TextField
    error={true}
    helperText="5~10文字入力してください"
    size='small'
    onChange={nameHandleChange}
  />
</div>
```

nameHandleChange関数内では、入力された内容を取得し、正規表現を用いて条件に一致するかを判断します。さらに返されたBoolean型の結果で\<TextField>の属性をコントロールしたいので、この結果をuseStateを使って管理します。

```javascript:App.jsx
const [nameError, setNameError] = useState(false)  // usernameの入力が正しいかを保存する

const nameHandleChange = event => {
  const pattern = /^\w{5,10}$/g                // 5~10文字か
  const res = pattern.test(event.target.value) // event.target.value: textFieldに入力された内容
  setNameError(!res)                           // 結果をnameErrorに保存する
}
```

ここで`setNameError(!res)`において、`!res`をnameErrorに保存しているのは、正規表現の結果がfalseの場合にnameErrorをtrueにしたい為です。

つぎにnameErrorの値によって\<TextField>の表示を変化させたいので、usernameの\<TextField>を下記のように修正します。

```javascript:App.jsx
<div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
  <p>username: </p>
  <TextField
    error={nameError}
    helperText={nameError ? "5~10文字入力してください" : ''}
    size='small'
    onChange={nameHandleChange}
  />
</div>
```

そうすると下記のようになります。

![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3532036/f62e28bb-bce5-850b-801b-c358d90f1b06.gif)

パスワード入力欄も同じように実装します。全体のコードが下記のようになります。

```javascript:App.jsx
import TextField from '@mui/material/TextField'
import { useState } from 'react'

function App() {
  const [nameError, setNameError] = useState(false)  // usernameの入力が正しいかを保存する
  const [passError, setPassError] = useState(false)  // passwordの入力が正しいかを保存する

  const nameHandleChange = event => {
    const pattern = /^\w{5,10}$/g                // 5~10文字か
    const res = pattern.test(event.target.value) // event.target.value: textFieldに入力された内容
    setNameError(!res)                           // 結果をnameErrorに保存する
  }

  const passHandleChange = event => {
    const pattern = /^\w{8,16}$/g                // 8~16文字か
    const res = pattern.test(event.target.value) // event.target.value: textFieldに入力された内容
    setPassError(!res)                           // 結果をpassErrorに保存する
  }

  return (
    <div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>username: </p>
        <TextField
          error={nameError}
          helperText={nameError ? "5~10文字入力してください" : ''}
          size='small'
          onChange={nameHandleChange}
        />
      </div>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>password: </p>
        <TextField
          error={passError}
          helperText={passError ? "8~16文字入力してください" : ''}
          size='small'
          onChange={passHandleChange}
        />
      </div>
      <button style={{marginTop: '30px'}}>ログイン</button>
    </div>
  )
}

export default App
```

最後にログインボタンを押した際、入力形式が正しければ'OK!'と出力し(実際のアプリでは、バックエンドに処理を投げる)、そうでなければ'NO!'と出力するようにコードを追加します。

`<button>`のonClick属性に、handleClick関数を追加します。handleClick関数内では、nameErrorおよびpassErrorの値によって出力を変えます。

```javascript:App.jsx
function App() {

  // ......省略
  
  const handleClick = () => {
    if (!nameError && !passError) {
      console.log('OK!')
    } else {
      console.log('NO!')
    }
  }

  return (
    <div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>

      {/* ......省略 */}

      <button onClick={handleClick} style={{marginTop: '30px'}}>ログイン</button>
    </div>
  )
}
```

これでおおむね完了ですが、nameErrorとpassErrorは初期値としてfalseを設定していますので、最初の画面で何も入力しない状態でも、`!nameError && !passError`がtrueになってしまいます。ここでは、`useRef`フックを使ってTextField内が空かどうかを確認します。

```javascript:App.jsx
import TextField from '@mui/material/TextField'
import { useRef, useState } from 'react'

function App() {
  const usernameRef = useRef('')                     // username入力欄への参照
  const passwordRef = useRef('')                     // password入力欄への参照
  const [nameError, setNameError] = useState(false)  // usernameの入力が正しいかを保存する
  const [passError, setPassError] = useState(false)  // passwordの入力が正しいかを保存する

  const nameHandleChange = event => {
    const pattern = /^\w{5,10}$/g                // 5~10文字か
    const res = pattern.test(event.target.value) // event.target.value: textFieldに入力された内容
    setNameError(!res)                           // 結果をnameErrorに保存する
  }

  const passHandleChange = event => {
    const pattern = /^\w{8,16}$/g                // 8~16文字か
    const res = pattern.test(event.target.value) // event.target.value: textFieldに入力された内容
    setPassError(!res)                           // 結果をpassErrorに保存する
  }

  const handleClick = () => {
    const username = usernameRef.current.value   // usernameRefの現在の値を取得
    const password = passwordRef.current.value   // passwordRefの現在の値を取得
    
    // username, passwordに値が入っていて、かつnameErrorとpassErrorがfalseの場合
    if (username && password && !nameError && !passError) {
      console.log('OK!')
    } else {
      console.log('NO!')
    }
  }

  return (
    <div style={{display: 'flex', flexDirection: 'column', alignItems: 'center'}}>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>username: </p>
        <TextField
          error={nameError}
          helperText={nameError ? "5~10文字入力してください" : ''}
          size='small'
          onChange={nameHandleChange}
          inputRef={usernameRef}           // useRefでusername入力欄への参照
        />
      </div>
      <div style={{margin: '20px', display: 'flex', alignItems: 'center', gap: '20px'}}>
        <p>password: </p>
        <TextField
          error={passError}
          helperText={passError ? "8~16文字入力してください" : ''}
          size='small'
          onChange={passHandleChange}
          inputRef={passwordRef}           // useRefでpassword入力欄への参照
        />
      </div>
      <button onClick={handleClick} style={{marginTop: '30px'}}>ログイン</button>
    </div>
  )
}

export default App
```

これでフロント側でのバリデーションチェック機能を実現できました。