こんにちは!Reactを触り始めて、「よし、状態管理だ!」と意気込んだものの、お馴染みの `useState` や最近よく耳にする `Zustand` の世界で、なんだかモヤモヤとした壁にぶつかっていませんか?
「画面のあちこちでデータを共有したいだけなのに、ちょっと文字を入力しただけで関係ないボタンまで再レンダリングされちゃう……」
「パフォーマンスがどうとか、セレクターがどうとか、急に難しそうな話になってきたぞ……」
大丈夫ですよ、安心してください。今日はそのモヤモヤを、身近なたとえ話を交えながらスッキリ解消していきますね!
伝説の(?)チーフアーキテクトである私が、現場の泥臭いリアルな話も含めて、優しく手取り足取り解説します。
—
1. なぜ「全部のデータ」を見るとアプリが重くなるの?(身近なたとえ話)
まずは、状態管理の基本からおさらいしましょう。
例えば、あなたが大きなおもちゃ箱(Zustandのストア)を持っているとします。その中には、ロボット、お人形、ブロック、そしてお絵描き帳が入っています。
ここで、あなたの「お友達Aくん」は、お絵描き帳だけに用事があるとします。
もし、お友達Aくんに「おもちゃ箱の中身ぜーんぶ見せるね!」とやってしまうとどうなるでしょう? おもちゃ箱の中でロボットがガシャンと動いたり、お人形の服が変わったりするたびに、Aくんは「わっ、なんだろう!?」と驚いてそっちを気にしてしまいますよね。
これが、Reactの「不要な再レンダリング」の正体です。
「本当は『お絵描き帳』だけ見ていればいいのに、ストア全体を丸ごと監視(購読)しているせいで、関係ないおもちゃが動くたびにコンポーネントが再描画されてしまう」 という現象が起きるのです。
アプリが小さいうちはこれでも気になりませんが、規模が大きくなると、文字入力が一文字遅れるような「カクつき」の原因になってしまいます。
—
2. 救世主「セレクター(Selector)」の登場
そこで登場するのが、今回の主役である「セレクター」です。
セレクターとは、おもちゃ箱の中から「私はこのおもちゃしか見ません!」と決める、専用のメガネのようなものです。
このセレクターを使うと、ストア全体がどんなに頻繁にアップデートされても、自分が注目しているデータが変わっていなければ、コンポーネントは一切再レンダリングされません。
百聞は一見にしかず。実際のコードを見てみましょう!
準備:Zustandのストアを作ってみる
まずは、ユーザーの名前(`name`)と、画面のカウンターの数字(`count`)を管理するストアを作ります。
import { create } from ‘zustand’;
// ストアを作成します(おもちゃ箱の準備)
export const useAppStore = create((set) => ({
name: ‘太郎’,
count: 0,
// 名前の更新関数
setName: (newName) => set({ name: newName }),
// カウントの更新関数
increment: () => set((state) => ({ count: state.count + 1 })),
}));
—
3. 実装パターン:ダメな例と、正しいセレクターの使い方
【ダメな例】ストアを丸ごと取ってくる
初心者の頃にやってしまいがちなのが、ストアをそのままポーンと受け取ってしまう書き方です。
import { useAppStore } from ‘./useAppStore’;
function CounterComponent() {
// ⚠️ ストア全体を購読しちゃっています!
const { count, increment } = useAppStore();
console.log(‘CounterComponentが再レンダリングされたよ!’);
return (
現在のカウント: {count}
);
}
この書き方だと、たとえ `name` が書き換わっただけでも、この `CounterComponent` まで再レンダリングされてしまいます。「カウントしか使ってないのに、なんでやねん!」という状態ですね。
【正しい例】セレクターを使ってピンポイントで購読する
それでは、セレクターを使って必要なデータだけをスマートに取り出してみましょう。
import { useAppStore } from ‘./useAppStore’;
function CounterComponent() {
// ✨ セレクター関数を使って、「count」と「increment」だけを取り出す!
const count = useAppStore((state) => state.count);
const increment = useAppStore((state) => state.increment);
console.log(‘CounterComponentが再レンダリングされたよ!’);
return (
現在のカウント: {count}
);
}
これだけです! `state => state.count` というアロー関数が、まさに「おもちゃ箱からこれだけを取ってくる」というセレクターの役割をしています。
これで、もう `name` が変更されても、このカウンターコンポーネントが無駄に再描画されることはありません。素晴らしいパフォーマンスの最適化ですね!
—
4. 現場でよくある「うっかりミス」に気をつけよう
最後に、実務や開発現場で新人さんがハマりがちな「ちょっとした罠」をこっそりシェアしておきますね。ここを知っているだけで、まわりのエンジニアから「おっ、分かってるね!」と思われるポイントです。
複数の値を同時に取り出したいときの注意点
「いくつかすっきりまとめたいな」と思って、以下のような書き方をしてしまうことがあります。
// ⚠️ やってしまいがちな書き方(オブジェクトを新しく作っちゃうパターン)
const { count, increment } = useAppStore((state) => ({
count: state.count,
increment: state.increment,
}));
一見よさそうに見えますが、実はこれ、セレクターの中で「毎回新しいオブジェクト( `{ count, increment }` )を新しく生み出してしまっている」状態になります。
Reactは「前回と中身が同じかどうか」を比較するときに、オブジェクトのメモリー上の場所(参照)を見るため、「毎回新しいものが作られた=中身が変わったんだな!」と勘違いしてしまい、結局再レンダリングが防げなくなってしまいます。
じゃあどう書けばいいの?
複数の値をきれいに取り出したい場合は、Zustandが用意してくれている `useShallow` というヘルパーを使うか、個別にセレクターを分けるのが安全で確実です。
import { useShallow } from ‘zustand/shallow’;
// ✨ useShallow を使うことで、中身が同じなら再レンダリングを防いでくれます
const { count, increment } = useAppStore(
useShallow((state) => ({
count: state.count,
increment: state.increment,
}))
);
こうしておけば、パフォーマンスの神様も大満足です!
—
まとめ
いかがでしたでしょうか?
- ストア全体を購読すると、関係ない変更でもコンポーネントが再レンダリングされてしまう。
- セレクター(`state => state.xxx`)を使うことで、必要なデータだけをピンポイントで監視できる。
- 複数の値を取り出すときは、オブジェクトの参照切れ(再生成)に少しだけ気を配る。
最初は難しく感じるかもしれませんが、この「必要な部分だけをスマートに覗き見る」感覚が掴めてくると、Reactでの状態管理が何倍も楽しく、そしてアプリがキビキビと動くようになって感動しますよ。
もし分からないところがあっても、焦らず一歩ずつ試していけば大丈夫です。あなたの楽しいReactライフを、チーフアーキテクトとしてこれからも応援しています!

コメント