abeshi blog
カテゴリーで検索

useEffect, useCallback, memo化について改めて

2022年7月27日

useEffect



useEffectはクラスコンポーネントの以下のライフサイクルを使用できるイメージ

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount



conponentDidMount



コンポーネントのdomが生成され、その後に一度呼び出されます。
ちなみに開発環境だとcomponentDidmountは2回呼ばれるようになっています。
年の為でNext.jsの仕様のようです。


本番環境では1回しか呼ばれないそうです。
ビルドしたらわかります。

componentDidUpdate


stateの値の値が更新された時などにcomponentDidUpdateが発火します。
useEffectは第二引数に配列で関ししたい値を指定することができます。
Vueのwatchのようなもの。


useEffect(() => {
  console.log("カントが更新されました")
}, [count])
return (
  <div className={styles.home}>
    <p>トップページです</p>
    <p>{count}</p>
    <button onClick={() => setCount((count) => count + 1)}>カウントアップ</button>
  </div>
)




この時useEffectは初回のmount時、カウントの値が更新された時の2パターンに発火します


componentWillUnmount



最後にコンポーネントのアンマウント時に呼ばれます。
アンマウント ... コンポーネントが不要になり破棄される時


useEffectの中でreturn文を書くとアンマウントされます。

useEffect(() => {
    console.log('レンダーされました。');
    return () => {
        console.log('アンマウント');
    };
});




この時まず最初に"レンダーされました"のmountが発火し、その後、アンマウントが発火されます。
そしてcomponent が破棄される時、つまりページの遷移をしたりcomponentが失われる時、アンマウントが発火します。


主に

  • イベントリスナーの解除
  • タイマーの処理のキャンセル
  • DBアクセス後の接続解除


等に使われます。



useEffectを使う際の注意点


マウント時にstateを更新して無限ループになる



useEffect(() => {
  console.log("カントが更新されました")
  setCount((count) => count + 1)
}, [count])
return (
  <div className={styles.home}>
    <p>トップページです</p>
    <p>{count}</p>
  </div>
)



例えばこんな例


mount時にsetCountはcountをプラス1します。
第二引数にcountを指定しているので、countが変化したタイミングでもう一度componentDidUpdateが呼び出されてcountが増え続ける無限ループが発生するのです。


アンマウント時にstateを更新しない




これは基本的に行うタイミングがないかもしれないですが、
アンマウント時にstateを更新しても呼び出されることはありません。


componentが破棄される際に値を変更しても意味がありません。
落とし穴として開発環境ではマウントが2回発火されるので、2回目のマウントが発火される前にアンマウントも走ります。
なので開発環境で試しているとアンマウントが発火されると錯覚してしまいますが、ビルドすると発火されないことが確認できます・



useEffectの中でdomを直接操作しない



基本的にReactの実装でdomを直接触ることはNGです。
domを操作したい場合はuseRefを使いましょう。



useEffectに渡すコールバック関数をPromiseしてはいけない



const [limit, setLimit] = useState(5);

useEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts${limit}`
  );
  console.log(response);
}, [limit]);




あまり使わないかもしれないですが、下記なら動きます

useEffect(() => {
  async function fetchTest() {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?${limit}`
    );
    console.log(response);
  }
  fetchTest()
}, [limit]);




コンポーネントのmemo化




componentがサイレンダリングされる条件は3つあります。

  • stateが更新された時
  • propsが更新された時
  • 親コンポーネントがサイレンダリングされた時



stateとpropsが更新された際はサイレンダリングが起きるのは必要です。
ただ親コンポーネントが更新された時に子コンポーネントがサイレンダリングされるのは不要な場合が多いです。
これを解決するのがmemo化です。


// components/Count.tsx
import {FC} from "react";

type Props = {
  count: number;
}

export const Count: FC<Props> = (props) => {

  const { count } = props;

  console.log("サイレンダリング")

  return (
    <div>
      <p>{count}</p>
    </div>
  )
}




新たにCountコンポーネントを作成します。


import type { NextPage } from 'next'
import styles from '../styles/Home.module.css'
import {useState} from "react";
import {Count} from "../components/Count";

const Home: NextPage = () => {

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

  const countUp = () => {
    setCount((count) => count + 1)
  }

  return (
    <div className={styles.home}>
      <p>トップページです</p>
      <Count count={count}/>
      <button onClick={countUp}>カウントアップ</button>
    </div>
  )
}

export default Home



それを親コンポーネントから呼び出しましょうか。


ボタンをクリックすると、stateが更新されるのでCountコンポーネントがサイレンダリングされます。




これは通常の動作です。



次にinputFieldのイベントを親コンポーネントに書いてみましょう。



const Home: NextPage = () => {

  const [count, setCount] = useState(0);
  const countUp = () => {
    setCount((count) => count + 1)
  }

  const [text, setText] = useState("");   // 追加
  const onChangeText = (e: ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  }  // 追加

  return (
    <div className={styles.home}>
      <p>トップページです</p>
      <input onChange={(e) => onChangeText(e)}/>
      <Count count={count}/>
      <button onClick={countUp}>カウントアップ</button>
    </div>
  )
}




そうすると、inputを更新した際にCountコンポーネントがサイレンダリングしてしまうようになります。





これを予防するのがmemo化です。

import {FC, memo} from "react";

type Props = {
  count: number;
}

export const Count: FC<Props> = memo((props) => {

  const { count } = props;

  console.log("サイレンダリング")

  return (
    <div>
      <p>{count}</p>
    </div>
  )
})

Count.displayName = "Count"



Countコンポーネントをmemoで囲むことで、親コンポーネントが更新された際にサイレンダリングを防ぎます。
Next.jsのデフォルトのESLintの設定だと、無名関数になっているエラーが出るので

Count.displayName = "Count"



をつければ一旦解決します。
設定をオフにしても良いかと思います。




ただしこれで全ての問題が解決したわけではありません。
countUpの関数の処理をCountにpropsで渡してみましょう。

<input onChange={(e) => onChangeText(e)}/>
<Count count={count} countUpFunc={countUp}/>



// components/Count.tsx
export const Count: FC<Props> = memo((props) => {

  const { count, countUpFunc } = props;

  console.log("サイレンダリング")

  return (
    <div>
      <p>{count}</p>
      <button onClick={countUpFunc}>カウントアップ</button>
    </div>
  )
})









memo化したのにもかかわらずまたCountがinoutフィールドのレンダリングに反応して更新されてしまいます。


useCallback




この原因として関数がmemoかされていないことが挙げられます。

const countUp = () => {
  setCount((count) => count + 1)
}



countUpの関数は今の状態では、inputがonChangeされた際に関数が再生成されます。
関数が再生成されたことをpropsが更新されたとCountコンポーネントが勘違いし、サイレンダリングされてしまうのです。


それを解決するのがuseCallbackです。

useCallbackは関数のmemo化です。

const countUp = useCallback(() => {
  setCount((count) => count + 1)
}, [count]);



第二引数には関しする値をつけます。
こうすることで, countUpに関数はcountの値が変わった時だけ、関数が再生成されるようになります。


これで不要なレンダリングが防げます。



つまりmemo化とuseCallbackはセットで使いましょう!!!
ということです。