React-routerのBrowerHistoryを、constructor内で使うと、Warning: setState(...):が発生する謎を解いた
Reduxのプロジェクトで100回は詰んだうちの1回です。 (果てしない)
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 tocomponentWillMount
.
素直に読んでみると、
①renderの中でstateをアップデートしていないか?
②他のコンポーネントのconstructorの中で(略
①render()内でstateをアップデートするのはなぜダメなのか?
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()を使っていませんでした。
じゃあなぜwarningが出るのか?
原因は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前に実行できます。
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前に呼び出されるので重宝します。