okinawa

IT勉強メモ

React hook基礎

参考

qiita.com

qiita.com

qiita.com

React hookとは

状態管理や副作用などのReactの機能を、クラスを書かずに使えるようにする機能。

かつては状態管理や副作用を使うにはクラスコンポーネントとというものを利用していたらしい。

関数コンポーネントの純粋性を保つために、状態管理や副作用を、関数コンポーネント内ではなく、React hookに任せる。

React hookのイメージ

・参考
初心者向け React フックの基礎

Reach hookを理解するにはコンポーネントが関数であることを思い出すと理解しやすい。

React hookは基本的に関数の外にあるものを管理する。

例えば、Stateは関数外の変数。

関数コンポーネントはステートレスな純粋関数だから、Stateは関数外部で管理するということらしい。

useState()は関数外部のgetter/setterを呼び出しているイメージ。

でも、純粋関数って関数外部の変数を参照も変更もしないはず。。。よくわからず。

前提として知っておきたいこと

1,関数コンポーネントはStateが更新される度に再実行(再レンダリング)される。
→useMemoで必要な知識

2.親コンポーネントが再実行(再レンダリング)されると、子コンポーネントも再実行される。

3.コールバック関数を Props として受け取った関数コンポーネントは必ず再レンダリングされる。
→useCallbackで必要な知識

→useMemoを使えば再レンダリングされなかった。React.memoだとされる。

4.JSXの <Child /> はコンポーネント呼び出し
関数呼び出しと同じ意味。

5.再レンダリング=関数コンポーネント再実行

6.関数コンポーネント再実行されると、関数内のオブジェクト(コールバック関数含む)は再生成され、参照値が変わる。→React.memoで必要な知識

//Example関数実行の度に、中のオブジェクトは再生成され、参照値が変わる
const Example = () => {
  const obj = {name: "takeshi", age:20};// オブジェクト
  const callbackFunc = () => {
    console.log("関数オブジェクト");
  }
  return (
    <h3>abc</h3>
    );
};

useState

useStateは、関数コンポーネント内で状態(State)管理を行うためのフックです。具体的には、変数を扱うことができます。

状態(State)を更新すると、関数コンポーネントが再実行(再レンダリング)され、即座にブラウザ上にも反映されます。

・構文

const [状態変数, 状態更新用関数] = useState(状態の初期値);

const [state, setState] = useState(0);//初期値0で初期化
seState(1); // stateを1に更新

コード例

setCountメソッドを使ってcountの値を更新できます。例えば、ボタンをクリックしたらcountを増減させることができます。

    import React, { useState } from 'react';
    function Counter() {
      const [count, setCount] = useState(0);//初期化
      const [str, setStr] = useState("Hello!");//文字列も可

      const increment = () => setCount(count + 1);//状態更新
      const decrement = () => setCount(count - 1);//状態更新

      return (
        <div>
          <h1>Counter</h1>
          <h2>カウント: {count}</h2>
          <button onClick={increment}>+</button>
          <button onClick={decrement}>-</button>
        </div>
      );
    }

Stateの特徴と注意点

■特徴

  • ステートが変わると再レンダリングされる(重要!!!)
  • ステートは再レンダリング後も保持。F5で画面全体再更新すると消える。
  • ステートはコンポーネント毎に保持
  • ステートはpropsで渡せる
  • ステートを更新する時は更新用関数を使う

■注意点

  • コンポーネントの中で呼び出す
  • if/for文の中で呼び出さない
  • 状態更新は即時ではない(予約)
  • オブジェクト型のステート変更する時は全プロパティを変更する
  • オブジェクト型と配列のステート変更時は、新しいオブジェクトを渡す
  • コンポーネントが消えるとステートも初期化される

Stateについて詳しくはこちら↓
React基礎 - okinawa

useEffect

useEffectは、関数コンポーネント内で副作用を実行するためのフックです。

副作用とは、fetch関数を使って外部のリソースからデータを取得したり、DOMの更新、ロギング(console.logも含む)などの処理を指します。

・構文

useEffect(() => {
  // 依存配列変更後に実行したい処理
  console.log("副作用処理")
},[依存配列]);

依存配列に変更があった時のみ、useEffect内の処理が実行されます。

依存配列未指定の場合は、初レンダリング時のみ実行されます。

useEffectの使い所

State変更後に何らかの処理を実行したい時。

コード例

ボタン押す度にcountが変更し、useEffect内の処理が実行される。

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

  useEffect(() => {
    console.log("useEffect内実行");
  }, [count]);//Stateであるcountが依存配列

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>ボタン</button>
    </>
  )

}

export default Example;

useMemo

useMemoは関数の計算結果を保存する。

関数自体を保存するわけではなく、値だけを保存する。(useCallbackは関数自体を保存する)

数値でも文字列でもJSXでもなんでも保存できますよ。

・構文

  useMemo(() => {
    return 重い処理
  },[依存配列])

依存配列に変更があったときのみ、useMemo内の処理が実行される。
そして、return結果がMemoに保存される。
依存配列未指定の場合は、初回レンダリング時のみ実行される。

useMemoの使い所

重い処理の結果を保存しておき、再利用する。

例えば、関数Aの中で2つの関数を呼び出していたとして

  • 処理の重い関数B
  • 処理の軽い関数C

関数Aを呼び出すと、BもCもどちらも実行される。

でも、重いBは必要なときだけ呼び出したいんだよなあ、という時に使う。

function 関数コンポーネントA() {
  // 関数Bの処理は非常に重いので、必要なときだけ呼び出したい
  関数B();

  // 関数Cの処理は軽いので、毎回呼び出してOK
  関数C();
}

例:5億回のループは必要なときだけにしたい

・useMemoを使わない場合

軽い処理ボタンを押すと、lightStateが更新される。

Stateが変わると再レンダリング、つまり関数Exampleが再実行されるので、heavyFunc(重い処理)も呼び出されてしまい、レンダリングに時間がかかる。

import { useEffect, useCallback, useMemo, useState } from 'react';

//Stateが更新されるとExample関数が再実行される
const Example = () => {
  const [heavyState, setHeavyState] = useState(1);
  const [lightState, setLightState] = useState(0);

  const lightFunc = () => {
    setLightState(prev => prev + 1);
    return lightState;
  }

  //heavyState * 5億回のループ
  const heavyFunc = (heavyState) => {
    let result = 0;
    for (let i = 0; i <= heavyState * 500000000; i++) {
      result = i;
    }
    return result;
  }

  // レンダリングの度に重い処理を呼び出す
  const result2 = heavyFunc(heavyState);

  return (
    <>
      <p>{lightState}</p>
      <button onClick={lightFunc}>軽い処理</button>
      <p>{heavyState} * 500000000 = {result2}回のループ</p>
      <button onClick={() => setHeavyState(prev => prev + 1)}>重い処理</button>
    </>

  );
};

export default Example;

↓ useMemo()を使うバージョン

useMemoでheavyFuncの結果を保存して再利用可能にしておく。

heavyFuncは、heavyStateの値が変わった時、つまり重い処理ボタン押したときのみ実行される。

import { useEffect, useCallback, useMemo, useState } from 'react';

//Stateが更新されるとExample関数が再実行される
const Example = () => {
  const [heavyState, setHeavyState] = useState(1);
  const [lightState, setLightState] = useState(0);

  const lightFunc = () => {
    setLightState(prev => prev + 1);
    return lightState;
  }

  //heavyState * 5億回のループ
  const heavyFunc = (heavyState) => {
    let result = 0;
    for (let i = 0; i <= heavyState * 500000000; i++) {
      result = i;
    }
    return result;
  }

  // 変更箇所はここのみ
  // useMemoで結果をキャッシュする
  // Example関数が再実行されても、heavyState変わってなければ、再実行されない
  // 再実行しない場合はキャッシュした結果を使う
  const result2 = useMemo(() => {
    return heavyFunc(heavyState);
  },[heavyState])


  return (
    <>
      <p>{lightState}</p>
      <button onClick={lightFunc}>軽い処理</button>
      <p>{heavyState} * 500000000 = {result2}回のループ</p>
      <button onClick={() => setHeavyState(prev => prev + 1)}>重い処理</button>
    </>

  );
};

export default Example;

useMemoの注意点

コンポーネントをメモ化する場合は注意。

特にコンポーネント丸ごとuseMemoに入れちゃえばええやん!とやるとハマりがち。

以下のコードは、ボタンクリックでコンソールに「click」と表示するものだが、機能しない。

なぜなら、ボタンが丸ごとメモ化されているので、依存配列が変わらない限り、useMemo内の処理は実行されない。

こういう時はReact.memoを使う。

import React, { useEffect, useCallback, useMemo, useState } from 'react';

function Child({handleClick}) {
  const memo = useMemo((handleClick) => {
    return <button onClick={handleClick}>Child</button>; 
  },[handleClick]);//依存配列に関数を入れれば行けそうな気がするがダメ。クリックしても関数自体が変化するわけではないので
  return memo;
}


export default function App() {

  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <Child handleClick={handleClick} />
    </>
  );
}

React.memoとは

※React.memoはhookではないが、useMemoと似ていてごっちゃになるので記載する。

React.memoは不要な再レンダリングを防ぐためのもの。

コンポーネントのpropsが変更された場合のみ、再レンダリングを行う。

propsが変更されない限り、前回のメモを再利用して再レンダリングを回避する。

React.memoとuseMemoの使い所

・使い所イメージ(React.memo)

親コンポーネントA {
  // 重いレンダリング処理B
  子コンポーネントB()

  // 軽い処理
  関数C()
}

親実行時に子Bが再レンダリングされるのを回避したいとき使う。-

・React.memoとuseMemoの再実行条件
useMemoは依存配列が変わった時のみ。

React.memoはpropsが変わった時。ちと複雑。

・propsの変更判定について
propsの値が変わってなくても、参照値が変わっていれば変更したと判定される。

例えば、親コンポーネント関数が再実行されると、親から渡されるpropsも再生成されるので、参照値が変わる。

参考
qiita.com

コード例

・React.memo未使用↓
parentボタンを押すとChildコンポーネントが再レンダリングされてしまう。

function Child() {
  console.log("Child再実行");
  return <p>Childコンポーネント</p>
}

export default function App() {

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

  const countUp = () => {
    setCount(prev => prev + 1);
  }

  return (
    <>
      <p>{count}</p>
      <button onClick={countUp}>parent</button>
      <Child />
    </>
  );
}

・React.memo使用↓
parentボタン押しても、Chilidが再レンダリングされない。

const Child = React.memo((props) => {
  console.log("Child再実行")
  // コンポーネントをメモ化
  return (
    <>
      <p>Childコンポーネント</p>
    </>
  );
});

export default function App() {

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

  const countUp = () => {
    setCount(prev => prev + 1);
  }

  return (
    <>

      <p>{count}</p>
      <button onClick={countUp}>parent</button>
      <Child />
    </>
  );
}

React.memoとuseMemoの違いまとめ

どっちも結果をメモする機能。

  1. useMemo:

    • useMemoは、値をメモ化するためのReactフックです。
    • 計算結果の値をキャッシュし、同じ計算を繰り返さないようにします。
    • 例えば、重い計算やデータの加工が必要な場合、その結果を保持して再利用できます。
    • 依存配列の変更に応じて再計算を行います。
  2. React.memo:

  3. useMemoの例:

    const expensiveValue = useMemo(() => {
      // 重い計算やデータの加工
      return computeExpensiveValue();
    }, [dependency1, dependency2]);
  • React.memoの例:
    const MyComponent = React.memo(props => {
      // レンダリングコストが高いコンポーネント
      return <div>{props.data}</div>;
    });

React.memoの詳細についてはこちら↓
React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする #JavaScript - Qiita

両者の違いはこちらがわかりやすい↓
yukimasablog.com

React.memoの注意点(意図しない再レンダリング

propsの値が変わるとメモを使用せず、再実行してしまう。

中身の値が変わらず、参照値が変わるだけでも再実行。こういう時はuseCallbackを使うと再実行を防げる

import React, { useEffect, useCallback, useMemo, useState } from 'react';

const Child = React.memo(({func}) => {
  //メモ内に親から渡されたオブジェクトがあるので、親再実行すると、memo内処理も実行される。
  console.log("メモ内実行");
  return <button onClick={func}>Child</button>;
});

export default function App() {
//再レンダリング時に関数オブジェクトが再生成され、参照値が変わる
  const callbackFunc = () => {
    console.log("関数オブジェクト");
  }
  return (
    <Child object={func={callbackFunc} />
  )

面倒だから全部useMemoに統一したいけどダメ?

残念ながらダメ。

useMemoの注意点で書いたケースはReact.memoを使う。

useCallback

※React.memoとの併用で使う。useMemoしか使っていないならuseCallbackの使い所はほぼない。

React.memoの特徴としてメモ内のpropsに変更があると、再レンダリングされる。

例えばコールバック関数がpropsとして渡されていると、親コンポーネントレンダリング→propsも再生成で参照値変わる→React.memoしてても子コンポーネントレンダリング

これを防ぐためにuseCallbackを使う。

詳しくは、リンクの「Reactコンポーネントの再レンダリング条件検証」を見るとわかる。

useCallbackの使い所

React.memoしているのに、子コンポーネントが再レンダリングしてしまうのを防ぎたい時。

関数をメモ化(保存)することで、参照値が変わらないようにする。

・使用方法
子にpropsとして渡すコールバック関数を親側でメモ化しておき、親再レンダリング時にコールバック関数を再生成しない。

そうすると、メモされたコールバック関数は、毎回同じオブジェクト判定なので、子のReact.memoに渡しても、React.memo内が再実行されない。(子が再レンダリングされない)

・構文

const memoCallbackFunc = useCallback(() => {
    // 第一引数は何らかの処理
    console.log("あいうえお");
  },[依存配列])

依存配列に変更があったときのみ、useCallback内の処理を保存し直す。(処理を実行はしない)
処理自体が保存されるので、returnはなくてもよい。
依存配列未指定の場合は、初回レンダリング時のみ保存される。

・useCallback未使用の例

参考
React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする #JavaScript - Qiita

import React, { useState } from "react";

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数はコンポーネントが再レンダリングされる度に再生成されるため、
  // 関数の内容が同じでも、新しい handleClick と前回の handleClick は
  // 異なるオブジェクトなので、等価ではない。
  // そのため、コンポーネントが再レンダリングされる。
  const handleClick = () => {
    console.log("click");
  };

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

・useCallback使用例

参考

React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする #JavaScript - Qiita

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

const Child = React.memo(props => {
  console.log("render Child");
  return <button onClick={props.handleClick}>Child</button>;
});

export default function App() {
  console.log("render App");

  const [count, setCount] = useState(0);
  // 関数をメモ化すれば、新しい handleClick と前回の handleClick は
  // 等価になる。そのため、Child コンポーネントは再レンダリングされない。
  const handleClick = useCallback(() => {
    console.log("click");
  }, []);

  return (
    <>
      <p>Counter: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment count</button>
      <Child handleClick={handleClick} />
    </>
  );
}

まとめ

  1. useState:状態管理
  2. useEffect:状態変更後に実行したい処理
  3. useMemo:重い処理の結果保存
  4. React.memo:重いコンポーネント保存
  5. useCallback:コールバック関数保存。React.memoと併用

React.memoとuseCallbackの関係だけややこしい。

  • React.memoとuseCallbackはセットで使うよ
  • React.memoの再実行条件はpropsオブジェクトに変更があった時
  • propsオブジェクトは、参照値が変わっただけで変更したと判定されるよ
  • だからuseCallback使って参照値も変わらないようにするよ