Reactの基礎 (Lifecycle)

ライフサイクル (Lifecycle)とは

前回に引き続き、Reactの基礎であるライフサイクルについて、見ていきます。

Reactには componentDidMount()や componentWillUnmount() などReactコンポーネントが特定のタイミングに差し掛かった時や、特定の状態になった時に呼び出されるフックがあります。それがライフサイクルであり、ライフサイクルを利用することで、例えば「Reactコンポーネントのマウントが完了した時に、特定の処理を実行したい」などといったことが可能になります。

ReactのLidfecycleは数が多く、ここですべて列挙することはしませんが、比較的よく使われる3つのLifecycleを挙げておきます。バージョンによっては deprecated になったり、名前が変更されることもあるので、詳細は最新の公式ドキュメントを参照してください。

componentDidMount()

componentDitMount() はReactコンポーネントがマウントされる (DOMツリーに追加される) 時に呼び出されます。Canvasの描画や、DOMを触るライブラリを呼び出さなくてはならない場合にはこのcomponentDidMount()内部に処理を記述します。

componentWillUnmount()

componentWIllUnmount() はコンポーネントがアンマウントされ、破棄される直前に呼び出されます。このメソッド中では、setInterval()を使ったタイマーのような処理を解除したり、何らかのデータの購読を解除したりする処理を記述します。

たとえば、componentDidMount()でsetInterval()によって、1秒間隔でダイアログを開いたり、閉じたりアニメーションするReactコンポーネントを実装してみましょう。コンポーネントがマウントされた時にsetInterval() によって開始したタイマー処理はコンポーネントが破棄される時に、解除されるようにします。

import React from "react";
import ReactDOM from "react-dom";

const wrapperStyle = {
  backgroundColor: "#000",
  height: "100%",
  position: "reative",
  width: "100%"
};

const dialogBaseStyle = {
  backgroundColor: "#E0F2F1",
  bottom: 0,
  left: 0,
  height: 0,
  margin: "auto",
  position: "absolute",
  right: 0,
  top: 0,
  transitionDuration: ".3s",
  width: 0
};

const dialogInitialStyle = { ...dialogBaseStyle, opacity: 0 };
const dialogAnimationStyle = {
  ...dialogBaseStyle,
  height: "300px",
  opacity: 1,
  width: "80%"
};

class DialogComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      doAnimation: false,
      intervalFunctionId: null
    };
  }

  toggleAnimation = () =>
    this.setState({
      doAnimation: !this.state.doAnimation
    });

  /* Componenetが破棄される時に、setIntervalをセットする。 */
  componentDidMount() {
    console.log("componentDidMount");
    const intervalId = setInterval(this.toggleAnimation, 1000);
    this.setState({ intervalFunctionId: intervalId });
  }

  /* Componenetが破棄される時に、setIntervalを解除する */
  componentWillUnmount() {
    console.log("componentWillUnmount");
    clearInterval(this.state.intervalFunctionId);
  }

  render() {
    return (
      <div
        style={
          this.state.doAnimation ? dialogAnimationStyle : dialogInitialStyle
        }
      />
    );
  }
}

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showDialg: true
    };
  }

  render() {
    return (
      <div style={wrapperStyle}>
        <button
          onClick={() => {
            this.setState({ showDialg: !this.state.showDialg });
          }}
        >
          TOGGLE
        </button>
        {this.state.showDialg ? <DialogComponent /> : null}
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

componentDidUpdate()

componentDidUpdate() はReactコンポーネントが更新されたときに呼ばれます。初回のレンダリングではコールされません。コンポーネント更新時にDOMを操作しなければならない場合に用いることができます。componentDidUpdate()内部でsetState()を呼び出すと無限ループが発生するので、setState()する場合にはif文などの条件式で呼び出すと良いでしょう。

useEffect (Hooks API)

Hooks はReact v17で正式にリリースされる予定のAPIです。Lifecycleを使うためにReact v16ではクラス構文を使用する必要がありましたが、Hooks ではクラス構文を使わずに componentDidMount()やcomponentWillUnmount() などのLifecycle処理を記述できるようになる予定です。クラス構文を使う必要の無い場面では積極的に使用していきましょう。

下記の例ではReact 16.8.0-alpha.1 を使用しています。

  "dependencies": {
    "react": "16.8.0-alpha.1",
    "react-dom": "16.8.0-alpha.1"
  }

Hooks APIの useEffect() を使ってコンポーネントのライフサイクルを実装します。クラス構文を使わずに、componentDidMount()やcomponentWillUnmount()といったライフサイクルの機能を簡潔に記述することができます。

useEffect (公式ドキュメント)

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";

const Child1 = () => {
  useEffect(() => {
    /* like componentDidMount() */
    alert("child1 Mounted");

    return () => {
      /* like componentwillUnmount() */
      alert("child1 unMounted");
    };
  });

  return <span>Child1</span>;
};

const Child2 = ({ count }) => {
  useEffect(() => {
    /* like componentDidMount() */
    alert("child2 Mounted");

    return () => {
      /* like componentwillUnmount() */
      alert("child2 unMounted");
    };
  });

  return <span>Child2</span>;
};

const App = () => {
  const [flag, toggle] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          toggle(!flag);
        }}
      >
        Toggle Component
      </button>
      <div>{flag ? <Child1 /> : <Child2 />}</div>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useEffect() のパフォーマンス・チューニング

毎回毎回ライフサイクルメソッドが不必要に走ってしまうことにより、コンポーネントのパフォーマンスに影響が出てしまうことがあります。特定の場合のみにuseEffect() を適用したい場合には下記のようなイディオムが使えます。下記では再レンダリング時であっても、countの値に変更がない場合には、useEffect()を実行しないように制御できます。

useEffect(() => {
  document.title = `${count}回クリックされました!`;
}, [count]); // countの値に変更があったときのみに実行される。