← All material

React · 1 of 2

The External Store

useSyncExternalStore asks for two functions: one to subscribe to changes, one to read the current value. Neither is React-specific, so the store stays plain JavaScript that any part of the app can own:

// cart-store.js
let state = { items: [] };
const listeners = new Set();

export const subscribe = (listener) => {
  listeners.add(listener);
  return () => listeners.delete(listener);
};

export const getSnapshot = () => {
  return state;
};

export const addItem = (item) => {
  state = { ...state, items: [...state.items, item] };
  listeners.forEach((listener) => listener());
};

subscribe returns its own unsubscribe function, which is how React cleans up on unmount without the store knowing React exists.

Replace state, don't mutate it

React compares the previous snapshot to the new one with Object.is. Pushing into state.items leaves the reference identical, so React concludes nothing happened and skips the re-render — hence the new object and new array in addItem.

Both sides use the same module

Nothing above imports React, so the framework you're migrating from can drive the store directly:

// cart-controller.ts
import { subscribe, getSnapshot, addItem } from "./cart-store.js";

export class CartController {
  private unsubscribe?: () => void;

  onInit() {
    this.unsubscribe = subscribe(() => this.render(getSnapshot()));
  }

  addToCart(item: CartItem) {
    addItem(item);
  }

  onDestroy() {
    this.unsubscribe?.();
  }

  private render(cart: CartState) { /* update the DOM */ }
}

A module evaluates once and every importer resolves to that same evaluated module, so there is one state and one set of listeners for the whole app. Nothing has to be wired up to make both frameworks agree on which store they mean.

cart-store.js is imported by both a React module and a custom framework module; neither owns it. React module Custom framework module cart-badge.jsx class CartController useSyncExternalStore subscribe / addItem cart-store.js subscribe · getSnapshot · addItem imports nothing — owned by neither side