【入門編】 useSyncExternalStoreとuseEffectの比較 – React実践ガイド

「え、まだ`useEffect`で外部データ取ってるの?」〜 `useSyncExternalStore`でスッキリ!レンダリングのモヤモヤを解消する話 〜

やっほー!フロントエンドの世界へようこそ!君がReactの世界に足を踏み入れたばかりなら、きっと「副作用」って言葉にドキッとしたんじゃないかな?特に、APIからデータを取ってきたり、ブラウザのローカルストレージを触ったり… そういう、コンポーネントの「中」で起こる、ちょっと「外」のことに影響を与えるような処理のことだね。

そんな副作用を扱うために、Reactには強力な味方、「`useEffect`」がある。これはもう、React開発者の「相棒」みたいなもの。でもね、実はこの`useEffect`、外部のデータソース(例えば、ReduxやZustandみたいな状態管理ライブラリ、あるいはブラウザの`localStorage`とか)を「購読」するっていう、ちょっと特殊な状況では、思わぬ落とし穴があることがあるんだ。

「え、`useEffect`でダメなの?!」って思った? 大丈夫、大丈夫!君は何も間違ってない。`useEffect`は本当に素晴らしいフックだし、多くの場面で大活躍してくれる。ただ、世の中には、もっと「ピッタリ」な道具があるんだってことを、今日は一緒に見ていこうと思うんだ。

今日は、そんな「ピッタリ」な道具、「`useSyncExternalStore`」について、`useEffect`と比べて、どういうところが違うのか、なんでこっちを使った方がいいのか、そして、それがどうやってレンダリングの「チグハグ感」をなくしてくれるのか、を、いつものお買い物に例えながら、優しく、じっくり解説していくよ!

そもそも「外部ストア」って何? 〜いつものお買い物に例えてみよう〜

まず、「外部ストア」って言葉がちょっと難しく聞こえるかもしれないけど、これも身近な例で考えてみよう。

君がお買い物に行くとしよう。お店には、いろんな商品が並んでるよね? そのお店の商品リスト、これが「外部ストア」だと思ってほしいんだ。

  • お店の商品リスト(外部ストア): 商品の種類、値段、在庫状況などが書かれている。
  • 君(Reactコンポーネント): その商品リストを見て、「あ、このTシャツ欲しいな!」とか「このリンゴ、まだあるかな?」って考える。
  • 店員さん(状態管理ライブラリやAPI): 商品リストを更新したり、君からの「このTシャツください!」っていう注文を受けたりする。

普段、君がReactコンポーネントで何かを表示するときは、そのコンポーネントの中に「状態」を持ってるよね。例えば、カウンターアプリで「何回ボタンを押したか」っていう数字をカウントする時みたいに。これは、君が「自分のカゴの中身」を管理してるイメージ。

でも、もし、お店の商品リスト(外部ストア)が更新されたらどうなる? 例えば、さっきまで在庫があったTシャツが、急に売り切れになったとしたら? 君は、まだそのTシャツが買えると思ってたら、お店に行ってみたら「あれ?ない!」ってなるよね。これって、すごくガッカリだし、時間の無駄だ。

Reactコンポーネントも同じ。もし、外部にあるデータ(お店の商品リスト)が変わったのに、コンポーネントがその変化に気づかなかったら、画面に表示されている情報が古いままで、ユーザーは混乱してしまう。

`useEffect`の「お買い物」 〜ちょっと慎重すぎる店員さん〜

そこで、`useEffect`の登場だ。`useEffect`は、コンポーネントが「レンダリングされた後」に、外部の何か(副作用)を実行するためのフックだ。

君がお店でTシャツを買おうとして、店員さんに「このTシャツ、在庫ありますか?」って聞くとする。

`useEffect`で外部ストアを購読する場合、この店員さん(`useEffect`)は、ちょっと慎重なんだ。

1. まず、君(コンポーネント)の様子を見る: 君がお店(コンポーネント)で欲しいものを決めて、店員さんに話しかけるのを待つ。
2. 君の「注文」を受け取る: 君が「このTシャツ、在庫ありますか?」って聞く(コンポーネントがレンダリングされる)。
3. 「在庫確認」という「お仕事」をする: 店員さん(`useEffect`)は、お店の商品リスト(外部ストア)を見て、在庫があるか確認する。
4. 結果を君に伝える: 「はい、ございます!」とか「すみません、売り切れです…」って教えてくれる。

この流れは、ほとんどの場合、うまくいっているように見える。でも、ここで問題が起こる可能性があるんだ。

問題点:レンダリングの「ズレ」

お店の商品リスト(外部ストア)は、君がお店に来る「前」に、すでに更新されているかもしれない。例えば、君がお店に着いたときには、もうそのTシャツは売り切れだったのに、君は「まだ在庫がある」って思って、店員さんに聞く(コンポーネントがレンダリングされる)まで、その事実を知らない。

この「ズレ」が、Reactの世界でも起こりうるんだ。

  • 外部ストアが更新される (例: サーバーから新しいデータが届く)
  • Reactコンポーネントがレンダリングされる (この時点では、まだ古いデータを見ている)
  • `useEffect`が「レンダリング後」に動く (ようやく、新しいデータに気づく)

この「気づくまでの間」に、画面に表示されている情報が古くなってしまうことがある。ユーザーにとっては、「あれ?さっきと表示が変わったような…?」とか、「あれ?この情報、もう古くない?」って、ちょっとモヤモヤする瞬間が生まれてしまうんだ。

`useEffect`を使った「お買い物」のコード例(おさらい)

どんな感じか、コードで見てみよう。ここでは、簡単のために、コンソールにログを出すだけだけど、これがもし画面表示だったら、ちょっとしたズレが起こりうるイメージだよ。

import React, { useState, useEffect } from ‘react’;

// 外部ストアを模倣した簡単なオブジェクト
const externalStore = {
value: ‘初期値’,
listeners: new Set(), // 購読者(コンポーネント)を管理するセット
// 値を更新する関数
updateValue: function(newValue) {
this.value = newValue;
// 購読者全員に通知する
this.listeners.forEach(listener => listener(this.value));
},
// 購読を開始する関数
subscribe: function(listener) {
this.listeners.add(listener);
// クリーンアップ関数(購読解除)を返す
return () => {
this.listeners.delete(listener);
};
}
};

function MyComponent() {
// コンポーネント内で管理する状態
const [data, setData] = useState(externalStore.value);

// useEffectで外部ストアを購読
useEffect(() => {
console.log(‘useEffect: 購読を開始します’);

// subscribe関数に、状態を更新する関数(setData)を渡す
const unsubscribe = externalStore.subscribe(newValue => {
console.log(‘useEffect: 外部ストアの値が更新されました!’, newValue);
setData(newValue); // 外部ストアの値が変わったら、コンポーネントの状態も更新
});

// クリーンアップ関数:コンポーネントがアンマウントされたら購読を解除
return () => {
console.log(‘useEffect: クリーンアップ(購読解除)’);
unsubscribe();
};
}, []); // 依存配列は空なので、マウント時に一度だけ実行される

console.log(‘コンポーネントレンダリング中:’, data); // 現在のコンポーネントの状態

return (

useEffectでの外部ストア購読

現在のデータ: {data}

);
}

export default MyComponent;

このコードでは、`MyComponent`は`useEffect`を使って`externalStore`を購読しています。`externalStore.updateValue`が呼ばれて値が更新されると、`subscribe`で渡されたコールバック関数が実行され、`setData`によってコンポーネントの状態が更新されます。

しかし、ここで注意したいのは、`useEffect`は「レンダリング後」に動くということです。もし、`externalStore.updateValue`が、コンポーネントのレンダリング中に呼ばれたら…? `useEffect`が動くのは、そのレンダリングが終わった後になるので、一瞬、古いデータが表示されてしまう可能性があるんです。

`useSyncExternalStore`の「お買い物」 〜賢くて、ちょっぴり先回りできる店員さん〜

そこで登場するのが、「`useSyncExternalStore`」! これは、まさに外部ストアとの「同期」を、もっと賢く、もっとレンダリングと整合性が取れるように設計されたフックなんだ。

君が、さっきの「お店のお買い物」で、もし「賢い店員さん」に出会ったら、どうなるだろう?

1. 君(コンポーネント)が「欲しいもの」を言う: 君が「このTシャツ、欲しいな!」って店員さんに話しかける(コンポーネントがレンダリングされる)。
2. 店員さんは「すぐさま」商品リストを見る: この賢い店員さんは、君が話しかけてきた「まさにその瞬間」に、お店の商品リスト(外部ストア)の最新の状態を確認してくれる。
3. 君に「最新の情報」を伝える: 「はい、このTシャツ、まだ在庫ございますよ!」とか、「あ、すみません、ちょうど今、最後の1枚が売れてしまいました…」って、君が知りたいと思った瞬間の、最新の状態を教えてくれる。

これが、`useSyncExternalStore`のすごいところなんだ。

  • レンダリングと「同期」する: `useSyncExternalStore`は、コンポーネントがレンダリングされる「直前」や「最中」に、外部ストアの最新の状態を取得し、コンポーネントの状態と同期させようとする。
  • 「ズレ」を許さない: これにより、「コンポーネントがレンダリングされたけど、実は外部ストアはもう更新されていて、表示されている情報が古い」という状態(レンダリングの不整合)が起こりにくくなる。

`useSyncExternalStore`がレンダリングの「チグハグ」をなくす仕組み

どうして`useSyncExternalStore`はそんなに賢く動けるんだろう? その秘密は、Reactの内部的な仕組みと深く関わっているんだ。

`useSyncExternalStore`は、Reactに「この外部ストアの状態が、私のコンポーネントの表示に影響しますよ!」と、より直接的に、そして「同期的に」伝えている。

Reactは、`useSyncExternalStore`を使っているコンポーネントに対して、以下のような動作を試みるんだ。

1. コンポーネントがレンダリングされる: まず、Reactはコンポーネントのレンダリングを開始する。
2. `useSyncExternalStore`が「最新の状態」を取得: このとき、`useSyncExternalStore`は、登録されている外部ストアから「最新の状態」を取得し、その状態を使ってレンダリングを進めるようにReactに指示する。
3. レンダリング結果が「最新」である保証: これにより、コンポーネントがレンダリングされるたびに、それは外部ストアの「最新の状態」に基づいたものになる。つまり、`useEffect`のように「レンダリングが終わってから気づく」のではなく、「レンダリングする時点で最新」だから、表示のズレが起こりにくいんだ。

例えるなら、`useEffect`が「お買い物リストの変更を、後で店員さんに聞きに行く」のに対して、`useSyncExternalStore`は「お店に入った瞬間に、店員さんに『最新のお買い物リストを見せてください!』って言って、その情報で計画を立てる」ようなイメージかな。

`useSyncExternalStore`の基本的な使い方

じゃあ、具体的にどうやって使うんだろう? 実は、とってもシンプルなんだ。

`useSyncExternalStore`は、3つの引数を取る。

1. `subscribe`関数: 外部ストアを購読するための関数。外部ストアの値が変更されたときに、コールバック関数を呼び出す。
2. `getSnapshot`関数: 外部ストアから「現在のスナップショット(状態)」を取得するための関数。
3. `getServerSnapshot`関数 (オプション): サーバーサイドレンダリング(SSR)時に使われる。SSRで取得できるスナップショット。

まずは、上で作った`externalStore`を`useSyncExternalStore`で使えるように、少しだけ形を変えてみよう。

import React, { useState, useSyncExternalStore } from ‘react’;

// 外部ストアを模倣した簡単なオブジェクト
const externalStore = {
value: ‘初期値’,
listeners: new Set(),
updateValue: function(newValue) {
this.value = newValue;
this.listeners.forEach(listener => listener(this.value));
},
subscribe: function(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
},
// getSnapshot関数を追加
getSnapshot: function() {
return this.value;
}
// getServerSnapshot は今回は省略(クライアントサイドのみを想定)
};

function MyComponentWithSync() {
// useSyncExternalStore を使って外部ストアを購読
const data = useSyncExternalStore(
externalStore.subscribe, // 購読関数
externalStore.getSnapshot // スナップショット取得関数
// getServerSnapshot は省略
);

console.log(‘コンポーネントレンダリング中 (useSyncExternalStore):’, data); // 現在のコンポーネントの状態

return (

useSyncExternalStoreでの外部ストア購読

現在のデータ: {data}

);
}

export default MyComponentWithSync;

どうかな? `useEffect`で自分で状態(`useState`)を持って、それを`useEffect`の中で更新していたのが、`useSyncExternalStore`を使うことで、外部ストアの値を直接取得して、それをそのままコンポーネントの表示に使えるようになったんだ。

`useState`も、`useEffect`も、もう必要ない! `useSyncExternalStore`が、外部ストアの値とコンポーネントの表示を「同期」してくれるから。

なぜ`useSyncExternalStore`を使うべきなのか? 〜まとめ〜

ここまで見てきたように、`useSyncExternalStore`は、特に外部ストア(状態管理ライブラリ、ブラウザAPIなど)を購読する際に、`useEffect`よりも優れている点がいくつかあるんだ。

  • レンダリングの整合性: `useSyncExternalStore`は、コンポーネントのレンダリングと外部ストアの状態を同期させることで、画面表示の「ズレ」や「チグハグ感」を防いでくれる。これは、ユーザー体験を大きく向上させるポイントだ。
  • コードの簡潔さ: `useState`や`useEffect`を組み合わせていたコードが、`useSyncExternalStore`一つで完結するため、コードがスッキリして読みやすくなる。
  • パフォーマンス: Reactの内部で最適化されているため、より効率的に状態の同期が行われる可能性がある。

どんな時に使うべき?

  • Redux, Zustand, Jotai などの状態管理ライブラリ: これらのライブラリのストアをReactコンポーネントで購読する際に、公式に推奨されている方法の一つ。
  • ブラウザの `localStorage` や `sessionStorage`: これらのAPIは、ブラウザのグローバルな状態と見なせる。コンポーネントがこれらの値を参照し、値の変更に追随したい場合に有効。
  • Web Workers や Service Workers との連携: 非同期で実行されるワーカーからのメッセージを受け取り、UIに反映させたい場合。

`useEffect`はダメなの?

いや、そんなことはない! `useEffect`は、

  • APIからのデータ取得(初回マウント時など)
  • タイマーの設定
  • イベントリスナーの登録(DOMイベントなど)
  • 外部ストアではない、コンポーネント内だけの副作用

など、まだまだたくさんの場面で大活躍してくれる、なくてはならないフックだ。

大切なのは、「道具を、その道具が最も得意なことに使う」ということ。外部ストアとの「同期」という、ちょっと繊細で、レンダリングの整合性が重要な場面では、`useSyncExternalStore`という「専用の道具」を使うのが、より賢くて、より良い選択なんだ。

最後に

どうだったかな? `useSyncExternalStore`、なんだか頼りになりそうなフックだなって思ってもらえたら嬉しいな。

最初はちょっと難しく感じるかもしれないけど、色々なライブラリで使われているのを見かけたり、自分で簡単な例を作ってみたりすると、きっと「なるほど!」って思える瞬間が来るはず。

Reactの世界は、どんどん進化している。新しいフックが登場することで、今まで「ちょっと面倒だな」って思っていたことが、驚くほど簡単に、そして、より堅牢に実装できるようになる。

これからも、この進化を一緒に楽しみながら、より良いWeb体験をユーザーに届けていこうね!もし、この話で「なるほど!」って思ってくれたら、ぜひ周りの開発仲間にも教えてあげてね!

コメント

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