Reactのアンマウントされたコンポーネントの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.


今回は２画面用意しました。

- 問題のコンポーネント
- 雑な画面

問題のコンポーネントの`componentDidMount`が走った頃合いで、違う画面に行くとエラーが発生します。

そんな実装がこれです。

```jsx
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](https://azu.github.io/slide-what-is-ecmascript/slide/4.html)のProposal: ECMAScript Cancellationに(おおむね)基づいて実装されたものです。
https://github.com/conradreuter/cancellationtoken

要は、「キャンセル状態」というものをstate管理するのでは無く、propsで購読するようにすれば良さそう。

問題のコンポーネントの上位層で、`CancellationToken `を作成し、propsに流し込む。
問題のコンポーネントで、

- `token.isCancelled `を見て、stateの更新を止める。
- `componentWillUnmount`で、`cancel()`する。

これで、アンマウントされた後の問題は解決したので、怒られなくなります。

# 実装

```jsx
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;
```