こんなものを作りました。
Reactの基本を学ぶためにカウターを作りました。
3の倍数のとき、モーダルが立ち上がります。全体を通して簡単でしたが、苦労した点がありました。
☆Special Thanks☆
・React Component ライフサイクル ひとめぐり (CodeSandbox 付き)
・js STUDIO 【componentDidUpdate】
信じられない...ライフサイクルを考えて実装しないといけないなんて...
[+1]を押下すると、カウントの値が1づつ増える。増えた値を判定してその値が3の倍数であったらモーダルを表示するフラグをtrueにする。この流れを1つの関数として以下のように実装しました。
increment() {
this.setState(prevState => ({
count: prevState.count + 1, }));
if(this.state.count && this.state.count % 3 === 0){
this.setState({isModalOpen: true});
}
}
信じられない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は、無限ループを防ぐために入れ子になった更新の数を制限します。
ということのようだが....
何だろう...このコードの何がいけないの?...
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は無限ループを防ぐ仕様になっていてよかった。問題ということなので、下記の【実行したいこと】をもとに問題箇所のコードを書き直しました。
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の勉強を始めたとき、ライフサイクルについてはサラーっと流してしまいましたが、すぐに必要な場面に出会うことができ大変理解が深まったと思います。
初心者だからと言って安易に読み飛ばしてはいけません!
入門書だからここは飛ばしていいよなんていう甘い言葉を鵜呑みにしてはいけません!!!
この記事が、桂三度さん思い出すきっかけになれれば幸いです。
発展課題
5の倍数だけ犬っぽくなるを実装する
まとめのコード
(※classNameは省いています)
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;