はじめに
- この記事ではReactアプリケーションでイベント(onClick、onChangeなど)のprop(コンポーネントの引数)の正しい渡し方について、サンプルを使いながら紹介します
- 初歩的な内容ですが筆者の中で言語化出来ていなかった為、記事としました
この記事の対象者
- 現在Reactを学ばれている方
- これからReactを学ばれる方
動作環境
- react-dom@18.2.0
- react-scripts@5.0.1
- react@18.2.0
この記事で使うサンプル
以下のサンプルは「ボタン1〜3」のどれかがクリックされる度に、「0」の部分がカウントアップされるというものです。
以降この記事で登場するソースコードはこちらのCodeSandbox内のファイル「App.js」にコピペすることで動作を確認することが出来ます。
では本題
サンプルに対して以下の機能を追加実装します。
- ボタンnがクリックされた場合、「ボタンnがクリックされました!」というメッセージを「Hello」と書かれている部分に表示する
( 例:ボタン1がクリックされた場合、「Hello」 → 「ボタン1がクリックされました!」
NGパターン
関数「func1」に引数「n」の追加、 setMessageの処理を追記しました。
それに合わせてbuttonタグのpropには引数の値としてボタン番号をセットしています。
import { useState } from "react";
export const App = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("Hello");
const func1 = (n) => {
setCount(count + 1);
// messageを更新
setMessage(`ボタン${n}がクリックされました!`);
};
return (
<>
<h1>{count}</h1>
<p>{message}</p>
{/* func1に引数としてボタン番号を渡す */}
<button onClick={func1(1)}>ボタン1</button>
<button onClick={func1(2)}>ボタン2</button>
<button onClick={func1(3)}>ボタン3</button>
</>
);
};
一見これで良さそうに思いますが、このソースコードではアプリケーションは動きません。
代わりに、以下のメッセージが表示されます。
このエラーは端的にこう言われています。
↓
React君「これ...無限ループしてるで!!」
上述NGパターンについて、関数「func1」を呼び出している以下の部分に注目してください。
{/* func1に引数としてボタン番号を渡す */}
<button onClick={func1(1)}>ボタン1</button>
ポイントは2つあります。
- 「関数名()」の形式で記述するとコンポーネントのレンダリング時に関数が実行される
- 関数「func1」内ではステート(今回はcountとmessage)の値を変更しており、ステートが変更されるとコンポーネント(今回はApp.js)が再レンダリングされる
つまり、無限ループになる仕組みは以下となります。
- 画面レンダリング(1回目)
- React君『関数「func1(1)」を実行するで〜』
- 関数「func1(1)」実行(1回目)
- React君『countとmessageのステートを変更するで〜』
- ステート変更(1回目)
- React君「ステートが変わった!コンポーネントを再レンダリングするで〜」
- 画面レンダリング(2回目)
- React君『関数「func1(1)」を実行するで〜』
- 関数「func1(1)」実行(2回目)
- React君『countとmessageのステートを変更するで〜』
- ...
- ...
- React君「これ...無限ループしてるで!!」
じゃあどうすれば?(前談)
正解の解説の前に、無限ループしていなかったサンプル初期の呼び出し方と、無限ループしてしまったNGパターンの呼び出し方を振り返って比較してみましょう。
{/* 無限ループしない(サンプル初期の呼び出し) */}
<button onClick={func1}>ボタン1</button>
{/* 無限ループする(NGパターンの呼び出し) */}
<button onClick={func1(1)}>ボタン1</button>
両者の違いは括弧があるか、ないか、それだけです。
それぞれの戻り値はどうなるでしょうか?分かりやすくしてみます。
{/* 無限ループしない(サンプル初期の呼び出し) */}
<button onClick={() => { return func1(); }}>ボタン1</button>
{/* 無限ループする(NGパターンの呼び出し) */}
<button onClick={ setCount(count + 1); setMessage(`ボタン1がクリックされました!`); }>ボタン1</button>
前者は「関数名」の形式で書くことで戻り値として関数を返しています。
それに対して後者は「関数名()」の形式で書くことで関数の実行結果を返しています。
実行結果のみな(戻り値は無い)ので、もし仮に無限ループがエラーとならずにボタンが押せたとしてもカウントアップ等されることはありません。
React君「お!ボタンが押された!!onClickの処理するで〜...え、実行結果だけ渡されても...関数渡してくれないと処理できないで!」ということですね。
じゃあどうすれば?
ここまで来ればお分かりですね。
イベントのpropには関数の実行結果ではなく、関数を渡しましょう。
NGパターンの「onClick={func1(1)}」を愚直に修正すると以下となります。
<button onClick={() => { return func1(1); }}>ボタン1</button>
Reactの公式ドキュメントに則って「波括弧」「return」「セミコロン」を省略した以下の表記を採用します。シンプルになりましたね。
<button onClick={() => func1(1)}>ボタン1</button>
ということで、最終的に完成したソースコード全体が以下となります。
import { useState } from "react";
export const App = () => {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("Hello");
const func1 = (n) => {
setCount(count + 1);
setMessage(`ボタン${n}がクリックされました!`);
};
return (
<>
<h1>{count}</h1>
<p>{message}</p>
<button onClick={() => func1(1)}>ボタン1</button>
<button onClick={() => func1(2)}>ボタン2</button>
<button onClick={() => func1(3)}>ボタン3</button>
</>
);
};
ボタンクリック時に「Hello」の部分が更新されれば成功です。めでたしめでたし。
参考
まとめ
- イベント(onClick、onChangeなど)のpropに「関数名()」の形式で書いてしまうと最悪の場合、無限ループしてアプリケーションが動かなくなってしまう
- イベントのpropには関数の実行結果ではなく、関数を渡そう