reactのsetStateによるrenderメソッド実行の動きを考える
- setStateとrerenderの動きを検証してみた
- 検証はReactNativeで行ったがReactでも同じであろう
- 内部の動きはソースを追ってみたけどよく分からなかった
- 詳しい記事などあれば教えて下さい
検証1
背景
- React.Componentを継承したReactコンポーネントでは、
setState
を呼び出してstateを更新することで、renderメソッドが実行されて再レンダリングされると理解している - そうなると、一連の処理の中で
setState
を何度も呼ぶとrenderメソッドが呼ばれまくってレンダリングされまくるんじゃないかという疑問があった
検証内容
- ボタンをクリックしたらsetStateを1000回呼んで見る
- renderメソッドにconsole.logを仕掛けといてrenderが何回呼ばれたか確認する
検証用コード
App.js
import React, { Component } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
export default class App extends Component {
state = { now: "ready" };
onPress = () => {
[...Array(1000)].forEach(() => {
console.log("---");
this.setState({ now: new Date().toLocaleTimeString() });
});
};
render() {
console.log("renderメソッド");
console.log(this.state);
return (
<View style={styles.container}>
<TouchableOpacity onPress={this.onPress}>
<Text>スタート</Text>
</TouchableOpacity>
<Text>{this.state.now}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
キャプチャ
結果
- renderメソッドは一度だけしか呼ばれなかった
- ループが1000回まわっている =>
setState
が1000回呼ばれていることも確認できる - ボタンを押してから画面の表示内容が変更されるまで間がある
考察
-
setState
を呼ぶ度にrenderメソッドが呼ばれる訳ではなさそう - 一連の処理が終わってから最後にrenderが呼ばれ描画内容が更新されるっぽい
検証2
背景
- 検証1の結果を受けて、
onPress
の先頭でstateAをsetState
し、重い処理のあと最後にstateBをsetState
したとする - その時にstateAの更新結果はすぐに反映されずに待たされてしまうのか?と疑問に思った
検証内容
- 以下の流れで動作させる
- ボタンをクリックしたら
start
というstateに時刻をsetする -
console.log
を5000回呼ぶ(時間のかかる処理ならなんでもよかった) - 最後に
stop
というstateに時刻をsetする
- ボタンをクリックしたら
検証コード
App.js
export default class App extends Component {
state = { start: "", stop: "" };
onPress = () => {
this.setState({ start: new Date().toLocaleTimeString() });
[...Array(5000)].forEach(() => {
console.log("---");
});
this.setState({ stop: new Date().toLocaleTimeString() });
};
render() {
console.log("renderメソッド");
console.log(this.state);
return (
<View style={styles.container}>
<TouchableOpacity onPress={this.onPress}>
<Text>スタート</Text>
</TouchableOpacity>
<Text>start: {this.state.start}</Text>
<Text>stop: {this.state.stop}</Text>
</View>
);
}
}
キャプチャ
結果
- 最初にsetしたstartというstateはすぐに反映されなかった
- 処理が全部終わってからまとめて画面に反映された
考察
- 処理が全部終わった後にrenderメソッドが呼ばれその時点のstateが反映されてるっぽい
- (あまりないとは思うが)重い処理を同期的に実行する場合、setStateの結果が画面に反映されるのは処理が終了してからだと覚えておいた方が良い
検証3
背景
- 検証2の結果を受けて、重い処理をしているとstateの反映が待たされると分かった
- だが、通信処理中にローディングを出すような時は一連の処理の中で
loading
のようなstateを切り替えてそれが反映されてるよなと思い出した - 非同期処理を実行するとそこで一度renderが呼ばれるんじゃないかと予想されるのでそれを試す
検証内容
-
loading
というstateがtrueだと画面を暗くしてインジケータを出すようにする - onPressの先頭で
loading
にtrueをsetする - 3秒待つだけの非同期処理を行う
-
loading
にfalseをsetする
検証コード
App.js
import React, { Component } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import Spinner from "react-native-loading-spinner-overlay";
export default class App extends Component {
state = { loading: false };
timeout = ms => new Promise(resolve => setTimeout(resolve, ms));
onPress = async () => {
this.setState({ loading: true });
console.log("start");
await this.timeout(3000);
console.log("done");
this.setState({ loading: false });
};
render() {
console.log("renderメソッド");
console.log(this.state);
return (
<View style={styles.container}>
{/* visibleがtrueだと画面が暗くなってインジケータが出る */}
<Spinner
visible={this.state.loading}
overlayColor="#rgba(0, 0, 0, .2)"
/>
<TouchableOpacity onPress={this.onPress}>
<Text>スタート</Text>
</TouchableOpacity>
</View>
);
}
}
キャプチャ
結果
- 想定通り非同期処理が実行されたタイミングで一度renderメソッドが呼ばれた
- ちなみに
timeout
の引数を0にした場合もコンソールの出力内容は同じ
考察
- 非同期処理が実行されるとrenderメソッドが呼ばれる
- 非同期処理のコールバック後の処理が完了するとまたrenderメソッドが呼ばれる
まとめ
- setStateを呼ぶとすぐにrenderメソッドが呼ばれるものと思っていたがそんなことはなかった
- setStateが散財すると見通しが悪くなってしまうので、この挙動を頭に入れて無駄なコードを書かないようにしたい