useEffectをちゃんと理解する

はじめに

この記事はQiita x Code Polaris共催!女性ITエンジニアが作るアドベントカレンダー Advent Calendar 202222日目の記事です。

本当は別のテーマ(Amazonほしい物リストとNotionをAPI叩いて連携させる)をやりたかったのですが、なかなか苦戦してしまい、違うテーマで投稿させて頂きます!いつかリベンジします!

Reactにおける再レンダリングの仕組み

本題に入る前に、Reactにおいて「再レンダリング」とは何かを復習します。

export const App = () => {
  console.log("レンダリング")

  const [count, setCount] = useState(0)
}

関数コンポーネントは、Stateが更新された時に再び頭から処理を実行する、という性質があります。その際に差分があるDOMを検知し、変更を反映して画面を表示しています。つまりこのconsole.logは最初のマウント時に呼ばれるほか、setCountが呼び出されるたびに呼び出されます。

この「変更を検知してコンポーネントを再処理」することを「再レンダリング」と呼びます。

これを hook-flow で解説されている図で見ると、

Stateが更新されると「Render」処理が走り、DOMをアップデートし、それによって画面が書き換えられる、というイメージです。

しかしこの「再び頭から処理を実行する」というのがネックで、Stateの数が増えるとレンダリングのたびに不要な処理が走ってしまう可能性があります。

useEffectとは

そこで再レンダリングのたびに処理を実行するのではなく、「特定の値が変わった時だけ処理を実行する」ために生み出されたのがuseEffectです。

useEffet(() => {
  // 実行したい副作用関数
  return () => {
    // クリーンアップ処理(あれば)
  };
}, [依存する変数]);

という構文で、第二引数の[依存する変数]の値が変更されると副作用関数を実行します。

こちらのサイトでわかりやすい例が紹介されていたので引用すると、

const App = () => {
    const [count, setCount] = useState(0);
    const [count2, setCount2] = useState(0);

    useEffect(() => {
        alert("クリックしました")
    },[count]);
  
    return (
      <div>
        <p>カウント数: {count} </p>
        <button onClick={() => setCount(count + 1)}>
          クリック
        </button>
        <p>カウント数: {count2} </p>
        <button onClick={() => setCount2(count2 + 1)}>
          クリック2
        </button>
      </div>
    );
  }

このコードの場合、setCountが呼び出されcountが変わった場合のみ、useEffectが実行されます。count2が変更されても実行されません。

副作用って何

このuseEffect、公式の説明によると「関数コンポーネント内で副作用を実行する」hooksと書かれています。

これだけ読むとなんのこっちゃ、となると思います。

まず副作用というのはどういうものかというと、UI の構築、つまり「JSX を返却する処理、および、そのための状態やイベントハンドラの定義」以外の処理 です。例えば

  • DOMの変更
  • state/propsの変更
  • 配列のpush
  • console.log()

など。これらの処理は、useEffect内に書く必要があります。

const App = () => {
  sideEffects() //ここに記述するのはNG
  useEffect(() => {
    sideEffects()
  }, [count])
}

ではなぜこれらの副作用は、useEffectの中で実行する必要があるのでしょうか?

useEffectの処理はReactがDOMを更新したさらに後に実行するのがポイントです。

例えばDOMに変更を加えたい場合、ReactがDOMを更新する前に実行してしまうと、そのDOMが存在していない可能性があります(特にマウント時の例で考えるとわかりやすいと思います)。useEffect内の関数は必ずレンダリングが完了した後に実行されるため、その中であれば安全に DOM を参照することができます

stateの更新についても、Stateが更新された時に再び頭から処理を実行するという関数コンポーネントの性質から、適切な場所で状態を更新しないと、無限ループを引き起こしてしまいます。

const App = () => {
  const [count, setCount] = useState(0)

  // 無限ループが引き起こされる
  setCount(1)
}

useEffect内でレンダリングの後に読み込むと、第二引数によって呼ばれるタイミングを制御することができます。例えば第二引数に空配列[]を指定した場合は、初回のマウント時のみに実行され、それ以降の再レンダリングのタイミングでは実行されません。

const App = () => {
  const [count, setCount] = useState(0)

  // 初回マウント時のみ実行
  useEffect(() => {
    setCount(1)
  }, []);
}

このようにReactのDOM変更や状態の更新といった処理(副作用)の実行タイミングを制御させるためのhooksがuseEffectです。

次回はuseEffectとuseLayoutEffectの違いについて書いていこうと思います。

参考