【実務・中級編】 Zustandにおけるセレクターによるレンダリング最適化 – React実践ガイド

やあ、調子はどうだい?
Reactでの状態管理、日々の開発で頭を悩ませていないかい?

「とりあえず`useState`で全部持って、子コンポーネントにプロパティをバケツリレーしていくうちに、何がなんだかわからなくなった」
「グローバルな状態管理を導入したはいいものの、画面のちょっとした入力のたびにアプリ全体が再レンダリングされて、モッサリした動作になった」

……おいおい、ため息をつくのはまだ早い。中級からもう一歩上のシニアの領域へステップアップしようとしている君なら、一度はぶち当たる壁だ。今日はそのモヤモヤを綺麗に解消しよう。

テーマは「Zustandにおけるセレクターによるレンダリング最適化」だ。

Reduxの面倒なボイラープレート(お膳立てのコード)に辟易していた我々を救ってくれたZustandだが、「ストア全体をフックでポンと取ってくる」という楽な書き方に甘えていると、裏側でブラウザのCPUを無駄にぶん回す「最悪のパフォーマンス爆弾」を仕込むことになる。

今回は、なぜそれが起きるのかというブラウザの裏側の事情から、実務で即座に使える洗練されたセレクターの極意まで、みっちり叩き込んでやろう。心してついてきな。

—

1. なぜ「ストア全体をそのまま取得する」と地獄を見るのか?

まず、ReactとZustandが裏側で何をやっているのかを解剖しよう。

Zustandのストアからフックで値を取り出すとき、何も考えずに次のようなコードを書いたことはないだろうか?

// 良くあるアンチパターン
const { user, theme, isOpenModal, updateIsOpenModal } = useAppStore();

一見すると、コードがすっきりしていて何の問題もないように見える。だが、Reactのレンダリングの仕組みを思い出してほしい。コンポーネントは、「自分が購読している状態が変化したとき」に再レンダリングされる。

Zustandはデフォルトで、ストア内の「どこか一つのプロパティが変更されただけでも、ストア全体(オブジェクト)の参照を新しくする」という挙動をとる。
つまり、上記のコードを書いたコンポーネントは、ユーザー名が変わろうが、モーダルの開閉状態が変わろうが、ダークモードの切り替えが起きようが、関係ない変更であっても無条件に毎回再レンダリングの嵐に巻き込まれることになる。

ブラウザのメインスレッドは、JavaScriptの実行とDOMの再描画(レイアウト計算やペイント)で大忙しだ。不要なコンポーネントの再レンダリングを走らせることは、UIのプチフリーズ(カクつき)や、スマホ端末でのバッテリー急消費に直結する。

ここで登場するのが、「セレクター(Selector)」という強力な武器だ。

—

2. セレクターとは何か? ブラウザの裏側の動きを知る

セレクターとは一言で言えば、「ストアという巨大なオブジェクトの山から、今、このコンポーネントに必要な最小限のピースだけをピンポイントで切り出す関数」のことだ。

Zustandの `useStore(selector)` は、セレクターが返す値の「参照」を監視している。
ブラウザのメモリ上で、前回のレンダリング時に返した値と、今回のレンダリング時に返した値が `===`(厳密等価)であるかを比較する。もし値が変わっていなければ、Reactに対して「このコンポーネントの再レンダリングはスキップしていいよ」とシグナルを送る仕組みになっているんだ。

この仕組みを理解しているといないとでは、大規模なアプリケーションを組んだときのパフォーマンスに雲泥の差が出る。

—

3. 実践! 現場で使えるセキュアで高速なセレクター実装パターン

百聞は一見にしかず。実際のプロジェクトでそのまま使える、綺麗で無駄のないコードを見ていこう。

今回は、よくある「ユーザープロフィール情報」と「UIのトグル状態」が混ざったストアを想定する。

ストアの定義(`useAppStore.ts`)

import { create } from ‘zustand’;

// ストアの型定義
interface User {
id: string;
name: string;
email: string;
role: ‘admin’ | ‘user’;
}

interface AppState {
user: User | null;
isLoading: boolean;
isSidebarOpen: boolean;
// アクション
setUser: (user: User | null) => void;
toggleSidebar: () => void;
}

export const useAppStore = create((set) => ({
user: null,
isLoading: false,
isSidebarOpen: false,
setUser: (user) => set({ user }),
toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
}));

さて、ここからが本番だ。
サイドバーの開閉状態だけを制御したいコンポーネントと、ユーザーの名前だけを表示したいコンポーネントを実装してみよう。

パターンA:単一プリミティブ値の取得(最も安全で効率的)

import React from ‘react’;
import { useAppStore } from ‘./useAppStore’;

export const SidebarToggleBuntton: React.FC = () => {
// 【極意】必要なプリミティブ値(booleanなど)だけをピンポイントで引っこ抜く
const isSidebarOpen = useAppStore((state) => state.isSidebarOpen);
const toggleSidebar = useAppStore((state) => state.toggleSidebar);

console.log(‘SidebarToggleButton が再レンダリングされました’);

return (

);
};

このコンポーネントは、`isSidebarOpen` の値が変わったときだけに再レンダリングが限定される。ユーザー情報が書き換わっても、このボタンは微動だにしない。完璧だ。

—

パターンB:複数プロパティを同時に取得したい場合の罠と解決策

「いやいや、ユーザーの『名前』と『メールアドレス』の両方を一つのコンポーネントで使いたいんだよ」という場合もあるだろう。
そんなとき、やってはいけないアンチパターンから見せよう。

// ❌ 悪い例:インラインでオブジェクトを返している
const { name, email } = useAppStore((state) => ({
name: state.user?.name,
email: state.user?.email,
}));

これ、パッと見は良さそうに見えるだろ? だが、大間違いだ。
JavaScriptの仕様上、関数が実行されるたびに新しいオブジェクト(`{ name, email }`)が毎回新しく生成される。Zustandから見ると、「毎回新しいオブジェクトが返ってきたぞ? 中身は同じかもしれないけど、参照が変わったから再レンダリングさせなきゃ!」と勘違いしてしまい、セレクターの意味が全くなくなってしまうのだ。

では、どう書くべきか?
複数の値をキレイに安全に取得するには、`shallow`(シャロー比較:浅い比較)というユーティリティを使う。

import React from ‘react’;
import { useAppStore } from ‘./useAppStore’;
import { shallow } from ‘zustand/shallow’; // v4系でのインポート例 (v5ではzustand/react/shallow等から取得)

export const UserProfileCard: React.FC = () => {
// 【極意】複数の値を取り出すときは、オブジェクトを返しつつ `shallow` を第2引数に渡す!
const { name, email } = useAppStore(
(state) => ({
name: state.user?.name ?? ‘ゲスト’,
email: state.user?.email ?? ‘未登録’,
}),
shallow // ← これが命綱。オブジェクトのプロパティを1つずつ比較してくれる
);

console.log(‘UserProfileCard が再レンダリングされました’);

return (

{name}

{email}

);
};

`shallow` を使うことで、返されたオブジェクトのプロパティ(`name` と `email`)の値が実際に変わっていない限り、再レンダリングをピタッと止めてくれる。これぞプロの技だ。

—

4. シニアから最後に送るアドバイス

いいかい、フロントエンドのパフォーマンス最適化において、「とりあえず全部ストアに入れて、とりあえず全部購読する」というのは、コードを書くときは楽だが、アプリが成長した瞬間に血を流す技術的負債になる。

今日から意識してほしいポイントは以下の3つだ。

1. ストア全体を構造化代入で一気に取らない。
2. 単一の値を取得する場合は、プリミティブなセレクター関数を渡す。
3. 複数の値をオブジェクトで取得したい場合は、必ず `shallow` 比較を併用する。

これを徹底するだけで、アプリの無駄な再レンダリングは劇的に減り、ユーザーに「サクサク動く快適な体験」を届けることができる。

現場のコードレビューで、もし誰かが「ストア丸ごとフック」をやっていたら、優しくこの知見をシェアしてあげてくれ。君のチームが、より強靭なエンジニア集団になることを期待しているよ。それじゃ、また次の現場で!

コメント

タイトルとURLをコピーしました