アンマウントされたコンポーネントのstate更新問題に対処する。

アンマウントされたコンポーネントのstate更新問題に対処する。:


あらすじ

画像を表示するコンポーネントで、非同期通信(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; 

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)