45
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-09-08

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

細かく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を併用する。

45
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?