Haramaki0326 / StudyToDo

2021年にチャレンジ、やりたいことリスト
0 stars 0 forks source link

React Hooks(useState, useEffect, memo, useCallback, useMemo) #79

Open Haramaki0326 opened 2 years ago

Haramaki0326 commented 2 years ago

useState

使い方

import { useState } from 'react';
import {} from "./components/ColoredMessage";

export const App = () => {
  const onClickButton = () => {
    alert();
    setNum(num + 1);    // これでも動く
    setNum((prev) => prev + 1);    // 推奨
  }

  return (
    <>
      <h1 style={{ color: 'red' }}>こんにちは!</h1>
      <ColoredMessage color='blue' message='お元気ですか?' />
      <button onClick={onClickButton}> ボタン </button>
      <p>{num}</p>
    </>
  );
};
Haramaki0326 commented 2 years ago

useEffect

概要

使い方

Haramaki0326 commented 2 years ago

再レンダリングの仕組み

概要

基本的にReactで再レンダリングが起きるタイミングは以下の3つ。

  1. stateが更新された時
  2. propsが更新された時
  3. 親コンポーネントが再レンダリングされた時

stateが更新された時

import { useState } from "react";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");

  const changeText = (e) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>App component</p>
      <input type="text" onChange={changeText} />
    </>
  );
}

上記のコードで空欄に文字を入力・削除してみると、処理回数に応じてconsoleに App が繰り返される。 すなわち、stateが変更されることで再レンダリングが発生している。

propsが更新された時

src/App.jsx

import { useState } from "react";
import Child from "./components/Child";

export default function App() {
  const [count, setCount] = useState(0);

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

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

src/components/Child.jsx

const Child = (props) => {
  const { count } = props;
  console.log("Child");

  return (
    <>
      <p>Child component</p>
      {count}
    </>
  );
};

export default Child;

親であるAppコンポーネントから子であるChildコンポーネントへcountというpropsを渡している。 ブラウザで実行してcount upボタンを押すと数字が1ずつ増えていき、consoleにはChildが回数分表示される。 すなわち、propsが変更されることで再レンダリングが発生している。

親コンポーネントが再レンダリングされた時

これは「親コンポーネントで再レンダリングが発生すると、その配下にある子コンポーネントが全て再レンダリングされてしまう」というもの。

src/App.jsx

import { useState } from "react";
import Child from "./components/Child";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");

  const changeText = (e) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>App component</p>
      <input type="text" onChange={changeText} />
      <br />
      <Child />
    </>
  );
}

src/components/Child.jsx

const Child = () => {
  console.log("Child");

  return (
    <>
      <p>Child component</p>
    </>
  );
};

export default Child;

上記のコードでは親子間でのpropsの受け渡しはないが、ブラウザ上で空欄に文字を入力・削除すると(親であるAppコンポーネントに記述されているinput要素に値を入力すると)、consoleにAppとChildが繰り返し表示されることが確認できる。 すなわち、親コンポーネントが再レンダリングされているタイミングで子コンポーネントも再レンダリングされている。 親子間で値の受け渡しが無いのにも関わらず、意図せずこのような再レンダリングが発生してしまうことで、パフォーマンスが下がってしまう。

Haramaki0326 commented 2 years ago

再レンダリングを最適化する

再レンダリングを最適化する、すなわち無駄な計算や処理を抑えるために必要なReactの機能が以下の3つ。

  1. memo
  2. useCallback
  3. useMemo

これらの機能を用いることで、メモ化(計算結果を保持し、それを再利用すること)ができる。 同じ結果を返す処理に関しては初回のみ処理を実行しておき、2回目以降は前回の処理結果を呼び出すことで毎回同じ処理を実行しなくてよくなる。

memo

以下のコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるAppコンポーネントに記述されているinput要素に値を入力すると)、consoleにはAppのみが繰り返し表示される。(メモ化していない先ほどのコードではAppとChildが交互に繰り返されていた) Childコンポーネントはメモ化されているので、Childはconsoleに表示されない。

import { useState } from "react";
import Child from "./components/Child";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");

  const changeText = (e) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>App component</p>
      <input type="text" onChange={changeText} />
      <br />
      <Child />
    </>
  );
}
import { memo } from "react";

const Child = memo(() => {
  console.log("Child");

  return (
    <>
      <p>Child component</p>
    </>
  );
});

export default Child;

useCallback

useCallbackはメモ化したコールバック関数を返すHooks API。 次に、以下のようなコードを実行してみる。

import { useState } from "react";
import Child from "./components/Child";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");

  const changeText = (e) => {
    setText(e.target.value);
  };

  return (
    <>
      <p>App component</p>
      <br />
      <Child changeText={changeText} />
    </>
  );
}
import { memo } from "react";

const Child = memo((props) => {
  console.log("Child");
  const { changeText } = props;

  return (
    <>
      <input type="text" onChange={changeText} />
      <p>Child component</p>
    </>
  );
});

export default Child;

先ほど親コンポーネント(App)で処理していたinput要素を子コンポーネント(Child)に移動させ、関数changeTextもpropsで受け渡している。 以下のコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるAppコンポーネントに記述されているinput要素に値を入力すると)、再びconsoleにAppとChildが繰り返し表示される。 「Childコンポーネントをメモ化しているのになぜ?」

この原因はpropsで受け渡した関数にある。 親コンポーネントで生成した関数をpropsで子コンポーネントに渡すと、関数の内容が同じでも子コンポーネントでは「毎回新しい関数が渡されている」と判断されてしまう。 そこでuseCallbackを使う。

import { useState, useCallback } from "react";
import Child from "./components/Child";

export default function App() {
  console.log("App");
  const [text, setText] = useState("");

  const changeText = useCallback(
    (e) => {
      setText(e.target.value);
    },
    [setText]
  );

  return (
    <>
      <p>App component</p>
      <br />
      <Child changeText={changeText} />
    </>
  );
}

以上のように関数(changeText)をuseCallbackで囲み、第2引数には配列を設定できる。(useEffectと同様) このコードをブラウザで実行し、空欄に文字を入力・削除すると(親であるAppコンポーネントに記述されているinput要素に値を入力すると)、consoleにはAppのみが繰り返し表示される。

useMemo