Help us understand the problem. What is going on with this article?

react hooksを使ったレンダリングの動き

どのコンポーネントがレンダリングされるのか?

細かくrenderの動きを検証できていなかったので備忘としてまとめます。

FCのレンダリング

単純なstate操作で再レンダリングさせてみて、子コンポーネントも再レンダリングされるか確認してみる。

単純なFC

とくに何も気にしない、ただのTestコンポーネントを作って、中にコンソールログを仕込んだ。
こうすることで再レンダリングされているかどうかをコンソールのログを見てチェックする。

function App() {
  const [isRender, setIsRender] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <Test />
    </div>
  );
}

const Test = () => {
  console.log("Test");
  return <div>Test</div>;
};

test.gif

親がレンダリングされるたびに、このコンポーネントもレンダリングされる。

memoを使う

次に、React.memoを使うことで再レンダリングされないことを確認する。

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [isRender, setIsRender] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <Test />
    </div>
  );
}

const Test = memo(() => {
  console.log("Test");
  return <div>Test</div>;
});

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Test2.gif

これだと、親が再レンダリングされても問題なく子はレンダリングされない。

memoにpropsを渡す

propsの比較をmemoで行う。
isRender2っていうstateを作ってそれをTestのpropsとして渡すようにした。

import React, { useState, memo } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [isRender, setIsRender] = useState(0);
  const [isRender2, setIsRender2] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <button onClick={() => setIsRender2(isRender2 + 1)}>increment2</button>
      <Test cnt={isRender2} />
    </div>
  );
}

const Test = memo(({ cnt }) => {
  console.log("Test");
  return <div>Test</div>;
});

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Test3.gif

すると、意図したとおりisRender2が変化したときのみレンダリングが走る。

contextを使う

hooksになってからcontextを使ってみようと思っているが、レンダリングがどのタイミングで動くのか検証してみた。
まずはシンプルにProviderのvalueを受け取ってみる。

function App() {
  const [isRender, setIsRender] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <TestContext.Provider value={{ isRender }}>
        <Test />
      </TestContext.Provider>
    </div>
  );
}

const Test = () => {
  console.log("Test");
  const { isRender } = useContext(TestContext);
  return <div>Test</div>;
};

Test$.gif

これはmemoとかもしてないので、毎回レンダリングされるのは納得。

contextとmemoを使う(propsなし)

Consumer側にmemoをつけるとどうなるか。

function App() {
  const [isRender, setIsRender] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <TestContext.Provider value={{ isRender }}>
        <Test />
      </TestContext.Provider>
    </div>
  );
}

const Test = memo(() => {
  console.log("Test");
  const { isRender } = useContext(TestContext);
  return <div>Test</div>;
});

Test4.gif

思ったとおりではあったが、再レンダリングされた。

contextとmemoを使う(propsあり)

では比較対象のpropsを渡して、それの変化がない場合にどう動くのか見てみる

function App() {
  const [isRender, setIsRender] = useState(0);
  const [isRender2, setIsRender2] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <button onClick={() => setIsRender2(isRender2 + 1)}>increment2</button>
      <TestContext.Provider value={{ isRender }}>
        <Test cnt={isRender2}/>
      </TestContext.Provider>
    </div>
  );
}

const Test = memo(({ cnt }) => {
  console.log("Test");
  const { isRender } = useContext(TestContext);
  return <div>Test</div>;
});

Test6.gif

memoでpropsを比較して入るが、Providerから渡されるvalueが変化しても再レンダリングされるみたい。
なるほど。

contextのProviderの値が変化しない場合

Providerから提供されるvalueが変化しない場合。

function App() {
  const [isRender, setIsRender] = useState(0);
  const [isRender2, setIsRender2] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <button onClick={() => setIsRender2(isRender2 + 1)}>increment2</button>
      <TestContext.Provider value={{ isRender2 }}>
        <Test />
      </TestContext.Provider>
    </div>
  );
}

const Test = memo(() => {
  console.log("Test");
  const { isRender2 } = useContext(TestContext);
  return <div>{isRender2}</div>;
});

test8.gif

想像と違って動く。
なんでだ。
Providerをmemoしてないから?
やってみる。

function App() {
  const [isRender, setIsRender] = useState(0);
  const [isRender2, setIsRender2] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <button onClick={() => setIsRender2(isRender2 + 1)}>increment2</button>
      <TestProvider isRender2={isRender2} />
    </div>
  );
}

const TestProvider = memo(({ isRender2 }) => {
  return (
    <TestContext.Provider value={{ isRender2 }}>
      <Test />
    </TestContext.Provider>
  )
});

const Test = memo(() => {
  console.log("Test");
  const { isRender2 } = useContext(TestContext);
  return <div>{isRender2}</div>;
});

test9.gif

こうすれば動かないのか。
reduxでconnectしたコンポーネントはPureComponentになるんだけど、contextはちゃんとmemoしてあげないとだめなんだねー。

子コンポーネントをmemo化して、useContextを使わない場合

気になったのでやってみる。

function App() {
  const [isRender, setIsRender] = useState(0);
  const [isRender2, setIsRender2] = useState(0);
  return (
    <div className="App">
      <div>{isRender}</div>
      <button onClick={() => setIsRender(isRender + 1)}>increment</button>
      <button onClick={() => setIsRender2(isRender2 + 1)}>increment2</button>
      <TestContext.Provider value={{ isRender2 }}>
        <Test />
        <Test2 />
      </TestContext.Provider>
    </div>
  );
}

const Test = memo(() => {
  console.log("Test");
  const { isRender2 } = useContext(TestContext);
  return <div>{isRender2}</div>;
});

const Test2 = memo(() => {
  console.log("Test2");
  return <div>Test2</div>;
});

Test10.gif

これではっきりわかったのが、useContextを使った子コンポーネントは、Providerがmemoされてvalueの比較がされていない場合はレンダリングされるが、useContextを使っていないコンポーネントは何も影響を受けない。

関数の定義でのレンダリング

よくパフォーマンスを落とすと言われているやつ。
ダメなやつと、上で定義してみたやつと、usaCallback使ったパターン。
受け取るコンポーネントはmemo化している。

function App() {
  const [isRender, setIsRender] = useState(0);
  const log = () => console.log("Test2");
  const log2 = useCallback(() => console.log("Test3"), []);
  return (
    <div className="App">
      <button onClick={() => setIsRender(isRender + 1)}>render</button>
      <Test log={() => console.log("Test1")} />
      <Test log={log} />
      <Test log={log2} />
    </div>
  );
}

const Test = memo(({ log }) => {
  log();
  return <div>Test1</div>;
});

test11.gif

これは今までの検証から想像通り。
useCallback使ってないと、再定義されて別の関数として判定してしまう。

useCallbackでmemoしてない場合

ちょっと気になったので検証
memoしなかったらレンダリングされちゃうよね?

function App() {
  const [isRender, setIsRender] = useState(0);
  const log2 = useCallback(() => console.log("Test3"), []);
  return (
    <div className="App">
      <button onClick={() => setIsRender(isRender + 1)}>render</button>
      <Test log={log2} />
    </div>
  );
}

const Test = ({ log }) => {
  log();
  return <div>Test1</div>;
};

test12.gif

やっぱりされた。

まとめ

レンダリングでパフォーマンスを上げたいと思ったときにはまず、
・momoを使ってみる
→他の記事では使わない場合のほうがパフォーマンスが上がるときもあるよう
・contextと使う場合はProviderをmemo化する
・関数を定義する場合は、useCallbackとmemoを併用する。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away