useEffectでcomponentDidMountを代用する


componentDidMountをやめる

「hooksで全部書いていきたい!」とはあまり思わないが、
1からFCでコンポーネントを書き始めると、
あとで「componentDidMountいるじゃん」となったときに面倒くさいことが多々あったので、
この際覚えちゃおうというと、いろいろ試してみる。

最終完成品
https://codesandbox.io/embed/dank-shape-mb1mh

軽く実装してみた(駄目な例)

codeSandBoxを利用したのをそのまま載せているので、細かいことは大きな心でスルーしてください。

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

const App = () => {
  return (
    <div className="App">
      <CountButtons initialCount={10} />
    </div>
  );
};

const CountButtons = ({ initialCount }) => {
  const [count, setCount] = useState(initialCount);
  const countUp = () => setCount(count + 1);
  const countDown = () => setCount(count - 1);
  useEffect(() => {
    console.log("useEffect");
  });
  return (
    <>
      <p>{count}</p>
      <button onClick={countUp}>↑↑increment↑↑</button>
      <button onClick={countDown}>↓↓decrement↓↓</button>
    </>
  );
};

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

useEffectはこんな感じで書くと、renderするたびに実行されていくようだ。
ボタンをクリックした後の再レンダリングでも発火してしまっているのがコンソールを見るとわかる。
なので、この書き方ではcomponentDidMountの動きは再現できない。

修正後

では、componentDidMountみたいにするにはどうしたらいいか。
初回レンダリングのときのみ発火させたい。

その時は第2引数に発火させる条件の変数を配列で入れてあげればいいようだ。

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

const App = () => {
  return (
    <div className="App">
      <CountButtons initialCount={10} />
    </div>
  );
};

const CountButtons = ({ initialCount }) => {
  const [count, setCount] = useState(initialCount);
  const countUp = () => setCount(count + 1);
  const countDown = () => setCount(count - 1);
  useEffect(() => {
    console.log("useEffect");
  }, [initialCount]); // ここにinitialCountを入れる
  return (
    <>
      <p>{count}</p>
      <button onClick={countUp}>↑↑increment↑↑</button>
      <button onClick={countDown}>↓↓decrement↓↓</button>
    </>
  );
};

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

こうすると何が起きるのかというと、
初期レンダリング時にpropsで渡されるinitialCountが来て、
その値を見て、useEffectが発火する。

なので2回目以降はinitialCountの値は変わらないので、
ボタンをクリックしてもuseEffectは発火しない
→componentDidMountの出来上がり。

更新ボタンを押したときのみuseEffectがコンソールログに出るので、
ちゃんと初期レンダリングのみ発火するようになっている。

propsがないとき

propsがあるからcomponentDidMountのように振る舞えているようにも見えるので、
propsがないときのパターンを考えてみる。

useEffectを発火させるのは、初期レンダリングにのみ定義されているものを第2引数に入れればいいので、
setCountをいれて試してみる。

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

const App = () => {
  return (
    <div className="App">
      <CountButtons />
    </div>
  );
};

const CountButtons = () => { // propsを消す
  const [count, setCount] = useState(10);
  const countUp = () => setCount(count + 1);
  const countDown = () => setCount(count - 1);
  useEffect(() => {
    console.log("useEffect");
  }, [setCount]); // useStateで定義した関数を入れてみる
  return (
    <>
      <p>{count}</p>
      <button onClick={countUp}>↑↑increment↑↑</button>
      <button onClick={countDown}>↓↓decrement↓↓</button>
    </>
  );
};

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

こんな感じにしておけばいいのではと思い実行。

できた。
なるほどね。

注意点

いろいろなサイトでこのuseEffectについて調べていたが、
第2引数に空配列[]を入れても動くみたいだけど、アンチパターンらしい。
よくわからないが、人的ミスを起こしやすくなるとか。

componentDidMountでよくね?

useEffectでcomponentDidMountの振る舞いをできるように実装してみたが、
useEffect自体がたくさんの処理を行えるので、
どんな処理がなされているのかを細かく見ないと、修正するのが大変になりそう。

それこそバグの原因にないかねない気がする。

実際につかっていって、これは使わないほうがいいわってなったときは
また記事にでもしましょうか。。。

追記

ラップすればcomponentDidMountとかは簡単に判別できるようになることに気がついた。。。

import { useEffect } from 'react';

export const useDidMount = (func: Function) => useEffect(() => { func() }, []);
export const useWillUnMount = (func: Function) => useEffect(() => func, []);