はじめに
こんにちは、かずや(@bearone236)と申します!
今回は、React のイベントハンドラに焦点を当て、その機能とイベントハンドラの挙動について詳しく説明します。
React は強力なフレームワークであり、イベントハンドリングもその一部です。しかし、正しく使わないと不思議な挙動が発生することがあります。
この記事では、React のイベントハンドラに関して実際に自分がぶつかってしまった問題とそれを解決する方法について解説します。React と JavaScript の連携についても一緒に考えていきましょう 💪
今回はReactの具体的な説明は省きますが、時間があれば仮想DOMやレンダリングといったReact学習のために必須知識について別ブログにてまとめていきたいと思います!
イベントハンドラとは
一般的に イベントハンドラ とは、イベントが発火した時に実行される (通常はユーザー定義の JavaScript 関数) コードのブロック・関数のことを指しています。
React では、JSX コードにイベントハンドラを記載することが可能であり、クリック、フォームの入力、ウィンド処理、マイス処理といったユーザーインタラクティブに応じてトリガ(実行)されます。
そのためイベントハンドラは 「イベントトリガ」 と言われることもあります。
今回イベントハンドラの関数の受け渡しで意図しない挙動が見られました。こちらに関して深掘りしていきます。どれも開発を行う上で必要になる知識です。ぜひここで覚えていきましょう。
今回の挙動と解決策に関しては下記の React 公式サイトに具体的に記載されていますので、深掘りしたい方はぜひ一読してみてください。
5 つの意図しない挙動たち
【意図しない挙動 1】 useState フックを用いたイベントハンドラ
下記のコードは、ボタンをクリックすることによって、onClick 関数がトリガされ、useState に 1 ずつ count 変数に足されていく予想で記載をしました。
<button onClick={setCount(count + 1)}>Click</button>
こちらを宣言すると、Too many re-renders. React limits the number of renders to prevent an infinite loop.
というエラーが出力されています。 (無限ループ)
こちらはイベントハンドラが直接的な原因とも言われていますが、useState とレンダリングも無限ループ問題に関連しています。
今回は具体的な説明は省きますが、簡単に説明すると、
React はレンダリングする条件の 1 つに「状態が更新された時」と言うものがあり、useState である useCount 関数 が呼び出されるたびに状態が更新され、レンダリングが毎回呼び出されるといった挙動となっています。
【意図しない挙動 2】 コンソールを用いたイベントハンドラ
このコードでは、ボタンをクリックすることによって console.log('test')
が毎回出力されるのではないかという予想で記載をしました。
<button onClick={console.log("test")}>Click</button>
【実行結果】
(何度クリックしても何も表示されない)
コード onClick={console.log("test")}
では、console.log("test")
がコンポーネントのレンダリング時に実行されます。
これは、JavaScript が console.log("test")
を実行し、その戻り値(この場合は undefined)を イベントハンドラ onClick に設定しているために発生する挙動です。
→ そのため実際のボタンクリック時には何も起こりません。
【意図しない挙動 3】 関数に引数を渡した場合のイベントハンドラ
export default function App() {
const e = 0;
function handleChange(e) {
console.log(`test: ${e}`);
}
return (
<div>
<button onClick={handleChange(e)}>Count</button>
</div>
);
}
【実行結果】
(何度クリックしても何も表示されない)
関数に引数を用いて例 2 と同様の処理を行いましたが、上記の同様の出力が見られました。
【正常挙動 4-1】 引数なし関数を用いたイベントハンドラ
export default function App() {
function handleChange() {
console.log("test");
}
return (
<div>
<button onClick={handleChange}>Count</button>
</div>
);
}
このように、ただ関数を定義し JSX 記述の外でトリガさせるような形にする ことによって正常動作が見られます。
挙動 1 での useState フックのコードも同様の形に変更することで正常な動作が可能です。
【意図しない挙動 4-2】 引数あり値なし関数を用いたイベントハンドラ
export default function App() {
function handleChange() {
console.log("test");
}
return (
<div>
<button onClick={handleChange()}>Count</button>
</div>
);
}
【実行結果】
(何度クリックしても何も表示されない)
・しかしながら、上記のように関数に引数を用いると意図しない挙動が見られます。
→ 関数に渡すための引数に問題がある…?
→ 引数ありとなしで違いがあるのでは…?
解決!!
React 公式ドキュメント の「落とし穴」というコーナーに疑問の全てが書かれていました…
【① 関数をイベントハンドラで利用したい時】
関数を渡す (正しい ⭕️) | 関数を呼び出す(間違い ❌ ) | |
---|---|---|
コード | <button onClick={handleClick}> |
<button onClick={handleClick()}> |
処理 | handleClick 関数が onClick イベントハンドラとして渡されている | handleClick()の末尾に空の()が存在している |
詳細 | React にこの関数を覚えてもらい、ユーザーがアクションを行った時にのみコールするように指示している | アクションを必要とせず、レンダーの際にすぐに実行されてしまう |
→ JSX の{}中のコードはすぐに実行される仕様となっている
イベントハンドラに渡す関数は、「渡す」べきであって「呼び出す」べきではない
= 確かに関数を定義する際には、関数宣言もアロー関数も引数を用いて「関数を渡す」処理を書いていた!!
【② インラインコードで直接イベントハンドラを利用したい時】
続いて、「JSX の中にインラインでコードを記載する際」はどのように記載するのか説明します。
関数を渡す (正しい ⭕️) | 関数を呼び出す(間違い ❌) | |
---|---|---|
コード | <button onClick={() ⇒ console.log(’…’)}> |
<button onClick={console.log(’…’)}> |
処理 | アロー関数(無名関数)を用いてインラインコードラップしている | イベントハンドラonClickに直接インラインコードを記載している |
詳細 | レンダーごとに中のコードが実行されているのではなく、後で呼び出されるべき関数を作成したことになる | クリックした時ではなく、コンポーネントがレンダーされるたびに実行されます |
→ useState の場合はずっと state が更新され続ける = 「無限ループ」 の完成
まとめ
1. 関数をインラインではなく JSX の外側で定義する際は、引数不要の「関数を渡す処理」を書く
2. インラインで JSX の中に記載したい際は、「アロー関数での処理」を書く
3. 関数に引数を欲しい際は、アロー関数での定義をする! (下記コード例)
【TypeScript で記載した参考コード】
export const Board: React.FC<BoardProps> = ({ squares, onClick }) => {
const callSquare = (i: number) => {
return <Square value={squares[i]} onClick={() => onClick(i)} />;
};
return (
<div>
<div className="board-row">
{callSquare(0)}
{callSquare(1)}
{callSquare(2)}
</div>
<div className="board-row">
{callSquare(3)}
{callSquare(4)}
{callSquare(5)}
</div>
<div className="board-row">
{callSquare(6)}
{callSquare(7)}
{callSquare(8)}
</div>
</div>
);
};
参考文献