Zustand Selector パターン早見表:equalityFn/shallow/構造的共有の勘所
Zustand でステート管理を行う際、不要な再レンダリングを防ぐことはパフォーマンス最適化の要です。しかし、セレクタの使い方次第では、期待通りに動作しないことがあります。
この記事では、Zustand のセレクタパターンについて、equalityFn、shallow、構造的共有の 3 つの重要な概念を中心に解説します。それぞれの使い分けを理解することで、適切なパフォーマンス最適化が可能になるでしょう。
| # | パターン | 用途 | 再レンダリング条件 | パフォーマンス | 推奨度 | 
|---|---|---|---|---|---|
| 1 | デフォルト(参照比較) | 単一プリミティブ値 | 参照が変わった時 | ★★★ | ★★★ | 
| 2 | shallow | 複数値・オブジェクト | 第 1 階層の値が変わった時 | ★★☆ | ★★★ | 
| 3 | カスタム equalityFn | 深い比較・特殊条件 | カスタム条件による | ★☆☆ | ★★☆ | 
| 4 | 構造的共有 | ネストしたオブジェクト | 実際に変更があった時のみ | ★★★ | ★★★ | 
| 5 | 複数セレクタ分割 | 独立した値の取得 | 各値が個別に変わった時 | ★★★ | ★★★ | 
背景
Zustand におけるセレクタの役割
Zustand は、React のステート管理ライブラリの中でもシンプルで直感的な API が特徴です。その中核となるのが「セレクタ」という仕組みで、ストアから必要な値だけを取り出す役割を担います。
typescript// セレクタの基本形
const useStore = create((set) => ({
  user: { name: 'Alice', age: 25 },
  count: 0,
  // ... その他のステート
}));
// コンポーネント内での使用
function Component() {
  const userName = useStore((state) => state.user.name); // セレクタ
  // ...
}
セレクタを使うことで、コンポーネントは必要なデータだけを購読できます。これにより、無関係なステート変更時の再レンダリングを防げるのです。
再レンダリングの判定メカニズム
Zustand は内部で「前回のセレクタ結果」と「今回のセレクタ結果」を比較し、変更があった場合のみコンポーネントを再レンダリングします。この比較方法がパフォーマンスの鍵を握ります。
デフォルトでは、JavaScript の Object.is() による参照比較が行われます。つまり、プリミティブ値(数値、文字列など)は値そのものが、オブジェクトや配列は参照が比較されるのです。
以下の図は、Zustand のセレクタが再レンダリングを判定する基本フローを示しています。
mermaidflowchart TD
  stateChange["ステート変更"] --> selector["セレクタ実行"]
  selector --> compare["前回値と今回値を比較"]
  compare -->|"変更あり"| rerender["コンポーネント再レンダリング"]
  compare -->|"変更なし"| skip["再レンダリングスキップ"]
  compare --> method["比較方法の選択"]
  method --> default["デフォルト<br/>(Object.is)"]
  method --> shallow_cmp["shallow<br/>(第1階層比較)"]
  method --> custom["カスタム<br/>(独自関数)"]
図の要点:
- ステート変更後、セレクタが再実行される
 - 前回と今回の結果を比較し、変更がある場合のみ再レンダリング
 - 比較方法は 3 種類から選択可能
 
この仕組みを理解することで、適切なセレクタパターンを選択できます。
構造的共有とイミュータブル更新
Zustand は内部で「構造的共有(Structural Sharing)」という最適化手法を採用しています。これは、ステートを更新する際に、実際に変更された部分だけ新しいオブジェクトを作成し、変更されていない部分は前回のオブジェクトを再利用する仕組みです。
typescript// 構造的共有の例
const prevState = {
  user: { name: 'Alice', age: 25 },
  settings: { theme: 'dark', lang: 'ja' },
};
// user.name だけを更新
const nextState = {
  user: { name: 'Bob', age: 25 }, // 新しいオブジェクト
  settings: prevState.settings, // 参照を再利用
};
// settings は参照が同じなので、settings を使うコンポーネントは再レンダリングされない
console.log(prevState.settings === nextState.settings); // true
この仕組みにより、セレクタが参照比較を行っても、変更されていない部分に依存するコンポーネントは再レンダリングされません。
課題
デフォルト参照比較の落とし穴
デフォルトの参照比較は、プリミティブ値の取得には最適ですが、オブジェクトや配列を返すセレクタでは問題が発生します。
typescript// 問題のあるパターン
function UserProfile() {
  // 毎回新しいオブジェクトが返されるため、常に再レンダリングされる
  const user = useStore((state) => ({
    name: state.user.name,
    age: state.user.age,
  }));
  return (
    <div>
      {user.name} ({user.age})
    </div>
  );
}
上記のコードでは、セレクタが毎回新しいオブジェクトを生成します。参照が毎回変わるため、実際の値が変わっていなくても再レンダリングが発生してしまうのです。
以下の図は、デフォルト参照比較での問題を視覚化したものです。
mermaidflowchart LR
  render1["レンダリング1"] --> obj1["{ name: 'Alice',<br/>age: 25 }"]
  render2["レンダリング2"] --> obj2["{ name: 'Alice',<br/>age: 25 }"]
  obj1 --> comp1["参照: 0x001"]
  obj2 --> comp2["参照: 0x002"]
  comp1 -.->|"参照が異なる"| result["再レンダリング発生"]
  comp2 -.->|"値は同じだが"| result
図の要点:
- 同じ値でも新しいオブジェクトが生成される
 - 参照が異なるため、不要な再レンダリングが発生
 
複数値取得時のジレンマ
コンポーネントが複数の値を必要とする場合、以下のようなジレンマに直面します。
選択肢 1: オブジェクトで返す
typescript// 常に再レンダリングされる
const data = useStore((state) => ({
  name: state.user.name,
  count: state.count,
}));
選択肢 2: 個別に取得
typescript// 冗長なコード
const name = useStore((state) => state.user.name);
const count = useStore((state) => state.count);
どちらの方法も一長一短です。前者はパフォーマンスに問題があり、後者はコードが冗長になります。
深い比較のパフォーマンスコスト
オブジェクトの内容を比較する「深い比較(Deep Equality)」は、ネストしたオブジェクトすべてを再帰的に比較するため、大きなオブジェクトでは処理コストが高くなります。
typescript// パフォーマンス上問題がある例
const largeData = useStore(
  (state) => state.complexNestedObject,
  (prev, next) =>
    JSON.stringify(prev) === JSON.stringify(next) // 非効率
);
毎回のレンダリングで深い比較を実行すると、かえってパフォーマンスが悪化する可能性があります。
解決策
パターン 1: デフォルト(参照比較)
用途: 単一のプリミティブ値または参照を取得する場合
デフォルトの参照比較は、最もシンプルで高速です。単一の値を取得する場合に最適です。
typescript// プリミティブ値の取得
function Counter() {
  const count = useStore((state) => state.count);
  return <div>{count}</div>;
}
typescript// オブジェクトの参照を直接取得
function UserCard() {
  const user = useStore((state) => state.user);
  return <div>{user.name}</div>;
}
メリット:
- 最も高速な比較方法
 - コードがシンプル
 - 構造的共有と組み合わせると効果的
 
デメリット:
- オブジェクトリテラルを返すと常に再レンダリング
 - 計算結果のオブジェクトを返すのには不向き
 
パターン 2: shallow(浅い比較)
用途: 複数の値をオブジェクトとして取得する場合
shallow は、オブジェクトの第 1 階層の値を個別に比較します。複数の値を取得する際の標準的な解決策です。
typescriptimport { shallow } from 'zustand/shallow';
// 複数値をオブジェクトで取得
function UserProfile() {
  const { name, age } = useStore(
    (state) => ({
      name: state.user.name,
      age: state.user.age,
    }),
    shallow // 第 1 階層を個別に比較
  );
  return (
    <div>
      {name} ({age})
    </div>
  );
}
以下の図は、shallow の比較動作を示しています。
mermaidflowchart TD
  obj1["前回:<br/>{ name: 'Alice', age: 25 }"] --> shallow_compare["shallow 比較"]
  obj2["今回:<br/>{ name: 'Alice', age: 25 }"] --> shallow_compare
  shallow_compare --> check1["name を比較"]
  shallow_compare --> check2["age を比較"]
  check1 -->|"'Alice' === 'Alice'"| same1["同じ"]
  check2 -->|"25 === 25"| same2["同じ"]
  same1 --> result["すべて同じ → 再レンダリングなし"]
  same2 --> result
図の要点:
- 第 1 階層の各プロパティを個別に比較
 - すべてが同じ値なら再レンダリングをスキップ
 
shallow の実装イメージ:
typescript// shallow の内部動作(簡略版)
function shallow(objA, objB) {
  if (Object.is(objA, objB)) return true;
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;
  for (const key of keysA) {
    if (!Object.is(objA[key], objB[key])) return false;
  }
  return true;
}
配列にも使用可能:
typescript// 配列の要素を比較
function TodoList() {
  const todoIds = useStore(
    (state) => state.todos.map((todo) => todo.id),
    shallow // 配列の各要素を比較
  );
  return (
    <ul>
      {todoIds.map((id) => (
        <li key={id}>{id}</li>
      ))}
    </ul>
  );
}
メリット:
- 複数値取得時のコードが簡潔
 - プリミティブ値の集合には十分高速
 - 配列にも適用可能
 
デメリット:
- ネストしたオブジェクトの変更は検出できない
 - 大量のプロパティがある場合はやや遅い
 
パターン 3: カスタム equalityFn
用途: 特殊な比較ロジックが必要な場合
カスタムの等価性判定関数を使うことで、独自の比較ロジックを実装できます。
typescript// 特定のプロパティのみを比較
function UserDisplay() {
  const user = useStore(
    (state) => state.user,
    (prev, next) => prev.id === next.id // id だけを比較
  );
  return <div>{user.name}</div>;
}
typescript// 配列の長さだけを比較
function TodoCount() {
  const todos = useStore(
    (state) => state.todos,
    (prev, next) => prev.length === next.length
  );
  return <div>Todo 数: {todos.length}</div>;
}
typescript// 複雑な条件での比較
function Dashboard() {
  const stats = useStore(
    (state) => ({
      total: state.items.length,
      completed: state.items.filter((item) => item.done)
        .length,
    }),
    (prev, next) => {
      // 10 未満の変化は無視
      return (
        Math.abs(prev.total - next.total) < 10 &&
        Math.abs(prev.completed - next.completed) < 10
      );
    }
  );
  return (
    <div>
      完了: {stats.completed} / {stats.total}
    </div>
  );
}
メリット:
- 柔軟な比較ロジック
 - 特定の条件でのみ更新できる
 - 不要な更新を細かく制御可能
 
デメリット:
- 複雑な比較関数はパフォーマンスに影響
 - バグを生みやすい
 - コードの可読性が下がる可能性
 
パターン 4: 構造的共有の活用
用途: ネストしたオブジェクトを効率的に管理する場合
Zustand の構造的共有を活用することで、セレクタをシンプルに保ちつつ、不要な再レンダリングを防げます。
typescriptconst useStore = create((set) => ({
  user: {
    name: 'Alice',
    age: 25,
    profile: { bio: 'Developer' },
  },
  settings: { theme: 'dark', lang: 'ja' },
  updateUserName: (name) =>
    set((state) => ({
      user: {
        ...state.user,
        name, // name だけを更新、profile は参照を維持
      },
    })),
}));
typescript// profile を取得するコンポーネント
function UserBio() {
  // user.name が変わっても、profile の参照は変わらないため再レンダリングされない
  const profile = useStore((state) => state.user.profile);
  return <div>{profile.bio}</div>;
}
以下の図は、構造的共有によるメモリ効率と再レンダリング最適化を示しています。
mermaidflowchart LR
  prev["前回ステート"] --> user1["user"]
  prev --> settings1["settings"]
  user1 --> name1["name: 'Alice'"]
  user1 --> profile1["profile"]
  update["updateUserName('Bob')"] --> next["次回ステート"]
  next --> user2["user (新)"]
  next --> settings2["settings (参照維持)"]
  user2 --> name2["name: 'Bob'"]
  user2 --> profile2["profile (参照維持)"]
  settings1 -.->|"同じ参照"| settings2
  profile1 -.->|"同じ参照"| profile2
図の要点:
- 変更された部分だけ新しいオブジェクトを生成
 - 変更されていない部分は参照を維持
 - セレクタが参照比較しても最適化が効く
 
Immer との組み合わせ:
typescriptimport { produce } from 'immer';
const useStore = create((set) => ({
  user: {
    name: 'Alice',
    profile: {
      bio: 'Developer',
      skills: ['React', 'TypeScript'],
    },
  },
  addSkill: (skill) =>
    set(
      produce((state) => {
        // Immer が自動的に構造的共有を行う
        state.user.profile.skills.push(skill);
      })
    ),
}));
メリット:
- セレクタがシンプルになる
 - デフォルトの参照比較で十分
 - メモリ効率が良い
 
デメリット:
- 更新ロジックを正しく書く必要がある
 - Immer などのライブラリが必要な場合もある
 
パターン 5: 複数セレクタ分割
用途: 独立した複数の値を取得する場合
関連性の低い値は、個別のセレクタに分割することで、より細かい更新制御が可能になります。
typescript// 各値が独立して更新される
function Dashboard() {
  const userName = useStore((state) => state.user.name);
  const todoCount = useStore((state) => state.todos.length);
  const theme = useStore((state) => state.settings.theme);
  // それぞれの値が変わった時のみ、このコンポーネントが再レンダリングされる
  return (
    <div>
      <h1>{userName}</h1>
      <p>Todo: {todoCount}</p>
      <ThemeIndicator theme={theme} />
    </div>
  );
}
typescript// さらに細かくコンポーネント分割
function UserName() {
  const name = useStore((state) => state.user.name);
  return <h1>{name}</h1>;
}
function TodoCount() {
  const count = useStore((state) => state.todos.length);
  return <p>Todo: {count}</p>;
}
function Dashboard() {
  return (
    <div>
      <UserName /> {/* name だけの変更で再レンダリング */}
      <TodoCount /> {/* todos だけの変更で再レンダリング */}
    </div>
  );
}
メリット:
- 最も細かい粒度で更新制御
 - コンポーネント設計が明確になる
 - デバッグしやすい
 
デメリット:
- コード量が増える
 - 過度な分割は逆に読みにくくなる
 
具体例
例 1: ユーザー情報とカウンタの表示
シナリオ: ユーザー名とカウンタを表示するコンポーネントで、それぞれの変更に応じて最適に再レンダリングしたい。
typescript// ストア定義
const useStore = create((set) => ({
  user: { name: 'Alice', age: 25 },
  count: 0,
  setUserName: (name) =>
    set((state) => ({
      user: { ...state.user, name },
    })),
  increment: () =>
    set((state) => ({ count: state.count + 1 })),
}));
❌ 悪い例: デフォルト参照比較でオブジェクトを返す
typescriptfunction BadExample() {
  // 毎回新しいオブジェクトが生成され、常に再レンダリング
  const data = useStore((state) => ({
    name: state.user.name,
    count: state.count,
  }));
  console.log('BadExample rendered'); // カウンタやユーザー名が変わるたびにログが出る
  return (
    <div>
      {data.name}: {data.count}
    </div>
  );
}
✅ 良い例 1: shallow を使う
typescriptimport { shallow } from 'zustand/shallow';
function GoodExample1() {
  const { name, count } = useStore(
    (state) => ({
      name: state.user.name,
      count: state.count,
    }),
    shallow // 第 1 階層を比較
  );
  console.log('GoodExample1 rendered'); // name か count が実際に変わった時だけログが出る
  return (
    <div>
      {name}: {count}
    </div>
  );
}
✅ 良い例 2: セレクタを分割する
typescriptfunction GoodExample2() {
  const name = useStore((state) => state.user.name);
  const count = useStore((state) => state.count);
  console.log('GoodExample2 rendered');
  return (
    <div>
      {name}: {count}
    </div>
  );
}
比較表:
| # | パターン | 再レンダリングタイミング | パフォーマンス | 
|---|---|---|---|
| 1 | 悪い例 | 毎回(不要な再レンダリング) | ★☆☆ | 
| 2 | shallow | name か count が変わった時 | ★★☆ | 
| 3 | セレクタ分割 | name か count が変わった時 | ★★★ | 
例 2: Todo リストのフィルタリング
シナリオ: Todo リストを表示し、完了済みの項目だけを表示するフィルタを実装。
typescript// ストア定義
const useStore = create((set) => ({
  todos: [
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Write code', done: true },
  ],
  filter: 'all', // 'all' | 'active' | 'completed'
  toggleTodo: (id) =>
    set((state) => ({
      todos: state.todos.map((todo) =>
        todo.id === id
          ? { ...todo, done: !todo.done }
          : todo
      ),
    })),
  setFilter: (filter) => set({ filter }),
}));
❌ 悪い例: 毎回フィルタリングした新しい配列を返す
typescriptfunction BadTodoList() {
  // 毎回新しい配列が生成され、filter が変わらなくても再レンダリング
  const filteredTodos = useStore((state) => {
    if (state.filter === 'active')
      return state.todos.filter((t) => !t.done);
    if (state.filter === 'completed')
      return state.todos.filter((t) => t.done);
    return state.todos;
  });
  console.log('BadTodoList rendered');
  return (
    <ul>
      {filteredTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
✅ 良い例 1: shallow で配列を比較
typescriptfunction GoodTodoList1() {
  const filteredTodos = useStore(
    (state) => {
      if (state.filter === 'active')
        return state.todos.filter((t) => !t.done);
      if (state.filter === 'completed')
        return state.todos.filter((t) => t.done);
      return state.todos;
    },
    shallow // 配列の各要素を比較
  );
  console.log('GoodTodoList1 rendered');
  return (
    <ul>
      {filteredTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
✅ 良い例 2: useMemo でメモ化
typescriptimport { useMemo } from 'react';
function GoodTodoList2() {
  const todos = useStore((state) => state.todos);
  const filter = useStore((state) => state.filter);
  // フィルタリング結果をメモ化
  const filteredTodos = useMemo(() => {
    if (filter === 'active')
      return todos.filter((t) => !t.done);
    if (filter === 'completed')
      return todos.filter((t) => t.done);
    return todos;
  }, [todos, filter]);
  console.log('GoodTodoList2 rendered');
  return (
    <ul>
      {filteredTodos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}
以下の図は、メモ化による最適化フローを示しています。
mermaidflowchart TD
  render["コンポーネントレンダリング"] --> check_deps["依存値チェック"]
  check_deps -->|"todos または filter が変更"| recalc["フィルタリング再計算"]
  check_deps -->|"変更なし"| reuse["前回の結果を再利用"]
  recalc --> new_array["新しい配列生成"]
  reuse --> cached_array["キャッシュされた配列"]
  new_array --> render_list["リスト描画"]
  cached_array --> render_list
図の要点:
- 依存値が変わった時だけ再計算
 - 変更がなければキャッシュを再利用
 
例 3: 深くネストしたステートの更新
シナリオ: ユーザーのプロフィール情報をネストした構造で管理し、一部だけを更新。
typescript// ストア定義
const useStore = create((set) => ({
  user: {
    name: 'Alice',
    profile: {
      bio: 'React Developer',
      social: {
        twitter: '@alice',
        github: 'alice',
      },
    },
    settings: {
      theme: 'dark',
      notifications: true,
    },
  },
  updateBio: (bio) =>
    set((state) => ({
      user: {
        ...state.user,
        profile: {
          ...state.user.profile,
          bio, // bio だけを更新、social は参照を維持
        },
      },
    })),
  updateTheme: (theme) =>
    set((state) => ({
      user: {
        ...state.user,
        settings: {
          ...state.user.settings,
          theme, // theme だけを更新
        },
      },
    })),
}));
構造的共有による最適化:
typescript// Bio を表示するコンポーネント
function UserBio() {
  // bio が変わった時だけ再レンダリング
  const bio = useStore((state) => state.user.profile.bio);
  console.log('UserBio rendered');
  return <div>{bio}</div>;
}
// SNS リンクを表示するコンポーネント
function SocialLinks() {
  // bio が変わっても、social の参照は変わらないため再レンダリングされない
  const social = useStore(
    (state) => state.user.profile.social
  );
  console.log('SocialLinks rendered');
  return (
    <div>
      <a href={`https://twitter.com/${social.twitter}`}>
        Twitter
      </a>
      <a href={`https://github.com/${social.github}`}>
        GitHub
      </a>
    </div>
  );
}
// テーマを表示するコンポーネント
function ThemeIndicator() {
  // theme が変わった時だけ再レンダリング
  const theme = useStore(
    (state) => state.user.settings.theme
  );
  console.log('ThemeIndicator rendered');
  return <div>Theme: {theme}</div>;
}
動作確認:
typescriptfunction App() {
  const updateBio = useStore((state) => state.updateBio);
  return (
    <div>
      <UserBio /> {/* updateBio で再レンダリング */}
      <SocialLinks /> {/* updateBio では再レンダリングされない */}
      <ThemeIndicator />{' '}
      {/* updateBio では再レンダリングされない */}
      <button onClick={() => updateBio('Senior Developer')}>
        Update Bio
      </button>
    </div>
  );
}
// updateBio('Senior Developer') を実行すると:
// → UserBio rendered(bio が変わったため)
// → SocialLinks、ThemeIndicator は再レンダリングされない(参照が変わっていないため)
例 4: カスタム equalityFn の実用例
シナリオ: 大量のデータを持つリストで、特定の条件でのみ更新したい。
typescript// ストア定義
const useStore = create((set) => ({
  items: Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`,
    priority: Math.floor(Math.random() * 5),
  })),
  updateItemName: (id, name) =>
    set((state) => ({
      items: state.items.map((item) =>
        item.id === id ? { ...item, name } : item
      ),
    })),
}));
カスタム比較関数で高優先度アイテムだけを監視:
typescriptfunction HighPriorityItems() {
  const highPriorityItems = useStore(
    (state) =>
      state.items.filter((item) => item.priority >= 4),
    (prev, next) => {
      // 高優先度アイテムの ID リストだけを比較
      const prevIds = prev.map((item) => item.id).join(',');
      const nextIds = next.map((item) => item.id).join(',');
      return prevIds === nextIds;
    }
  );
  console.log('HighPriorityItems rendered');
  return (
    <ul>
      {highPriorityItems.map((item) => (
        <li key={item.id}>
          {item.name} (優先度: {item.priority})
        </li>
      ))}
    </ul>
  );
}
// 動作:
// - 低優先度アイテムの name が変わっても再レンダリングされない
// - 高優先度アイテムの name が変わっても、ID リストが同じなら再レンダリングされない
// - 高優先度アイテムが増減した時だけ再レンダリング
比較パターン一覧表:
| # | 比較方法 | 使用例 | 更新頻度 | 
|---|---|---|---|
| 1 | デフォルト | 単一値 | name が変わるたび | 
| 2 | shallow | 複数値・配列 | 要素が追加/削除されるたび | 
| 3 | カスタム(ID) | ID だけ比較 | ID が変わるたび | 
| 4 | カスタム(長さ) | 配列の長さ | 長さが変わるたび | 
まとめ
Zustand のセレクタパターンは、適切に使い分けることで React アプリケーションのパフォーマンスを大幅に向上させられます。
重要なポイント:
- 
単一値はデフォルト参照比較で十分 - プリミティブ値や単一のオブジェクト参照を取得する場合、デフォルトが最適です。
 - 
複数値は shallow を使う - オブジェクトや配列で複数の値を返すセレクタには、
shallowによる第 1 階層比較が効果的です。 - 
構造的共有を活用する - Zustand の構造的共有により、セレクタをシンプルに保ちながら不要な再レンダリングを防げます。
 - 
カスタム equalityFn は慎重に - 特殊な比較ロジックが必要な場合のみ使用し、パフォーマンスへの影響を考慮しましょう。
 - 
セレクタ分割も選択肢 - 関連性の低い値は個別のセレクタに分割することで、より細かい更新制御が可能です。
 
パターン選択のフローチャート:
mermaidflowchart TD
  start["セレクタを書く"] --> single{"単一値?"}
  single -->|"Yes"| primitive{"プリミティブ値?"}
  single -->|"No"| multiple["複数値を返す"]
  primitive -->|"Yes"| default1["デフォルト参照比較"]
  primitive -->|"No"| object1["オブジェクト参照"]
  object1 --> structural{"構造的共有が効く?"}
  structural -->|"Yes"| default2["デフォルト参照比較"]
  structural -->|"No"| custom1["カスタム equalityFn"]
  multiple --> array_or_obj{"配列 or オブジェクト?"}
  array_or_obj -->|"Yes"| shallow_choice["shallow"]
  array_or_obj -->|"No"| split["セレクタ分割を検討"]
  shallow_choice --> nested{"ネストが深い?"}
  nested -->|"Yes"| custom2["カスタム equalityFn または<br/>セレクタ分割"]
  nested -->|"No"| shallow_ok["shallow で OK"]
  default1 --> done["完了"]
  default2 --> done
  shallow_ok --> done
  custom1 --> done
  custom2 --> done
  split --> done
図で理解できる要点:
- 単一値か複数値かで大きく分岐
 - プリミティブ値はデフォルトが最適
 - 複数値は shallow を基本とする
 - ネストが深い場合は追加の考慮が必要
 
これらのパターンを理解し、状況に応じて使い分けることで、Zustand の真の力を引き出せるでしょう。適切なセレクタ戦略により、アプリケーションは高速で保守性の高いものになります。
関連リンク
articleZustand × useTransition 概説:並列レンダリング時代に安全な更新を設計する
articleフィーチャーフラグ運用:Zustand で段階的リリースとリモート設定を実装
articleオフラインファースト設計:Zustand で楽観的 UI とロールバックを実現
articleZustand Selector パターン早見表:equalityFn/shallow/構造的共有の勘所
articleTurborepo で Zustand スライスをパッケージ化:Monorepo 運用の初期設定
articleZustand × TanStack Query × SWR:キャッシュ・再検証・型安全の実運用比較
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleAstro のレンダリング戦略を一望:MPA× 部分ハイドレーションの強みを図解解説
articleWebLLM が読み込めない時の原因と解決策:CORS・MIME・パス問題を総点検
articleVitest ESM/CJS 混在で `Cannot use import statement outside a module` が出る技術対処集
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来