見出し画像

React Hooks (useEffect)

コンポーネントに副作用を追加する。
副作用という言い方がわかりにくい。

例としてカウンターコンポーネントを作ってみる。(下の画像)

カウントが2つあり、一つはクリックしたら増えるもの。
もう一つは一定時間で増えるもの(これはコンポーネントがアンマウントしたらタイマーも消す必要がある)。

スクリーンショット 2021-01-23 0.57.10

今までのclassコンポーネントの場合

import React from "react";

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

render() {
  return (
    <>
      <p>
        // ここを押してカウンターを出したり消したりする
        // カウント、タイマーがクリアされていることを確認する
        <button
          onClick={() => {
            this.setState({
              isShowCounter: !this.state.isShowCounter
            });
          }}
        >
          counter toggle
        </button>
      </p>
      {this.state.isShowCounter && <Counter />}    
    </>
  );
}
}

class Counter extends React.Component {
constructor(props) {
  super(props);
  this.id = null;
  this.state = {
    count: 0,
    timerCount: 0
  };
}
componentDidMount() { // コンポーネントがマウントされた時だけ呼ばれる
  console.log("コンポーネントをマウントしたよ");
  this.id = setInterval(() => { // タイマー
    this.setState((prevState) => {
      return { timerCount: prevState.timerCount + 1 };
    });
  }, 500);
}
componentDidUpdate(prevProps, prevState) { // props,stateがupdateしたときに呼ばれる
  if (prevState.count !== this.state.count) {
    console.log("countをupdateしたよ");
  }
  if (prevState.timerCount !== this.state.timerCount) {
    console.log("timerCountをupdateしたよ");
  }
}
componentWillUnmount() { // コンポーネントがアンマウントされる直前だけ呼ばれる
  console.log("コンポーネントをアンマウントするよ");
  clearInterval(this.id);  // タイマーを解除
}

render() {
  return (
    <div>
      <p>count: {this.state.count} 回</p>
      <p>timerCount: {this.state.timerCount} 回</p>
      <button
        onClick={() => {
          this.setState({ count: this.state.count + 1 });
        }}
      >
        count++
      </button>
    </div>
  );
}
}

export default CountWrapper;


・componentDidMount, componentDidUpdate, componentWillUnmount
と分けてロジックを定義する必要がある。上の例でいうとcomponentDidMount で設定したタイマーをcomponentWillUnmountでクリアしている。処理が漏れそうでバグを生みやすい。

・componentDidUpdateはpropsとstateが更新されると呼ばれるので、
「count」が更新された時だけ、などに処理を走らせたい時も関数内で分岐を書かなくてはいけない。

useEffect使用して書き換えたコンポーネントの場合

import React, { useEffect, useState } from "react";

const CountWrapper = () => {
 const [isShowCounter, setIsShowCounter] = useState(true);
 return (
   <>
     <p>
        // ここを押してカウンターを出したり消したりする
        // カウント、タイマーがクリアされていることを確認する
       <button
         onClick={() => {
           setIsShowCounter(!isShowCounter);
         }}
       >
         counter toggle
       </button>
     </p>
     {isShowCounter && <Counter />}
   </>
 );
};

const Counter = () => {
 const [count, setCount] = useState(0);
 const [timerCount, setTimerCount] = useState(0);

 useEffect(() => {
   console.log("コンポーネントをマウントしたよ");
   const id = setInterval(() => { // タイマー
     setTimerCount((prev) => prev + 1);
   }, 500);
   return () => { // ここに関数を渡すと、アンマウント or 副作用が再実行される前に呼ばれる
     console.log("コンポーネントをアンマウントするよ");
     clearInterval(id);
   };
 }, []); // 第二引数に空の配列で、マウント時に1回だけ呼ばれる
 useEffect(() => {
   console.log("countをupdateしたよ");
 }, [count]); // ここに入れた値が変化した時のみ、この副作用が呼ばれる
 useEffect(() => {
   console.log("timerCountをupdateしたよ");
 }, [timerCount]);
 useEffect(() => {
   console.log("count,timerCountをupdateしたよ");
 }, [count, timerCount]); // 複数も指定できる

 return (
   <div>
     <p>count: {count} 回</p>
     <p>timerCount: {timerCount} 回</p>
     <button
       onClick={() => {
         setCount(count + 1);
       }}
     >
       count++
     </button>
   </div>
 );
};

export default CountWrapper;

・「コンポーネントがマウント、副作用が実行される」時の処理と、「コンポーネントがアンマウント副作用が再実行される」時の処理が同じ useEffect内に書ける。対になっているから、処理が漏れにくい。

componentDidUpdateと違って、変化した値(count,timerCount)ごとの副作用を用意できるので、それぞれの処理が分離してシンプルに書ける、気がする。

componentDidMount、componentDidUpdateと違ってuseEffectでスケジュールされた副作用はブラウザによる画面更新をブロックしないので表示がスムーズになる。

ちなみにuseEffect内のsetTimerCountの箇所を

  useEffect(() => {
   console.log("コンポーネントをマウントしたよ");
   const id = setInterval(() => {
     setTimerCount((prev) => prev + 1);
   }, 500);
   return () => {
     console.log("コンポーネントをアンマウントしたよ");
     clearInterval(id);
   };
 }, []);
 

の所を

 
   useEffect(() => {
   console.log("コンポーネントをマウントしたよ");
   const id = setInterval(() => {
     setTimerCount(timerCount + 1);
   }, 500);
   return () => {
     console.log("コンポーネントをアンマウントしたよ");
     clearInterval(id);
   };
 }, []);

としたら怒られて、動かなくなった。

React Hook useEffect has a missing dependency: 'timerCount'. 
Either include it or remove the dependency array. 
You can also do a functional update 'setTimerCount(t => ...)' 
if you only need 'timerCount' in the 'setTimerCount' call. 
(react-hooks/exhaustive-deps)eslint

理由がこちらに書いてあった。
https://ja.reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often

setIntervalが呼び出された時に、timerCountが0のクロージャが生成されるため、setInterval内でのコールバックでtimerCountの値が変わらなくなる。

setTimerCount(0 + 1) がずっと呼ばれ続ける。このバグをなくすために、副作用の第二引数の配列にtimerCountを入れた場合、更新の度にclearIntervalが入ってタイマーが止まってしまう。
なので、

setTimerCount((prev) => prev + 1);

こうすることで、常に最新のstateにアクセス出来る。