【React】childrenを用いたパフォーマンス最適化
React.memo
を使用する他に、children
で要素を受け取ることによって子コンポーネントの再レンダリングを抑制することができます。この手法はContext
を用いる際によく出てくるため、意識せずとも使用しているケースは多いのではないでしょうか?
例えば、以下のようなThemeProvider
コンポーネントの状態が更新されたとしてもuseContext
で値を受け取っているコンポーネント以外は再レンダリングされません。これはchildren
で要素を受け取って出力しているためです。
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState("light")
const themeValue = useMemo(() => ({ theme, setTheme }), [theme])
return (
<ThemeContext.Provider value={themeValue}>
{children}
</ThemeContext.Provider>
)
}
const App = () => {
return (
<ThemeProvider>
<SomeComponent />
</ThemeProvider>
)
}
再レンダリング抑制にはいくつかの手法がありますが、本記事ではどういった時にchildren
でパフォーマンス最適化したほうが良いのかについて考えていきたいと思います。
再レンダリングの抑制
以下のようなコンポーネントで想定します。
const CountPage: React.FC = () => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>カウントアップ</button>
<p>{count}</p>
<Body />
<CounterFooter count={count} />
</div>
)
}
親コンポーネントが再レンダリングされると全ての子コンポーネントが再レンダリングされるため、count
を更新すると状態を受け取っていない<Body />
も再レンダリングされます。
count
を複数のコンポーネントが依存しているため、状態管理を別コンポーネントに切り分けることもできません。
このような時に<Body />
の再レンダリングを抑制するいくつかの方法を見ていきます。
1. React.memoでメモ化する
よくあるパフォーマンス対策の一つとして、Body
コンポーネントをReact.memo
でメモ化します。
第2引数を使用しなければバグを生み出すことは基本的には無いため、必要最小限の労力で再レンダリングを抑制できます。
このコンポーネント内の値に<Body />
が依存している場合、他の選択肢が限られるためほぼこの手法を使うことになると思います。
2. childrenで子要素として受け取る
そもそも今回の場合は<Body />
はこのコンポーネント内の値に依存していません。ならば、このコンポーネント内で<Body>
を呼び出す必要がなくchildren
で子要素として受け取る設計にすることもできます。
const CountPage: React.FC = () => {
return (
<CountPageLayout>
<CountBody />
</CountPageLayout>
)
}
type Props = {
children: React.ReactElement;
}
const CountPageLayout: React.FC<Props> = ({children}) => {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>カウントアップ</button>
<p>{count}</p>
{children}
<CounterFooter count={count} />
</div>
)
}
こうすることでコンポーネントとして状態の依存が明確になる上に、コンポーネントではなく要素を受け取ることによって<Body />
は再レンダリングされず、パフォーマンス対策にもなります。
ちなみにchildren
は公式のコンポジション vs 継承 – Reactで解説されている通り、独自に定義したPropsで要素を渡しても問題ありません。
3. Context or 状態管理ライブラリを使用する
そもそも複数のコンポーネントでcount
を利用するのだから共有しやすいように状態管理すべきだ。という場合はContextやRedux、Recoil等を使用し、親コンポーネントで状態を管理しないようにすれば<Body />
の再レンダリングを抑えることができます。
count
の状態に依存するコンポーネントを切り出し、対象のコンポーネント内でそれぞれ状態を参照するように修正します。
const CountPage: React.FC = () => {
return (
<div>
<Count />
<Body />
<Footer />
</div>
)
}
ただしこの方法はプロジェクトにおける状態管理のルールに左右されやすく、コード量が増えて複雑になりがちなデメリットはあります。今回の場合はcount
の状態管理の要件が膨れてきた場合に移行を検討するぐらいが良いかなと思います。
まとめ
パフォーマンス対策はコンポーネント設計・状態管理設計に大きく影響を受けるため、常にこれが正しいといった手法を決めるのは難しいように思えますが、頻出するパターン自体はあります。
パフォーマンス対策を考える際にchildren
等で要素を受け取る事によって、再レンダリングを抑えることができないかを考えることも選択肢の一つとして持っても良いのでは無いかと思います。
Discussion