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でカスタムフックを呼び出すことができる

