Subscribing to a Backbone Model

A Backbone model announces changes through its own plain event methods, on()/off(), rather than through React state – exactly the kind of external, mutable source useSyncExternalStore was built for.


import React, { useSyncExternalStore } from 'react';
import { createRoot } from 'react-dom/client';
import Backbone from 'backbone';

function useModelField(model, field) {
  return useSyncExternalStore(
    (callback) => {
      model.on('change:' + field, callback);
      return () => model.off('change:' + field, callback);
    },
    () => model.get(field)
  );
}

function Item({ model }) {
  const text = useModelField(model, 'text');
  return <li>{text}</li>;
}

const model = new Backbone.Model({ text: 'A' });
const root = createRoot(document.getElementById('root'));
root.render(<Item model={model} />);

Compare this to the useForceUpdate-based Item component in Using Backbone Models in Components. The rewrite above has two concrete advantages: it stays correct under React's concurrent rendering, where a plain forced re-render can't guarantee every consumer of the same model observes it consistently within one render pass; and it takes an optional third getServerSnapshot argument for server rendering, which the manual pattern doesn't address at all. The same swap works for that page's List/collection example too – it just takes a little more care picking a getSnapshot that keeps returning an equal result while nothing has changed and a genuinely different one once membership does, so a simple derived value like collection.length is a safe, simple choice for tracking additions and removals specifically.