React カスタムフック(Custom Hook)の使い方を理解する #7

JS

React カスタムフック(Custom Hook)の使い方について解説します。

Reactでは独自のhook(関数)を作ることができます。

カスタムフック(Custom Hook)を作ることで、複数の箇所で再利用することができます。

カスタムフックを利用する前に、1つの入力フィールドを用意した場合、2つの入力フィールドを用意した場合とで、それぞれバリデーションチェックを行うコードを見ていきます。

カスタムフックの作り方と、それを利用する方法を紹介し、利用前と後の違いを見ていきます。

1.React カスタムフック(Custom Hook)を使う前の処理

入力フィールドを1つ用意したcomponentを用意します。

InputField1.js

import { useState } from 'react';

const InputField1 = (props) => {
  const [enteredName, setEnteredName] = useState('');
  const [enteredNameTouched, setEnteredNameTouched] = useState(false);
  const enteredNameIsValid = enteredName.trim() !== '';
  const nameInputIsInvalid = !enteredNameIsValid && enteredNameTouched;

  const nameInputChangeHandler = (event) => {
    setEnteredName(event.target.value);
  };

  const nameInputBlurHandler = event => {
    setEnteredNameTouched(true);
  };

  const formSubmissionHandler = (event) => {
    event.preventDefault();
    setEnteredNameTouched(true);
    if (!enteredNameIsValid) {
      return;
    }
    console.log(enteredName);
    setEnteredName('');
    setEnteredNameTouched(false);
  };

  const nameInputClasses = nameInputIsInvalid ? 'form-control invalid' : 'form-control';

  return (
    <form onSubmit={formSubmissionHandler}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Name</label>
        <input
          type='text'
          id='name'
          onChange={nameInputChangeHandler}
          onBlur={nameInputBlurHandler}
          value={enteredName}
        />
        {nameInputIsInvalid && (
          <p className='error-text'>Name must not be empty.</p>
        )}
      </div>
      <div className='form-actions'>
        <button>Submit</button>
      </div>
    </form>
  );
};

export default InputField1;

1つの入力フォームに対してバリデーションチェックを行っています。

inputField1

Nameフィールドが未入力の状態では「Submit」ボタンを押せないようになっています。
一度、カーソルをNameフィールドに当てて、未入力のままフォーカスが外れるとエラーメッセージが表示されます。
onBlurにて、入力フォームにフォーカスされたかを検知して、enteredNameTouchedでtrue/falseの状態管理をしています。

Nameフィールドに文字を入力して、「Submit」ボタンを押すとエラーは出ずに、consoleにログが出力されます。

1つの入力フォームであれば、そこまでコードは煩雑ではないかと思います。
これに対して、もう1つ別の入力フォームを追加して同様にバリデーションチェックを行おうとする場合の例が以下になります。

InputField2.js
今度は、Emailの入力フィールドを追加しました。

import { useState } from 'react';

const InputField2 = (props) => {
  const [enteredName, setEnteredName] = useState('');
  const [enteredNameTouched, setEnteredNameTouched] = useState(false);
  const [enteredEmail, setEnteredEmail] = useState('');
  const [enteredEmailTouched, setEnteredEmailTouched] = useState(false);
  const enteredNameIsValid = enteredName.trim() !== '';
  const nameInputIsInvalid = !enteredNameIsValid && enteredNameTouched;
  const enteredEmailIsValid = enteredEmail.includes('@');
  const enteredEmailIsInvalid = !enteredEmailIsValid && enteredEmailTouched;
  let formIsValid = false;

  if (enteredNameIsValid && enteredEmailIsValid) {
    formIsValid = true;
  }

  const nameInputChangeHandler = (event) => {
    setEnteredName(event.target.value);
  };

  const emailInputChangeHandler = (event) => {
    setEnteredEmail(event.target.value);
  };

  const nameInputBlurHandler = (event) => {
    setEnteredNameTouched(true);
  };

  const emailInputBlurHandler = (event) => {
    setEnteredEmailTouched(true);
  };

  const formSubmissionHandler = (event) => {
    event.preventDefault();
    setEnteredNameTouched(true);
    if (!enteredNameIsValid) {
      return;
    }
    console.log(enteredName);
    setEnteredName('');
    setEnteredNameTouched(false);
    setEnteredEmail('');
    setEnteredEmailTouched(false);
  };

  const nameInputClasses = nameInputIsInvalid ? 'form-control invalid' : 'form-control';
  const emailInputClasses = enteredEmailIsInvalid ? 'form-control invalid' : 'form-control';

  return (
    <form onSubmit={formSubmissionHandler}>
      <div className={nameInputClasses}>
        <label htmlFor='name'>Name</label>
        <input
          type='text'
          id='name'
          onChange={nameInputChangeHandler}
          onBlur={nameInputBlurHandler}
          value={enteredName}
        />
        {nameInputIsInvalid && (
          <p className='error-text'>Name must not be empty.</p>
        )}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor='email'>Email</label>
        <input
          type='email'
          id='email'
          onChange={emailInputChangeHandler}
          onBlur={emailInputBlurHandler}
          value={enteredEmail}
        />
        {enteredEmailIsInvalid && (
          <p className='error-text'>Please enter a valid email.</p>
        )}
      </div>
      <div className='form-actions'>
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default InputField2;

inputField2

Emailの入力フィールドに対しても、同様にバリデーションチェックを行い、Nameが空、Emailが適切に入力されていない(Emailに@が含んでいない)場合、「Submit」ボタンを押下できないようになります。

フォームに2つの入力フィールドがあり、同じような処理をしているので、コードが長くなり、1つのときより煩雑になることがわかります。

そこで、カスタムフック(Custom Hook)を定義して、上記の処理を簡潔にしていきます。

2.React カスタムフック(Custom Hook)の作成

カスタムフック用にhooksフォルダを作成して、その中にuse-input.jsファイルを作成します。

カスタムフックを作成する際は、先頭にuseをつけます。

hooks/use-input.js

import { useState } from "react";

const useInput = (validateValue) => {
  const [enteredValue, setEnteredValue] = useState("");
  const [isTouch, setIsTouch] = useState(false);
  const valueIsValid = validateValue(enteredValue);
  const hasError = !valueIsValid && isTouch;

  const valueChangeHandler = (event) => {
    setEnteredValue(event.target.value);
  };

  const inputBlurHandler = (event) => {
    setIsTouch(true);
  };

  const reset = () => {
    setEnteredValue('');
    setIsTouch(false);
  }

  return {
    value: enteredValue,
    isValid: valueIsValid,
    hasError,
    valueChangeHandler,
    inputBlurHandler,
    reset
  };
};

export default useInput;

inputField1やinputField2で行っているJSの処理をuseInputに記載しています。

共通処理として使えるように、各変数等の名前を一般的に変えています。

引数にvalidateValueを関数として受け取るようにして、値のバリデーション結果をvalueIsValidにセットします。

reset関数は、フォーム送信後の入力フォームのリセットを行う用になります。

returnには、object {} を返すようにしています。

これにて、useInputを呼び出して、使う準備ができました。

3.React カスタムフック(Custom Hook)を利用後の処理

inputField2をuseInputを用いて書き直した形が以下になります。

SimpleInput.js

import useInput from "../hooks/use-input";

const SimpleInput = (props) => {
  const {
    value: enteredName,
    isValid: enteredNameIsValid,
    hasError: nameInputHasError,
    valueChangeHandler: nameChangeHandler,
    inputBlurHandler: nameBlurHandler,
    reset: resetNameInput
  } = useInput(value => value.trim() !== '');
 
  const {
    value: enteredEmail,
    isValid: enteredEmailIsValid,
    hasError: emailInputHasError,
    valueChangeHandler: emailChangeHandler,
    inputBlurHandler: emailBlurHandler,
    reset: resetEmailInput
  } = useInput(value => value.includes('@'));

  let formIsValid = false;
  if (enteredNameIsValid && enteredEmailIsValid) {
    formIsValid = true;
  }

  const submitHandler = (event) => {
    event.preventDefault();
    if (!enteredNameIsValid || !enteredEmailIsValid) {
      return;
    }
    console.log(enteredName);
    console.log(enteredEmail);
    resetNameInput();
    resetEmailInput();
  };

  const nameInputClasses = nameInputHasError ? "form-control invalid" : "form-control";
  const emailInputClasses = emailInputHasError ? "form-control invalid" : "form-control";

  return (
    <form onSubmit={submitHandler}>
      <div className={nameInputClasses}>
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          onChange={nameChangeHandler}
          onBlur={nameBlurHandler}
          value={enteredName}
        />
        {nameInputHasError && (
          <p className="error-text">name must not be empty.</p>
        )}
      </div>
      <div className={emailInputClasses}>
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          onChange={emailChangeHandler}
          onBlur={emailBlurHandler}
          value={enteredEmail}
        />
        {emailInputHasError && (
          <p className="error-text">Please enter a valid email.</p>
        )}
      </div>
      <div className="form-actions">
        <button disabled={!formIsValid}>Submit</button>
      </div>
    </form>
  );
};

export default SimpleInput;

カスタムフックuseInputは、import文で呼び出せます。

以下の処理は、入力フィールドNameに対して、useInputを使用しています。

 const {
   value: enteredName,
   isValid: enteredNameIsValid,
   hasError: nameInputHasError,
   valueChangeHandler: nameChangeHandler,
   inputBlurHandler: nameBlurHandler,
   reset: resetNameInput
 } = useInput(value => value.trim() !== '');

JavaScriptの分割代入(Destructuring)を利用することで、useInput内で定義している変数や関数を取り出すことができます。

またuseInputにarrow 関数を渡します。

inputField2で書いてたイベント処理はフォームの送信処理(submitHandler)以外は、不要になったため、SampleInputでは記載を消しています。

JSXコードの処理は同じですが、変数等の名前を変えています。

SimpleInput

フォームのNameとEmailに適切に入力されると「Submit」ボタンが押せるようになり、ボタン押下後は、ログを出力してフォームをクリアします。

SimpleInput2

フォームがクリアされたため、「Submit」ボタンはまた非活性(disabled)になります。

 

4.まとめ

  • カスタムフック(Custom Hook)をuseXXXXで独自に作成することができる
  • 同じような処理を1つのカスタムフックにまとめることができる
  • component内にimportでカスタムフックを呼び出すことができる