アンマウントされたコンポーネントのstate更新問題に対処する。
アンマウントされたコンポーネントのstate更新問題に対処する。:
画像を表示するコンポーネントで、非同期通信(Ajax, fetch)で取得したものをstateで管理するようにしたら怒られた。
そんな実装がこれです。
コンポーネントが「アンマウント」される前に
先ずは、
CancellationTokenはTC39のProposal: ECMAScript Cancellationに(おおむね)基づいて実装されたものです。
https://github.com/conradreuter/cancellationtoken
要は、「キャンセル状態」というものをstate管理するのでは無く、propsで購読するようにすれば良さそう。
問題のコンポーネントの上位層で、
問題のコンポーネントで、
ECMAScript 2015のClassのプロパティに
あらすじ
画像を表示するコンポーネントで、非同期通信(Ajax, fetch)で取得したものをstateで管理するようにしたら怒られた。Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.今回は2画面用意しました。
- 問題のコンポーネント
- 雑な画面
componentDidMount
が走った頃合いで、違う画面に行くとエラーが発生します。そんな実装がこれです。
import React from "react"; import ReactDOM from "react-dom"; import { compose, withStateHandlers, lifecycle } from "recompose"; import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom"; import "./styles.css"; function getReactImage() { // 3秒後にReactの画像返してくれる処理 return new Promise(resolve => { setTimeout(() => { resolve( "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xMS41IC0xMC4yMzE3NCAyMyAyMC40NjM0OCI+CiAgPHRpdGxlPlJlYWN0IExvZ288L3RpdGxlPgogIDxjaXJjbGUgY3g9IjAiIGN5PSIwIiByPSIyLjA1IiBmaWxsPSIjNjFkYWZiIi8+CiAgPGcgc3Ryb2tlPSIjNjFkYWZiIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIi8+CiAgICA8ZWxsaXBzZSByeD0iMTEiIHJ5PSI0LjIiIHRyYW5zZm9ybT0icm90YXRlKDYwKSIvPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIiB0cmFuc2Zvcm09InJvdGF0ZSgxMjApIi8+CiAgPC9nPgo8L3N2Zz4K" ); }, 3000); }); } function App(props) { const { imageUrl, history } = props; return ( <div className="App"> <h1>Hello CodeSandbox</h1> <Link to="/sub">go Sub</Link> <h2>Start editing to see some magic happen!</h2> {imageUrl ? <img src={imageUrl} /> : <span>NO IMAGE</span>} </div> ); } const enhance = compose( withStateHandlers( { imageUrl: null }, { handleImageUrl: (state, props) => imageUrl => { return { imageUrl }; } } ), lifecycle({ async componentDidMount() { const { handleImageUrl } = this.props; // 画像取得して、 const url = await getReactImage(); // 画像をstateにセット。 handleImageUrl(url); } }) ); const Main = enhance(props => <App {...props} />); const rootElement = document.getElementById("root"); ReactDOM.render( <Router basename="/"> <Switch> <Route exact path="/" component={Main} /> <Route path="/sub" render={() => <Link to="/">go Main</Link>} /> </Switch> </Router>, rootElement );
方針
コンポーネントが「アンマウント」される前に- 非同期処理を中断する
- stateの更新をしないようにする
ざっくり解説
先ずは、 npm install cancellationtoken
する。CancellationTokenはTC39のProposal: ECMAScript Cancellationに(おおむね)基づいて実装されたものです。
https://github.com/conradreuter/cancellationtoken
要は、「キャンセル状態」というものをstate管理するのでは無く、propsで購読するようにすれば良さそう。
問題のコンポーネントの上位層で、
CancellationToken
を作成し、propsに流し込む。問題のコンポーネントで、
-
token.isCancelled
を見て、stateの更新を止める。 -
componentWillUnmount
で、cancel()
する。
実装
import React from "react"; import ReactDOM from "react-dom"; import { compose, withStateHandlers, lifecycle } from "recompose"; import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom"; import CancellationToken from "cancellationtoken"; import "./styles.css"; function getReactImage() { return new Promise(resolve => { setTimeout(() => { resolve( "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xMS41IC0xMC4yMzE3NCAyMyAyMC40NjM0OCI+CiAgPHRpdGxlPlJlYWN0IExvZ288L3RpdGxlPgogIDxjaXJjbGUgY3g9IjAiIGN5PSIwIiByPSIyLjA1IiBmaWxsPSIjNjFkYWZiIi8+CiAgPGcgc3Ryb2tlPSIjNjFkYWZiIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIi8+CiAgICA8ZWxsaXBzZSByeD0iMTEiIHJ5PSI0LjIiIHRyYW5zZm9ybT0icm90YXRlKDYwKSIvPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIiB0cmFuc2Zvcm09InJvdGF0ZSgxMjApIi8+CiAgPC9nPgo8L3N2Zz4K" ); }, 3000); }); } function App(props) { const { imageUrl } = props; return ( <div className="App"> <h1>Hello CodeSandbox</h1> <Link to="/sub">go Sub</Link> <h2>Start editing to see some magic happen!</h2> {imageUrl ? <img src={imageUrl} /> : <span>NO IMAGE</span>} </div> ); } const enhance = compose( withStateHandlers( { imageUrl: null }, { handleImageUrl: (state, props) => imageUrl => { return { imageUrl }; } } ), lifecycle({ async componentDidMount() { const { handleImageUrl, token } = this.props; const url = await getReactImage(); // `token.isCancelled `を見て、stateの更新を止める。 if (!token.isCancelled) { handleImageUrl(url); } }, componentWillUnmount() { // `componentWillUnmount`で、`cancel()`する。 this.props.cancel(); } }) ); const Main = enhance(props => <App {...props} />); const rootElement = document.getElementById("root"); ReactDOM.render( <Router> <Switch> <Route exact path="/" render={() => { // 問題のコンポーネントの上位層で、`CancellationToken `を作成し、propsに流し込む。 const { cancel, token } = CancellationToken.create(); const { Provider, Consumer } = React.createContext(); // Provider, Consumerは予習しないとわからないかも。 // React「context API」を一筆書き。 // https://qiita.com/shsssskn/items/a107e98d1af0ea2c8b5c return ( <Provider value={{ token, cancel }}> <Consumer> {({ token, cancel }) => <Main token={token} cancel={cancel} />} </Consumer> </Provider> ); }} /> <Route path="/sub" render={() => <Link to="/">go Main</Link>} /> </Switch> </Router>, rootElement );
あとがき
ECMAScript 2015のClassのプロパティにisMounted
を持つことで、今回の問題に対応できるが自分の好みではなかった。class News extends Component { isMounted = false; ... componentDidMount() { this.isMounted = true; ... componentWillUnmount() { this.isMounted = false;
コメント
コメントを投稿