Help us understand the problem. What is going on with this article?

[ React / Preact ] JSX内のイベントハンドラと引数について (恐怖の無限ループ)

More than 1 year has passed since last update.

守秘義務のため記載してあるソースコードは問題が起きたものを再現したもので,そのものではありません。

=

社員さんからはこの記事を書くための了承は得ています。

何が起こったの??

これはインターン中に起こった悲劇です。
自分のアサインされているプロジェクトでは Preact と呼ばれる 軽量化版React のようなものを使って開発を行なっています。機能面で言えば些細な違いはありますがほぼほぼ React です。

そこで以下のようなJSXを書きました。

<div onClick={props.switchToggle(props.list)}>
  <Notification />
</div>

この this.props.switchToggle と呼ばれるメソッドは Unistore と呼ばれる redux のような状態管理ツールの state をいじっています。

これを実行したところ、なぜかブラウザ超重くなり動かなくなっちゃいました。

原因を究明するために this.props.switchToggle 内に console.log("test") を記載するとおびただしい量の test という文字列が出力され続けました。
この挙動を見る限り「無限ループ」起きてるじゃんということに気づきました。

メソッド内に問題があるか否かを検証するために console.log("test") で置き換える。

<div onClick={console.log("test")}>
  <Notification />
</div>

その結果、レンダリング時にクリックしていないのにも関わらず test が表示されました。。
訳がわかりません。

そこでダメ元でアロー演算子を使ってみた..。

<div onClick={() => console.log("test")}>
  <Notification />
</div>

すると正常に動く...。
なので、最初の構文もアロー演算子で実装すると...。

<div onClick={() => props.switchToggle(props.list)}>
  <Notification />
</div>

これも正常に動く...。

ただ、自分たちのプロジェクトだとJSX内にアロー関数を書くと eslint error になるので代案が必要です。

まず、解決の前にこの原因をとりあえずこの状態で色々試したみた所、

「JSX内の関数に引数をつけると即時関数になっているのでは」

という結論に至りました。
これを解決する為、もともと functional Component で作成したいましたが,それを Class Compoent に置き換えて以下のようなメソッドを定義。

class Header extends Component {

  switchToggle = () => this.props.switchToggle(this.props.list);

こうすることでJSX内では引数なしで関数を呼び出すことができます。
こんな感じです。

class Header extends Component {
  switchToggle = () => this.props.switchToggle(this.props.list);
  render() {
    return (
      <div onClick={this.switchToggle}>
        <Notification />
      </div>
    );
  }
}

結局,何が起こっていたのか??

この記事を書くにあたって色々調べていたら公式でも説明がありました。
https://reactjs.org/docs/handling-events.html#passing-arguments-to-event-handlers

要約すると関数に引数を渡すとレンダリング時にそのまま実行しちゃうので アロー関数 を使うか bind させるかする必要があるようです。

けど,それだと無限ループ起こらんくない??

確かに普通の関数をが即時実行されているのであれば起こりません。
ただ,今回の場合は即時実行されていたのは uniStorestore の状態をいじっていたメソッドです。

つまり

  1. コンポーネントがレンダリングされる
  2. 問題のメソッドが実行される
  3. Store内の値が書き換わる
  4. Store内の値が変更されたのでそれに合わせて再レンダリングが走る
  5. レンダリングされたので 2. に戻る

という感じでループが発生していたようです。

追記

自分は Class Component を使用して実装しましたが他のパターンでの実装も教えてもらったので共有します!

functional Component

const Header = props => {
  const { list, switchToggle } = props;
  const handleClick = () => switchToggle(list);
  return (
    <div onClick={handleClick}>
      <Notification />
    </div>
  );
}

Hooks

Preactはv10からHooksに対応します。
https://github.com/preactjs/preact/releases

const Header = ({ list, switchToggle }) => {
  const toggle = useCallback(() => {
    switchToggle(list);
  }, [list, switchToggle]);

  return (
    <div onClick={toggle}>
      <Notification />
    </div>
  );
}

最後に

あんまり知られていないであろう知識なので共有しました。
僕自身,このバグで1時間以上費やしたのでこの記事が同じ問題に遭遇した誰かの役に立てれば嬉しいです...。

自分もまだまだ勉強中の身なのでこの記事に技術的な間違いや誤字があるかもしれないので,その時は優しくコメントで指摘してもらえると幸いです。
それでは最後までお付き合いありがとうございました!!

kubo_programmer
National Institute of Technology, Kitakyushu College
https://note.mu/kuboblog
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした