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つの入力フォームに対してバリデーションチェックを行っています。
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;
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コードの処理は同じですが、変数等の名前を変えています。
フォームのNameとEmailに適切に入力されると「Submit」ボタンが押せるようになり、ボタン押下後は、ログを出力してフォームをクリアします。
フォームがクリアされたため、「Submit」ボタンはまた非活性(disabled)になります。
4.まとめ
- カスタムフック(Custom Hook)をuseXXXXで独自に作成することができる
- 同じような処理を1つのカスタムフックにまとめることができる
- component内にimportでカスタムフックを呼び出すことができる