Closed32

useSyncExternalStoreのdocsを読む

hajimismhajimism

Refference

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

function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  // ...
}
  1. The subscribe function should subscribe to the store and return a function that unsubscribes.
  2. The getSnapshot function should read a snapshot of the data from the store.
hajimismhajimism

なんでLayoutEffectなんかはよくわからん。"This needs to be updated in the layout phase"とコメントは書いてあるけど、なんで?

hajimismhajimism

subscribe(handleStoreChange);を見るに、subscribe側でもcallbackを呼べばre-renderできるってことだよね?それをやるタイミングはよくわかってないけれども。

hajimismhajimism

When the store changes, it should invoke the provided callback. This will cause the component to re-render.

callbackによりre-renderっておもっきし書いてあったわ

hajimismhajimism

While the store has not changed, repeated calls to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component.

storeの更新を検知するための機能ということがよくわかる

hajimismhajimism

Returns

The current snapshot of the store which you can use in your rendering logic.

snapshotが帰ってきますよと。だからuseAtomValueみたいなんに近い。storeの更新関数はこの関数の外側にある。

hajimismhajimism

The store snapshot returned by getSnapshot must be immutable.

そうですね。

hajimismhajimism

Usage

いつ使いたくなるのか気になる。

毎度思うけど、この色付きの説明わかりやすすぎる。

hajimismhajimism

todoStoreはこんな感じらしい。まあわかるっちゃわかる。

let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

hajimismhajimism

When possible, we recommend using built-in React state with useState and useReducer instead. The useSyncExternalStore API is mostly useful if you need to integrate with existing non-React code.

大事なことかいてあった。「使えるなら普通にReactの機能使え。既存のコードを統合するのがつらい時だけこっち」って感じね。

hajimismhajimism

Another reason to add useSyncExternalStore is when you want to subscribe to some value exposed by the browser that changes over time. For example, suppose that you want your component to display whether the network connection is active. The browser exposes this information via a property called navigator.onLine.

This value can change without React’s knowledge, so you should read it with useSyncExternalStore.

なるほど、たしかにこの例は見たことがあったかもしれない。change overtime な browser API経由の値。

hajimismhajimism
import { useSyncExternalStore } from 'react';

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

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);
  };
}

なるほどですねえ

hajimismhajimism

↑だとserver renderingのときに困るから、こいつを追加する

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

getServerSnapshotが呼び出されるのは次の2点のみ

  • It runs on the server when generating the HTML.
  • It runs on the client during hydration, i.e. when React takes the server HTML and makes it interactive.
hajimismhajimism

↑serverで適当な値返すのがアレだったら、client renderingを強制しろとのこと

hajimismhajimism

Make sure that getServerSnapshot returns the same exact data on the initial client render as it returned on the server. For example, if getServerSnapshot returned some prepopulated store content on the server, you need to transfer this content to the client. One way to do this is to emit a <script> tag during server rendering that sets a global like window.MY_STORE_DATA, and read from that global on the client in getServerSnapshot. Your external store should provide instructions on how to do that.

これはあれだね、最近のDeep diveに書いてあったね。initial JSXをClientに送りつけとくやつ。

hajimismhajimism

Troubleshooting

getSnapshotで新規Objectを返すと、storeが更新されなくてもsnapshot取得のたびに変わってくるのでだめ

React will re-render the component if getSnapshot return value is different from the last time.

function getSnapshot() {
  // 🔴 Do not return always different objects from getSnapshot
  return {
    todos: myStore.todos
  };
}

Your getSnapshot object should only return a different object if something has actually changed. If your store contains immutable data, you can return that data directly:

function getSnapshot() {
  // ✅ You can return immutable data
  return myStore.todos;
}
hajimismhajimism

同様にsubscribe functionをcomponent内部で定義するのもだめ

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  
  // 🚩 Always a different function, so React will resubscribe on every re-render
  function subscribe() {
    // ...
  }

  // ...
}
hajimismhajimism

読んでみて

利用頻度は少なそうだがReactの考え方の再確認として勉強にはなった。useOnlineStatusはふつうに使えるか。Third-pairty libraryでもおかしくないAPIだと思うけど、相当困る人がいたのか、公式見解として出す意味があったのか、どうか。

次はuseReducerとかSuspenseとか、まあまあ使用頻度の高いやつ行きたい。

このスクラップは2023/07/10にクローズされました