LoginSignup
73
35

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-20

あらすじ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
73
35