どのコンポーネントがレンダリングされるのか?
細かく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>;
};
親がレンダリングされるたびに、このコンポーネントもレンダリングされる。
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);
これだと、親が再レンダリングされても問題なく子はレンダリングされない。
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);
すると、意図したとおり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>;
};
これは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>;
});
思ったとおりではあったが、再レンダリングされた。
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>;
});
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>;
});
想像と違って動く。
なんでだ。
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>;
});
こうすれば動かないのか。
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>;
});
これではっきりわかったのが、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>;
});
これは今までの検証から想像通り。
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>;
};
やっぱりされた。
まとめ
レンダリングでパフォーマンスを上げたいと思ったときにはまず、
・momoを使ってみる
→他の記事では使わない場合のほうがパフォーマンスが上がるときもあるよう
・contextと使う場合はProviderをmemo化する
・関数を定義する場合は、useCallbackとmemoを併用する。