React+Typescriptでカウンターアプリ作成に難航したお話〜3の倍数の時モーダル出現が難しい訳ないじゃん....〜


## こんなものを作りました。
Reactの基本を学ぶためにカウターを作りました。
3の倍数のとき、モーダルが立ち上がります。全体を通して簡単でしたが、苦労した点がありました。
<a href="https://gyazo.com/dc50b76dddd4d9c0f67554a96caf1c29"><img src="https://i.gyazo.com/dc50b76dddd4d9c0f67554a96caf1c29.gif" alt="Image from Gyazo" width="734"/></a>

## ☆Special Thanks☆
・<a href="https://iktakahiro.hatenablog.com/entry/2018/05/28/123000">React Component ライフサイクル ひとめぐり (CodeSandbox 付き)</a>

・<a href="https://teratail.com/questions/144256">ReactでsetStateをすぐに反映させる方法…</a>


・<a href="http://js.studio-kingdom.com/react/component_lifecycle/updating_componentdidupdate">js STUDIO 【componentDidUpdate】</a>


## 信じられない...ライフサイクルを考えて実装しないといけないなんて...

[+1]を押下すると、カウントの値が1づつ増える。増えた値を判定してその値が3の倍数であったらモーダルを表示するフラグをtrueにする。この流れを1つの関数として以下のように実装しました。

```App.tsx
increment() {
 this.setState(prevState => ({
  count: prevState.count + 1, }));
  if(this.state.count && this.state.count % 3 === 0){
   this.setState({isModalOpen: true});
  }
}
```

すると...
<img width="523" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/119773/a4f3c62a-77e6-e2d9-c5fe-fc12f5911783.png">

信じられない4が3の倍数だなんて...(違う)

setStateでcountを変えても、全てを一気に書き換えるというsetStateの特性上、`this.state.count`を直後に呼び出してもそのstateは更新前の状態になっている。これが想い通りにならない原因のようだった。

そこで、コンポーネントの更新を終えてすぐに関数を呼び出したい時（DOMを操作したい時）は、`componentDidUpdate` を使うといいよ、ということでしたので試しました。
React入門でこのカウンターを作っていたので「ライフサイクルとかとりあえずいいやっ」そんな軽い気持ちで読み飛ばしていたせいで引っかかってしまいました。


## まさかの落とし穴...無限ループに陥るべからず...

`componentDidUpdate`を使って、countに[+1]づつ足していき、値が3の倍数かどうかを判定するという実装をしようと試みた矢先の出来事でした。

consoleにはけたたましい数のエラーがっ（｡ﾟ(ﾟ´Д｀ﾟ)ﾟ｡）52回でループが止まってよかった...
こんなエラーも出てました。

>Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.


ふむふむ。。なるほど。(Google先生によると)
>最大更新深度を超えました。これは、コンポーネントがcomponentWillUpdateまたはcomponentDidUpdate内でsetStateを繰り返し呼び出すと発生する可能性があります。 Reactは、無限ループを防ぐために入れ子になった更新の数を制限します。

ということのようだが....

### 何だろう...このコードの何がいけないの？...
```App.tsx
increment() {
 this.setState(prevState => ({
  count: prevState.count + 1, }));
 }

 componentDidUpdate(prevProps: object, prevState: AppState){
   this.judge(prevState.count + 1); //←問題はこの部分に条件式がないこと
}

 judge(num: number) {
   if(this.state.count && this.state.count % 3 === 0) {
    this.setState({isModalOpen: true});
   }
 }
```

【上記コードの問題点】
（1）[+1]を押下する
↓
（2）prevState.count+1 した値をcontへ渡す
↓
（3）`componentDidUpdate`内で、countの値を判定し、3の倍数（かつ0以外）のときモーダル表示フラグisModalOpenをtrueにする

(3)において、judge関数内で状態変更がなされた後、その状態の変更によって再度`componentDidUpdate`がトリガされ、setStateによって状態が再びtrueに設定される。これにより、`componentDidUpdate`が何度もトリガされる。

このようになっており無限ループが始まってしまう問題をはらんでいます。Reactは無限ループを防ぐ仕様になっていてよかった。問題ということなので、下記の【実行したいこと】をもとに問題箇所のコードを書き直しました。

```App.tsx
componentDidUpdate(prevProps: object, prevState: AppState){
 if (prevState.count !== this.state.count) {
  this.judge(prevState.count + 1);
 }
}
```

【実行したいこと】
・[+1]を押下する
↓
・押下する前の値prevState.countを補完する
↓
`・prevState.countと押下後の値countを比較して違っている場合だけjudge()を発火させる(★)`
↓
・countが3の倍数（かつ0以外）のときモーダル表示フラグisModalOpenをtrueにする


問題の箇所に、(★)の条件式を追加して解決しました。

## まとめ

Reactの勉強を始めたとき、ライフサイクルについてはサラーっと流してしまいましたが、すぐに必要な場面に出会うことができ大変理解が深まったと思います。

初心者だからと言って安易に読み飛ばしてはいけません！
入門書だからここは飛ばしていいよなんていう甘い言葉を鵜呑みにしてはいけません！！！


この記事が、<a href="https://anohitohaima.tokyo/post-534/">桂三度さん</a>思い出すきっかけになれれば幸いです。

### 発展課題

5の倍数だけ犬っぽくなるを実装する

## まとめのコード

（※classNameは省いています）

```App.tsx
import React, { Component } from 'react';
import { Card, Statistic } from 'semantic-ui-react';

import './App.css';

interface AppState {
  count: number;
  isModalOpen: boolean;
}

class App extends Component<{}, AppState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      count: 0,
      isModalOpen: false,
    }
  }

  modalOpen() {
    this.setState({isModalOpen: true});
  }

  modalClose() {
    this.setState({isModalOpen: false});
  }

  increment() {
    this.setState(prevState => ({
      count: prevState.count + 1, }));
  }

  componentDidUpdate(prevProps: object, prevState: AppState){
    if (prevState.count !== this.state.count) {
      this.judge(prevState.count + 1);
    }
  }

  decrement() {
    this.setState(prevState => ({
    count: prevState.count - 1, }));
  }

  judge(num: number) {
    console.log(this.state.count);
    if(this.state.count && this.state.count % 3 === 0) {
      this.setState({isModalOpen: true});
    }
  }

  render() {
    const { count } = this.state;
    let modal;

    if(this.state.isModalOpen) {
      modal = (
        <div className="modal">
          <div className="modal-inner">
            <div className="modal-hedder">☆3の倍数お知らせBot☆</div>
            <div className="modal-value">
              <h2>{count}</h2>
              <p>{count}は3の倍数です。</p>
            </div>
            <button onClick={() => {this.modalClose()}}>
              とじる
            </button>
          </div>
        </div>
      )
    }

    return (
      <div>
        {modal}
        <header>
          <h1>カウンター</h1>
        </header>
        <Card>
          <Statistic>
            <Statistic.Value>
            {count}
            </Statistic.Value>
          </Statistic>
          <Card.Content>
            <div>
              <button onClick={() => this.decrement()}> -1</button>
              <button onClick={() => this.increment()}>+1 </button>
            </div>
          </Card.Content>
        </Card>
      </div>
    );
  }
}

export default App;

```