useSyncExternalStore γ―γ€ε€–ιƒ¨γ‚Ήγƒˆγ‚’γΈοΏ½?ァブスクラむブを可能にする React οΏ½?フックです。

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

γƒͺフゑレンス

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

ε€–ιƒ¨γƒ‡γƒΌγ‚Ώγ‚Ήγƒˆγ‚’γ‹γ‚‰ε€€γ‚’θͺ­γΏε–γ‚‹γŸγ‚γ«γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?γƒˆγƒƒγƒ—γƒ¬γƒ™γƒ«γ§ useSyncExternalStore を呼び出します。

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

γ“γ‚Œγ―γ€γ‚Ήγƒˆγ‚’γ«γ‚γ‚‹γƒ‡γƒΌγ‚ΏοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ—γΎγ™γ€‚εΌ•ζ•°γ¨γ—γ¦ 2 ぀�?ι–’ζ•°γ‚’ζΈ‘γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™οΌš

  1. subscribe ι–’ζ•°γ―γ‚Ήγƒˆγ‚’γΈοΏ½?γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’ι–‹ε§‹γ—γΎγ™γ€‚γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’θ§£ι™€γ™γ‚‹ι–’ζ•°γ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚
  2. getSnapshot ι–’ζ•°γ―γ€γ‚Ήγƒˆγ‚’γ‹γ‚‰γƒ‡γƒΌγ‚ΏοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θͺ­γΏε–γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚

さらに例を見る

εΌ•ζ•°

  • subscribe: γ‚Ήγƒˆγ‚’γ«γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’ι–‹ε§‹γ—γ€γΎγŸ callback εΌ•ζ•°γ‚’ε—γ‘ε–γ‚‹ι–’ζ•°γ€‚γ‚Ήγƒˆγ‚’γŒε€‰ζ›΄γ•γ‚ŒγŸιš›γ«ζΈ‘γ•γ‚ŒγŸ callback γ‚’ε‘Όγ³ε‡Ίγ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ“γ‚Œγ«γ‚ˆγ‚Šγ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒε†γƒ¬γƒ³γƒ€γƒΌγ•γ‚ŒγΎγ™γ€‚subscribe 閒数は、ァブスクγƒͺプションをクγƒͺγƒΌγƒ³γ‚’γƒƒγƒ—γ™γ‚‹ι–’ζ•°γ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚

  • getSnapshot: γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒεΏ…θ¦γ¨γ™γ‚‹γ‚Ήγƒˆγ‚’γ«γ‚γ‚‹γƒ‡γƒΌγ‚ΏοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ™ι–’ζ•°γ€‚γ‚Ήγƒˆγ‚’γŒε€‰ζ›΄γ•γ‚Œγ¦γ„γͺγ„ε ΄εˆγ€getSnapshot へ�?ε†ε‘Όγ³ε‡Ίγ—γ―εŒγ˜ε€€γ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ‚Ήγƒˆγ‚’γŒε€‰ζ›΄γ•γ‚Œγ¦θΏ”γ•γ‚ŒγŸε€€γŒοΌˆObject.is で比較して)異γͺγ‚‹ε ΄εˆγ€React γ―γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’ε†γƒ¬γƒ³γƒ€γƒΌγ—γΎγ™γ€‚

  • 省η•₯可能 getServerSnapshot: γ‚Ήγƒˆγ‚’οΏ½?データ�?εˆζœŸγ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ™ι–’ζ•°γ€‚γ“γ‚Œγ―γ‚΅γƒΌγƒγƒ¬γƒ³γƒ€γƒͺγƒ³γ‚°δΈ­γ€γŠγ‚ˆγ³γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§οΏ½?ァーバレンダγƒͺγƒ³γ‚°γ•γ‚ŒγŸγ‚³γƒ³γƒ†γƒ³γƒ„οΏ½?ハむドレーション中に�?γΏδ½Ώη”¨γ•γ‚ŒγΎγ™γ€‚γ‚΅γƒΌγƒγ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ―γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ¨γ‚΅γƒΌγƒι–“γ§εŒδΈ€γ§γͺγ‘γ‚Œγ°γͺγ‚‰γšγ€ι€šεΈΈγ―γ‚΅γƒΌγƒγ‹γ‚‰γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ«ζΈ‘γ•γ‚Œγ‚‹γ‚·γƒͺγ‚’γƒ©γ‚€γ‚Ίγ•γ‚ŒγŸγ‚‚οΏ½?です。こ�?εΌ•ζ•°γ‚’ηœη•₯γ™γ‚‹γ¨γ€γ‚΅γƒΌγƒδΈŠγ§οΏ½?γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?レンダγƒͺγƒ³γ‚°γ―γ‚¨γƒ©γƒΌγ‚’η™Ίη”Ÿγ•γ›γΎγ™γ€‚

θΏ”γ‚Šε€€

レンダγƒͺγƒ³γ‚°γƒ­γ‚Έγƒƒγ‚―γ§δ½Ώη”¨γ§γγ‚‹γ‚Ήγƒˆγ‚’οΏ½?現在�?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ€‚

注意点

  • getSnapshot γ«γ‚ˆγ£γ¦θΏ”γ•γ‚Œγ‚‹γ‚Ήγƒˆγ‚’οΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ―γ‚€γƒŸγƒ₯γƒΌγ‚Ώγƒ–γƒ«οΌˆimmutable; ζ›Έγζ›γˆδΈθƒ½οΌ‰γ§γͺγ‘γ‚Œγ°γͺγ‚ŠγΎγ›γ‚“γ€‚θƒŒεΎŒγ§δ½Ώγ£γ¦γ„γ‚‹γ‚Ήγƒˆγ‚’γŒγƒŸγƒ₯ータブルγͺγƒ‡γƒΌγ‚Ώγ‚’ζŒγ£γ¦γ„γ‚‹ε ΄εˆγ€γƒ‡γƒΌγ‚ΏγŒε€‰ζ›΄γ•γ‚ŒγŸε ΄εˆγ―ζ–°γ—γ„γ‚€γƒŸγƒ₯ータブルγͺγ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ—γ€γγ‚Œδ»₯ε€–οΏ½?ε ΄εˆγ―γ‚­γƒ£γƒƒγ‚·γƒ₯γ•γ‚ŒγŸζœ€εΎŒοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ™γ‚ˆγ†γ«γ—γΎγ™γ€‚

  • 再レンダー中に異γͺγ‚‹ subscribe ι–’ζ•°γŒζΈ‘γ•γ‚ŒγŸε ΄εˆγ€React γ―ζ–°γ—γζΈ‘γ•γ‚ŒγŸ subscribe ι–’ζ•°γ‚’δ½Ώγ£γ¦γ‚Ήγƒˆγ‚’γ«ε†γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ—γΎγ™γ€‚γ“γ‚Œγ‚’ι˜²γγ«γ―γ€subscribe γ‚’γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?倖で�?�言します。

  • γƒŽγƒ³γƒ–γƒ­γƒƒγ‚­γƒ³γ‚°εž‹οΏ½?γƒˆγƒ©γƒ³γ‚Έγ‚·γƒ§γƒ³ζ›΄ζ–°οΏ½?ζœ€δΈ­γ«γ‚Ήγƒˆγ‚’οΏ½?ζ›Έγζ›γˆγŒη™Ίη”Ÿγ—γŸε ΄εˆγ€React はそ�?ζ›΄ζ–°γ‚’γƒ–γƒ­γƒƒγ‚­γƒ³γ‚°εž‹γ§θ‘Œγ†γ‚ˆγ†γ«γƒ•γ‚©γƒΌγƒ«γƒγƒƒγ‚―γ—γΎγ™γ€‚ε…·δ½“ηš„γ«γ―γ€React は DOM に更新を適用する前に getSnapshot γ‚’ε†εΊ¦ε‘Όγ³ε‡Ίγ—γΎγ™γ€‚γγ“γ§ζœ€εˆοΏ½?倀とは異γͺγ‚‹ε€€γŒθΏ”γ•γ‚ŒγŸε ΄εˆγ€React γ―γƒˆγƒ©γƒ³γ‚Έγ‚·γƒ§γƒ³οΏ½?ζ›΄ζ–°γ‚’ζœ€εˆγ‹γ‚‰γ‚„γ‚Šη›΄γ—γΎγ™γŒγ€ε†θ©¦θ‘Œζ™‚γ«γ―γƒ–γƒ­γƒƒγ‚­γƒ³γ‚°εž‹οΏ½?ζ›΄ζ–°γ‚’θ‘Œγ†γ“γ¨γ§γ€η”»ι’δΈŠοΏ½?ε…¨γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒγ‚Ήγƒˆγ‚’γ‹γ‚‰οΏ½?εŒδΈ€γƒγƒΌγ‚Έγƒ§γƒ³οΏ½?ε€€γ‚’εζ˜ γ—γ¦γ„γ‚‹γ“γ¨γ‚’δΏθ¨Όγ—γΎγ™γ€‚

  • useSyncExternalStore γ‹γ‚‰θΏ”γ•γ‚Œγ‚‹ε€€γ«εŸΊγ₯γ„γ¦γƒ¬γƒ³γƒ€γƒΌγ‚’γ‚΅γ‚Ήγƒšγƒ³γƒ‰γ•γ›γ‚‹γ“γ¨γ―ζŽ¨ε₯¨γ•γ‚Œγ¦γ„γΎγ›γ‚“γ€‚ε€–ιƒ¨γ‚Ήγƒˆγ‚’γ§θ΅·γγŸε€‰ζ›΄γ―γƒŽγƒ³γƒ–γƒ­γƒƒγ‚­γƒ³γ‚°εž‹οΏ½?γƒˆγƒ©γƒ³γ‚Έγ‚·γƒ§γƒ³ζ›΄ζ–°γ¨γ—γ¦γƒžγƒΌγ‚―γ™γ‚‹γ“γ¨γŒγ§γγͺγ„γŸγ‚γ€η›΄θΏ‘οΏ½? Suspense γƒ•γ‚©γƒΌγƒ«γƒγƒƒγ‚―γŒθ΅·ε‹•γ—γ¦γ—γΎγ„γΎγ™γ€‚ζ—’γ«η”»ι’δΈŠγ«θ‘¨η€Ίγ•γ‚Œγ¦γ„γ‚‹γ‚³γƒ³γƒ†γƒ³γƒ„γŒγƒ­γƒΌγƒ‡γ‚£γƒ³γ‚°γ‚Ήγƒ”γƒŠγ§ιš γ‚Œγ¦γ—γΎγ†γŸγ‚γ€ι€šεΈΈγ―ζœ›γΎγ—γγͺいユーア体験に぀γͺγŒγ‚ŠγΎγ™γ€‚

    δΎ‹γˆγ°δ»₯δΈ‹οΏ½?γ‚ˆγ†γͺγ‚³γƒΌγƒ‰γ―ζŽ¨ε₯¨γ•γ‚ŒγΎγ›γ‚“γ€‚

    const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));

    function ShoppingApp() {
    const selectedProductId = useSyncExternalStore(...);

    // ❌ Calling `use` with a Promise dependent on `selectedProductId`
    const data = use(fetchItem(selectedProductId))

    // ❌ Conditionally rendering a lazy component based on `selectedProductId`
    return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;
    }

使用法

ε€–ιƒ¨γ‚Ήγƒˆγ‚’γΈοΏ½?ァブスクラむブ

React γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?ほとんどは、props、state γŠγ‚ˆγ³γ‚³γƒ³γƒ†γ‚―γ‚Ήγƒˆγ‹γ‚‰οΏ½?みデータをθͺ­γΏε–γ‚ŠγΎγ™γ€‚γ—γ‹γ—γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―ζ™‚ι–“γ¨ε…±γ«ε€‰εŒ–γ™γ‚‹ React ε€–οΏ½?γ‚Ήγƒˆγ‚’γ‹γ‚‰γƒ‡γƒΌγ‚Ώγ‚’θͺ­γΏε–γ‚‹εΏ…θ¦γŒγ‚γ‚‹ε ΄εˆγŒγ‚γ‚ŠγΎγ™γ€‚γ“γ‚Œγ«γ―δ»₯δΈ‹οΏ½?γ‚ˆγ†γͺγ‚‚οΏ½?γŒε«γΎγ‚ŒγΎγ™οΌš

  • React οΏ½?ε€–ιƒ¨γ§ηŠΆζ…‹γ‚’δΏζŒγ™γ‚‹γ‚΅γƒΌγƒ‰γƒ‘γƒΌγƒ†γ‚£οΏ½?ηŠΆζ…‹οΏ½?�理ラむブラγƒͺ。
  • 可倉�?倀を、そ�?ε€‰ζ›΄γ«γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ™γ‚‹γŸγ‚οΏ½?γ‚€γƒ™γƒ³γƒˆγ¨γ‚‚γ«ε…¬ι–‹γ™γ‚‹γƒ–γƒ©γ‚¦γ‚Ά API。

ε€–ιƒ¨γƒ‡γƒΌγ‚Ώγ‚Ήγƒˆγ‚’γ‹γ‚‰ε€€γ‚’θͺ­γΏε–γ‚‹γŸγ‚γ«γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?ζœ€δΈŠδ½γ§ useSyncExternalStore を呼び出します。

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

γ“γ‚Œγ―γ‚Ήγƒˆγ‚’ε†…οΏ½?データ�?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ—γΎγ™γ€‚εΌ•ζ•°γ¨γ—γ¦ 2 ぀�?ι–’ζ•°γ‚’ζΈ‘γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™οΌš

  1. subscribe ι–’ζ•°γ―γ€γ‚Ήγƒˆγ‚’γΈοΏ½?γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’θ‘Œγ„γ€γΎγŸγ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’θ§£ι™€γ™γ‚‹ι–’ζ•°γ‚’θΏ”γ—γΎγ™γ€‚
  2. getSnapshot ι–’ζ•°γ―γ€γ‚Ήγƒˆγ‚’γ‹γ‚‰γƒ‡γƒΌγ‚ΏοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θͺ­γΏε–γ‚ŠγΎγ™γ€‚

React γ―γ“γ‚Œγ‚‰οΏ½?ι–’ζ•°γ‚’δ½Ώγ£γ¦γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’γ‚Ήγƒˆγ‚’γ«γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ•γ‚ŒγŸηŠΆζ…‹γ«δΏγ‘γ€ε€‰ζ›΄γŒγ‚γ‚‹γŸγ³γ«ε†γƒ¬γƒ³γƒ€γƒΌγ—γΎγ™γ€‚

δΎ‹γˆγ°γ€δ»₯δΈ‹οΏ½?γ‚΅γƒ³γƒ‰γƒœγƒƒγ‚―γ‚Ήγ§γ―γ€todosStore は React οΏ½?ε€–ιƒ¨γ«γƒ‡γƒΌγ‚Ώγ‚’δΏε­˜γ™γ‚‹ε€–ιƒ¨γ‚Ήγƒˆγ‚’γ¨γ—γ¦οΏ½?οΏ½θ£…γ•γ‚Œγ¦γ„γΎγ™γ€‚TodosApp γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―γ€useSyncExternalStore フックを使ってそ�?ε€–ιƒ¨γ‚Ήγƒˆγ‚’γ«ζŽ₯ηΆšγ—γΎγ™γ€‚

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

補袳

ε―θƒ½γ§γ‚γ‚Œγ°γ€React 硄み込み�? state οΏ½?οΏ½η†ζ©Ÿθƒ½γ§γ‚γ‚‹ useState γŠγ‚ˆγ³ useReducer γ‚’δ»£γ‚γ‚Šγ«δ½Ώη”¨γ™γ‚‹γ“γ¨γ‚’γŠε‹§γ‚γ—γΎγ™γ€‚useSyncExternalStore API γ―γ€ζ—’ε­˜οΏ½?非 React γ‚³γƒΌγƒ‰γ¨η΅±εˆγ™γ‚‹εΏ…θ¦γŒγ‚γ‚‹ε ΄εˆγ«δΈ»γ«ε½Ήη«‹γ‘γΎγ™γ€‚


ブラウア API へ�?ァブスクラむブ

useSyncExternalStore γ‚’θΏ½εŠ γ™γ‚‹γ‚‚γ† 1 ぀�?η†η”±γ―γ€ζ™‚ι–“γ¨γ¨γ‚‚γ«ε€‰εŒ–γ™γ‚‹γ€γƒ–γƒ©γ‚¦γ‚ΆγŒε…¬ι–‹γ™γ‚‹ε€€γ«γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ—γŸγ„ε ΄εˆγ§γ™γ€‚γŸγ¨γˆγ°γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒγƒγƒƒγƒˆγƒ―γƒΌγ‚―ζŽ₯ηΆšγŒγ‚’γ‚―γƒ†γ‚£γƒ–γ‹γ©γ†γ‹γ‚’θ‘¨η€Ίγ—γŸγ„γ¨γ—γΎγ™γ€‚γƒ–γƒ©γ‚¦γ‚Άγ―γ€γ“οΏ½?ζƒ…ε ±γ‚’ navigator.onLine というプロパティを介して公開します。

こ�?倀は React οΏ½?ηŸ₯らγͺγ„γ¨γ“γ‚γ§ε€‰ζ›΄γ•γ‚Œγ‚‹ε―θƒ½ζ€§γŒγ‚γ‚‹οΏ½?で、useSyncExternalStore γ§γγ‚Œγ‚’θͺ­γΏε–るべきです。

import { useSyncExternalStore } from 'react';

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}

getSnapshot ι–’ζ•°γ‚’οΏ½?οΏ½θ£…γ™γ‚‹γŸγ‚γ«γ―γ€γƒ–γƒ©γ‚¦γ‚Ά API γ‹γ‚‰ηΎεœ¨οΏ½?ε€€γ‚’θͺ­γΏε–γ‚‹γ“γ¨γŒεΏ…θ¦γ§γ™οΌš

function getSnapshot() {
return navigator.onLine;
}

欑に、subscribe ι–’ζ•°γ‚’οΏ½?οΏ½θ£…γ™γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚δΎ‹γˆγ°γ€navigator.onLine γŒε€‰εŒ–γ™γ‚‹γ¨γ€γƒ–γƒ©γ‚¦γ‚Άγ― window γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆδΈŠγ§ online γŠγ‚ˆγ³ offline γ¨γ„γ†γ‚€γƒ™γƒ³γƒˆγ‚’η™Ίη«γ—γΎγ™γ€‚γ“γ‚Œγ‚‰ε―ΎεΏœγ™γ‚‹γ‚€γƒ™γƒ³γƒˆγ« callback εΌ•ζ•°γ‚’η™»ιŒ²γ—γ€γγ‚Œγ‚’θ§£ι™€γ™γ‚‹ι–’ζ•°γ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™οΌš

function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

γ“γ‚Œγ§ React は、倖部�? navigator.onLine API から倀をθͺ­γΏε–る方法と、そ�?倉更にァブスクラむブする方法をηŸ₯γ‚‹γ“γ¨γŒγ§γγΎγ™γ€‚γƒγƒƒγƒˆγƒ―γƒΌγ‚―γ‹γ‚‰γƒ‡γƒγ‚€γ‚Ήγ‚’εˆ‡ζ–­γ™γ‚‹γ¨γ€γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒεεΏœγ—γ¦ε†γƒ¬γƒ³γƒ€γƒΌγ•γ‚Œγ‚‹γ“γ¨γ«ζ³¨οΏ½?γ—γ¦γγ γ•γ„οΌš

import { useSyncExternalStore } from 'react';

export default function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  return <h1>{isOnline ? 'βœ… Online' : '❌ Disconnected'}</h1>;
}

function getSnapshot() {
  return navigator.onLine;
}

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}


γƒ­γ‚Έγƒƒγ‚―γ‚’γ‚«γ‚Ήγ‚Ώγƒ γƒ•γƒƒγ‚―γ«ζŠ½ε‡Ίγ™γ‚‹

ι€šεΈΈγ€useSyncExternalStore γ‚’η›΄ζŽ₯γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆε†…γ«θ¨˜θΏ°γ™γ‚‹γ“γ¨γ―γ‚γ‚ŠγΎγ›γ‚“γ€‚δ»£γ‚γ‚Šγ«γ€θ‡ͺεˆ†θ‡ͺθΊ«οΏ½?γ‚«γ‚Ήγ‚Ώγƒ γƒ•γƒƒγ‚―γ‹γ‚‰ε‘Όγ³ε‡Ίγ™γ“γ¨γŒδΈ€θˆ¬ηš„γ§γ™γ€‚γ“γ‚Œγ«γ‚ˆγ‚Šγ€η•°γͺγ‚‹γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‹γ‚‰εŒγ˜ε€–ιƒ¨γ‚Ήγƒˆγ‚’γ‚’δ½Ώη”¨γ§γγΎγ™γ€‚

δΎ‹γˆγ°γ€γ“οΏ½?γ‚«γ‚Ήγ‚Ώγƒ  useOnlineStatus γƒ•γƒƒγ‚―γ―γƒγƒƒγƒˆγƒ―γƒΌγ‚―γŒγ‚ͺγƒ³γƒ©γ‚€γƒ³γ§γ‚γ‚‹γ‹γ©γ†γ‹γ‚’θΏ½θ·‘γ—γΎγ™οΌš

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}

function getSnapshot() {
// ...
}

function subscribe(callback) {
// ...
}

γ“γ‚Œγ§γ€η•°γͺγ‚‹γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγŒγ€εŸΊζœ¬ηš„γͺοΏ½?οΏ½θ£…γ‚’ηΉ°γ‚ŠθΏ”γ™γ“γ¨γͺく useOnlineStatus γ‚’ε‘Όγ³ε‡Ίγ›γ‚‹γ‚ˆγ†γ«γͺγ‚ŠγΎγ—γŸοΌš

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? 'βœ… Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('βœ… Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}


ァーバーレンダγƒͺング�?γ‚΅γƒγƒΌγƒˆγ‚’θΏ½εŠ γ™γ‚‹

React γ‚’γƒ—γƒͺγŒγ‚΅γƒΌγƒγƒ¬γƒ³γƒ€γƒͺγƒ³γ‚°γ‚’δ½Ώη”¨γ—γ¦γ„γ‚‹ε ΄εˆγ€React γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ―εˆζœŸ HTML γ‚’η”Ÿζˆγ™γ‚‹γŸγ‚γ«γƒ–γƒ©γ‚¦γ‚Άη’°ε’ƒε€–γ§γ‚‚οΏ½?οΏ½θ‘Œγ•γ‚ŒγΎγ™γ€‚γ“γ‚Œγ«γ‚ˆγ‚Šγ€ε€–ιƒ¨γ‚Ήγƒˆγ‚’γΈοΏ½?ζŽ₯ηΆšγ«ι–’γ™γ‚‹γ„γγ€γ‹οΏ½?θͺ²ι‘ŒγŒη”Ÿγ˜γΎγ™γ€‚

  • ブラウア専用�? API にζŽ₯ηΆšγ—γ¦γ„γ‚‹ε ΄εˆγ€γγ‚Œγ―γ‚΅γƒΌγƒδΈŠγ§γ―ε­˜εœ¨γ—γͺγ„γŸγ‚ε‹•δ½œγ—γΎγ›γ‚“γ€‚
  • ァードパーティ�?γƒ‡γƒΌγ‚Ώγ‚Ήγƒˆγ‚’γ«ζŽ₯ηΆšγ—γ¦γ„γ‚‹ε ΄εˆγ€γ‚΅γƒΌγƒγ¨γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆι–“γ§γοΏ½?γƒ‡γƒΌγ‚Ώγ‚’δΈ€θ‡΄γ•γ›γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚

γ“γ‚Œγ‚‰οΏ½?ε•ι‘Œγ‚’θ§£ζ±Ίγ™γ‚‹γŸγ‚γ«γ€useSyncExternalStore に getServerSnapshot 閒数を第 3 εΌ•ζ•°γ¨γ—γ¦ζΈ‘γ—γΎγ™οΌš

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}

function getSnapshot() {
return navigator.onLine;
}

function getServerSnapshot() {
return true; // Always show "Online" for server-generated HTML
}

function subscribe(callback) {
// ...
}

getServerSnapshot 閒数は getSnapshot γ¨δΌΌγ¦γ„γΎγ™γŒγ€δ»₯δΈ‹οΏ½? 2 ぀�?犢況で�?み�?οΏ½θ‘Œγ•γ‚ŒγΎγ™οΌš

  • γ‚΅γƒΌγƒδΈŠγ§γ€HTML γ‚’η”Ÿζˆγ™γ‚‹ιš›γ«οΏ½?οΏ½θ‘Œγ•γ‚Œγ‚‹γ€‚
  • γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆδΈŠγ§γ€React γŒγ‚΅γƒΌγƒ HTML γ‚’γ‚€γƒ³γ‚Ώγƒ©γ‚―γƒ†γ‚£γƒ–γ«γ™γ‚‹γ¨γγ€γ€γΎγ‚Šγƒγ‚€γƒ‰γƒ¬γƒΌγ‚·γƒ§γƒ³δΈ­γ«οΏ½?οΏ½θ‘Œγ•γ‚Œγ‚‹γ€‚

γ“γ‚Œγ«γ‚ˆγ‚Šγ€γ‚’γƒ—γƒͺγŒγ‚€γƒ³γ‚Ώγƒ©γ‚―γƒ†γ‚£γƒ–γ«γͺγ‚‹ε‰γ«δ½Ώη”¨γ•γ‚Œγ‚‹εˆζœŸοΏ½?γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆε€€γ‚’ζŒ‡οΏ½?�できます。ァーバレンダγƒͺング中に意味�?γ‚γ‚‹εˆζœŸε€€γŒε­˜εœ¨γ—γͺγ„ε ΄εˆγ―γ€γ“οΏ½?εΌ•ζ•°γ‚’ηœη•₯γ—γ¦γ€εΌ·εˆΆηš„γ«γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ§γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γ‚ˆγ†γ«γ—γΎγ™γ€‚

補袳

εˆε›žοΏ½?γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγƒ¬γƒ³γƒ€γƒͺングでは、getServerSnapshot γ―γ‚΅γƒΌγƒγ§θΏ”γ—γŸγ‚‚οΏ½?γ¨εΏ…γšζ­£η’Ίγ«εŒδΈ€οΏ½?γƒ‡γƒΌγ‚Ώγ‚’θΏ”γ™γ‚ˆγ†γ«γ—γ¦γγ γ•γ„γ€‚δΎ‹γˆγ°γ€getServerSnapshot γŒγ‚΅γƒΌγƒδΈŠγ§δΊ‹ε‰γ«ζΊ–ε‚™γ•γ‚ŒγŸγ‚Ήγƒˆγ‚’γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’θΏ”γ—γŸε ΄εˆγ€γ“οΏ½?γ‚³γƒ³γƒ†γƒ³γƒ„γ‚’γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆγ«θ»’ι€γ™γ‚‹εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ“γ‚Œγ‚’θ‘Œγ† 1 ぀�?方法は、ァーバレンダγƒͺング中に window.MY_STORE_DATA οΏ½?γ‚ˆγ†γͺグローバル倉数を設�?�する <script> γ‚Ώγ‚°γ‚’η™Ίθ‘Œγ—γ¦γŠγγ€γ‚―γƒ©γ‚€γ‚’γƒ³γƒˆοΏ½? getServerSnapshot でそ�?グローバル倉数からθͺ­γΏθΎΌγ‚€γ“とです。あγͺγŸγŒδ½Ώγ†ε€–ιƒ¨γ‚Ήγƒˆγ‚’γ«γοΏ½?ζ–Ήζ³•γŒθ¨˜θΌ‰γ•γ‚Œγ¦γ„γ‚‹γ―γšγ§γ™γ€‚


γƒˆγƒ©γƒ–γƒ«γ‚·γƒ₯ーティング

β€œThe result of getSnapshot should be cached” γ¨γ„γ†γ‚¨γƒ©γƒΌγŒε‡Ίγ‚‹

こ�?エラーは、getSnapshot ι–’ζ•°γŒε‘Όγ°γ‚Œγ‚‹γŸγ³γ«ζ–°γ—γ„γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ‚’θΏ”γ—γ¦γ„γ‚‹γ“γ¨γ‚’ζ„ε‘³γ—γΎγ™γ€‚δΎ‹γˆγ°οΌš

function getSnapshot() {
// πŸ”΄ Do not return always different objects from getSnapshot
return {
todos: myStore.todos
};
}

getSnapshot οΏ½?θΏ”γ‚Šε€€γŒε‰ε›žγ¨η•°γͺγ‚‹ε ΄εˆγ€React γ―γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆγ‚’ε†γƒ¬γƒ³γƒ€γƒΌγ—γΎγ™γ€‚γ“οΏ½?γŸγ‚γ€εΈΈγ«η•°γͺる倀を返すと焑限ループにε…₯γ‚Šγ€γ“οΏ½?γ‚¨γƒ©γƒΌγŒη™Ίη”Ÿγ—γΎγ™γ€‚

getSnapshot γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ―γ€οΏ½?οΏ½ιš›γ«δ½•γ‹γŒε€‰ζ›΄γ•γ‚ŒγŸε ΄εˆγ«οΏ½?み、εˆ₯οΏ½?γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ‚Ήγƒˆγ‚’γ«γ‚€γƒŸγƒ₯ータブルγͺγƒ‡γƒΌγ‚ΏγŒε«γΎγ‚Œγ¦γ„γ‚‹ε ΄εˆγ―γ€γοΏ½?データを直ζŽ₯θΏ”γ™γ“γ¨γŒγ§γγΎγ™οΌš

function getSnapshot() {
// βœ… You can return immutable data
return myStore.todos;
}

γ‚Ήγƒˆγ‚’γƒ‡γƒΌγ‚ΏγŒγƒŸγƒ₯ータブルγͺε ΄εˆγ€getSnapshot 閒数はそ�?γ‚€γƒŸγƒ₯ータブルγͺγ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ™εΏ…θ¦γŒγ‚γ‚ŠγΎγ™γ€‚γ€γΎγ‚Šγ€ζ–°γ—γ„γ‚ͺγƒ–γ‚Έγ‚§γ‚―γƒˆγ‚’δ½œζˆγ™γ‚‹εΏ…θ¦γ―γ‚γ‚ŠγΎγ™γŒγ€ζ―Žε›žδ½œζˆγ—γ¦γ―γ„γ‘γͺいということです。そ�?δ»£γ‚γ‚Šγ«γ€ζœ€εΎŒγ«θ¨ˆοΏ½?οΏ½γ•γ‚ŒγŸγ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’δΏε­˜γ—γ¦γŠγγ€γ‚Ήγƒˆγ‚’ε†…οΏ½?γƒ‡γƒΌγ‚ΏγŒε€‰ζ›΄γ•γ‚Œγ¦γ„γͺγ„ε ΄εˆγ―ε‰ε›žγ¨εŒγ˜γ‚ΉγƒŠγƒƒγƒ—γ‚·γƒ§γƒƒγƒˆγ‚’θΏ”γ™γ‚ˆγ†γ«γ—γΎγ™γ€‚γƒŸγƒ₯ータブルγͺγƒ‡γƒΌγ‚ΏγŒε€‰ζ›΄γ•γ‚ŒγŸγ‹γ©γ†γ‹γ‚’εˆ€ζ–­γ™γ‚‹ζ–Ήζ³•γ―γ€γƒŸγƒ₯ータブルγͺγ‚Ήγƒˆγ‚’γ«γ‚ˆγ£γ¦η•°γͺγ‚ŠγΎγ™γ€‚


subscribe γŒζ―Žγƒ¬γƒ³γƒ€γƒΌγ”γ¨γ«ε‘Όγ³ε‡Ίγ•γ‚Œγ‚‹

こ�? subscribe ι–’ζ•°γ―γ‚³γƒ³γƒγƒΌγƒγƒ³γƒˆοΏ½?内部で�?οΏ½ηΎ©γ•γ‚Œγ¦γ„γ‚‹γŸγ‚γ€ε†γƒ¬γƒ³γƒ€γƒΌγ™γ‚‹γŸγ³γ«η•°γͺγ£γŸε€€γ«γͺγ‚ŠγΎγ™οΌš

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// 🚩 Always a different function, so React will resubscribe on every re-render
function subscribe() {
// ...
}

// ...
}

React は、再レンダー間で異γͺγ‚‹ subscribe ι–’ζ•°γ‚’ζΈ‘γ™γ¨γ€γ‚Ήγƒˆγ‚’γ«ε†γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ—γΎγ™γ€‚γ“γ‚ŒγŒγƒ‘γƒ•γ‚©γƒΌγƒžγƒ³γ‚ΉοΏ½?ε•ι‘Œγ‚’εΌ•γθ΅·γ“γ—γ€ε†γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ‚’ιΏγ‘γŸγ„ε ΄εˆγ―γ€subscribe ι–’ζ•°γ‚’ε€–ιƒ¨γ«η§»ε‹•γ—γ¦γγ γ•γ„οΌš

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}

// βœ… Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}

あるいは、subscribe γ‚’ useCallback γ§γƒ©γƒƒγƒ—γ™γ‚‹γ“γ¨γ§γ€εΌ•ζ•°γŒε€‰ζ›΄γ•γ‚ŒγŸγ¨γοΏ½?γΏε†γ‚΅γƒ–γ‚Ήγ‚―γƒ©γ‚€γƒ–γ™γ‚‹γ“γ¨γŒγ§γγΎγ™οΌš

function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// βœ… Same function as long as userId doesn't change
const subscribe = useCallback(() => {
// ...
}, [userId]);

// ...
}