ReactのsetStateについて

Reactは内部で非同期的にstateを更新する

Reactは、setStateが呼び出されると、将来this.stateが更新されることを予定します。

そしてstateが更新されたタイミングで、renderメソッドが呼び出され、同コンポーネントとその子コンポーネントが再描画されます。

つまり、setStateの直後のrenderの時点では、this.stateが更新されていることが保証されます。

しかし、this.stateの直後にthis.stateにアクセスする処理を書いても、this.stateが更新されているとは限りません。

これは、React内部での処理の最適化の意図があります。

setStateの書き方

Reactでは、この事実に基づいて、いくつかsetStateに記述のパターンがあります。

更新が現在のstateに依存せず、後続する処理がないとき

単にsetStateに次の状態を表すオブジェクトを渡します。

これが最も一般的なパターンだと思いますが、いつも使えるとは限りません。

1
2
3
this.setState({
// next state
})

現在のstateに依存した更新を行いたいとき

現在のstateに依存した更新を行いたいときは、次の状態を表す関数を渡します。

setStateは、第1引数でオブジェクトではなく関数を受け取ることが出来ます。

関数は、現在のthis.stateを受け取り、変更後のthis.stateをオブジェクトで返すものにします。

例えば、現在のthis.state.countをインクリメントする場合、以下のようにします。

1
2
3
4
5
6
7
function increment() {
this.setState((prev) => {
return {
count: prev.count + 1
};
});
}

こうすることで、以下のように複数回setStateを呼び出しても、変更が即座に反映されるようになります。

1
2
3
increment(); // 1
increment(); // 2
increment(); // 3

逆に言えば、以下のように単にオブジェクトを渡している場合、this.stateは変更が即座にはされません。

1
2
3
4
5
function increment() {
this.setState({
count: prev.count + 1
});
}

現在のthis.stateに依存した変更をsetStateに渡す場合、関数を使うようにしましょう。

次のstateに依存する処理を実行させたいとき

次の状態に依存する処理を実行させたいときは、第2引数でコールバック関数を渡します。

このコールバック関数は、必ずstateが更新された後に実行されます。

必ずstateが更新された後に処理を実行させたいときは、この書き方を使います。

1
2
3
this.setState({/* ...state */}, () => {
// この処理は、stateが更新された後に呼び出される
})

React管理外のコードは即座に実行されうる

以下のコンポーネントは、描画されたときにログを出力します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component } from 'react';
class TestComponent extends React.Component {
constructor(...args) {
super(...args);
this.state = {
dollars: 10
};
this._onTimeoutHandler = this._onTimeoutHandler.bind(this);
}
componentDidMount() {
setTimeout(this._onTimeoutHandler, 10000);
}
render() {
console.log('State in render: ' + JSON.stringify(this.state));
return (
<p>hello</p>
);
}
_onTimeoutHandler() {
console.log('State before (timeout): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 30
});
console.log('State after (timeout): ' + JSON.stringify(this.state));
}
};
export default TestComponent;

このコンポーネントがマウントされると、コンソールには以下のように出力されます。

1
2
3
State before (_onClickHandler): {"dollars":10}
State in render: {"dollars":40}
State after (_onClickHandler): {"dollars":40}

setStateは非同期的に実行されるはずですが、同期的に実行されているように見えます。

これは、setTimeOut関数が、Reactの管理外にあるためです。

この他にも、

  • addEventListener
  • AJAX呼び出し

などは、Reactの管理外にあたります。

なぜReactはstateを同期的に更新しているようにみえるのか

Reactは、自身の管理外にあるコードが最新のstateを利用できるようにします。

これは、最適化を行わないということではなく、可能な限り防御的に処理を実行しようとするためです。

あくまでも、最適化とそのための遅延はReactの管理内で行われます。

参考文献