React-routerのBrowerHistoryを、constructor内で使うと、Warning: setState(...):が発生する謎を解いた

More than 1 year has passed since last update.


Reactなんだか良くわからんWarning多すぎ問題

以下は /user(user.js) にアクセスした時に

currentUserがnullであれば(=signinしていなければ)

/sign-in (SIGN_IN_PATH) に遷移しなさいという処理。

加えて、user/hogeページ(this.props.children)にアクセスした時も、

currentUserがnullじゃないかどうかを判断しましょうね、というコード。

(こちらは今回は蛇足です)


user.js

class User extends Component {

constructor(props) {
super(props);
this.state = {currentUser: null}
//問題となる部分
if (!this.props.currentUser){
return browserHistory.push(CommonConstants.SIGN_IN_PATH);
}
}

....
// /user であれば renderMyAccount を
// /user/hoge であればそれをレンダリングする
renderContent() {
if (this.props.children) {
return this.props.children;
} else {
return this.renderMyAccount();
}
}

renderMyAccount() {
const {t} = this.props;
const user = this.state.currentUser;
if (!user) {return}
return (
<div className='col-md-8 col-md-offset-2'>
アカウントページをずらっと出力。
</div>
)
}

render() {
return (
<div className='row'>
{this.renderContent()}
</div>
);
}
}


しかしエラーがでる。


Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.


素直に読んでみると、

①renderの中でstateをアップデートしていないか?

②他のコンポーネントのconstructorの中で(略


①render()内でstateをアップデートするのはなぜダメなのか?

renderの中でsetStateを呼んではいけない理由


render時にstateが更新される=>stateが更新されるとrenderが実行される=>render時にstateが更新される=>stateが・・・・ということになり無限にrenderが実行されてしまっていた。


なるほど、、、しかし今回はこのケースではないです。


②他のコンポーネントのconstructorの中でstateをアップデートするのはなぜダメなのか?

What will happen if I use setState() function in constructor of a Class in ReactJS or React Native?

Ability to call setState in the constructor

簡単に言えば、非同期通信だかららしい。

どういうことかといえば、stateは本来一つのコンポーネントに依存しているべきであって、受け渡したり、複数のコンポーネントで同時に使われるべきではないということ。

非同期通信が可能故に、2つのコンポーネントでstateが同時進行的に変わるのは好ましくないということでした。


でも今回はstateの更新なんてしてないぞ??

問題は今回のソースコードのconstructor内で、setState()を使っていないということでした。

原因はBrowserHistoryのpushメソッドにありました。

React-routerの公式ドキュメント

History関数の公式ドキュメント


Navigation

history objects may be used programmatically change the current

location using the following methods:

history.push(path, [state])

history.replace(path, [state])

history.go(n)

history.goBack()

history.goForward()

history.canGo(n) (only in createMemoryHistory)


というわけで、push,replaceどちらを使っても、stateを受け渡すことになってしまい、それがエラー文中のupdateに該当しているということでした。

(間違っていたら是非指摘していただきたいです。)


解決策

componentWillMountを使う

こちらを参照

こちらも参照

これだったら確実にrender前に実行できます。


user.js

class User extends Component {

constructor(props) {
super(props);
this.state = {currentUser: null}
//ここでは一度返す
if (!this.props.currentUser) {return}
UserAction.getUser((user) => {
this.setState({currentUser: user});
});

//ここで呼び出す。
componentWillMount() {
if (!this.props.currentUser) {
return browserHistory.push(CommonConstants.SIGN_IN_PATH);
}
}
}

....

renderContent() {
if (this.props.children) {
return this.props.children;
} else {
return this.renderMyAccount();
}
}


componentWillMountはrender前に呼び出されるので重宝します。