Skip to main content

Store Observer & Reactions (quick reference)

A 5-minute cheat sheet: two small templates that let you make your own components auto-respond to store changes, or fire side effects (auto-save, analytics, logging) whenever data changes.

๐Ÿ“š For a deep dive into MobX tracking rules, all built-in events, and performance tips, read Reactivity & Events. This page is the short, copy-paste version.


Wrap your component in observer โ€” auto-respond to store changesโ€‹

store is a reactive object. To make your React component re-render automatically when store changes, wrap it with observer from mobx-react-lite:

import { observer } from 'mobx-react-lite';
import { createStore } from '@ydesign/react-editor';

const store = createStore({ key: 'YOUR_API_KEY' });

// Re-renders whenever store.objects changes (add/remove element)
const ElementsCount = observer(({ store }) => (
<div>{store.objects.length} elements on the canvas</div>
));

The rules that matter:

  • โœ… MobX tracks by field โ€” whatever you read, you subscribe to
  • โœ… Only re-renders when the fields you read actually change โ€” free performance
  • โŒ Don't forget observer, or the component never updates

Watching the current selection? Just read store.selectedElements:

const Hint = observer(({ store }) => {
const el = store.selectedElements[0];
if (!el) return <div>Select an element first</div>;
return (
<div>
Selected {el.type} ยท at ({Math.round(el.left)}, {Math.round(el.top)})
</div>
);
});

All built-in Ydesign panels and toolbars already use observer internally โ€” you only need to add it to your own components.


Listen for changes: store.on('change') and reactionโ€‹

Don't want to render UI โ€” just run a side effect when data changes (auto-save, analytics, sync to a backend)? Two approaches, pick by scenario.

Option A: the easy one โ€” store.on('change')โ€‹

A high-level subscription to any canvas-object change (add / remove / modify). Under the hood it deep-compares snapshots, so only real changes fire:

const dispose = store.on('change', (objects) => {
console.log('Canvas changed:', objects.length, 'elements');

// Typical use: auto-save
fetch('/api/designs/current', {
method: 'PUT',
body: JSON.stringify(store.toJSON()),
});
});

// Stop subscribing when you're done
dispose();

Good for:

  • Auto-save
  • Dirty tracking / "unsaved changes" hints
  • Simple "something changed, do X"

Option B: any field โ€” mobx.reactionโ€‹

Want to track one specific field instead of the whole canvas? Use MobX's reaction:

import { reaction } from 'mobx';

// Log canvas size whenever it changes
const dispose = reaction(
() => [store.width, store.height], // 1) source
([width, height]) => { // 2) side effect
console.log('Canvas size โ†’', width, 'ร—', height);
},
);

dispose(); // when done

reaction's first argument is a pure read function (selector), the second runs whenever its return value changes. More granular than store.on('change') โ€” unrelated field changes don't bother you.

๐Ÿ’ก Use both at once. store.on('change') handles "something on canvas changed", reaction handles "this specific field changed".


Managing subscriptions in Reactโ€‹

Both options above return a dispose function. In React always put them in useEffect so they're cleaned up on unmount โ€” otherwise you'll leak memory and handlers:

import { useEffect } from 'react';
import { reaction } from 'mobx';
import { observer } from 'mobx-react-lite';

const AutoSave = observer(({ store }) => {
useEffect(() => {
// Auto-save with 1s throttle
const dispose = reaction(
() => store.toJSON(),
(json) => fetch('/api/save', { method: 'PUT', body: JSON.stringify(json) }),
{ delay: 1000 },
);

return dispose; // ๐Ÿ‘ˆ unsubscribe on unmount
}, [store]);

return null;
});

Same pattern for store.on('change', ...):

useEffect(() => {
const dispose = store.on('change', (objects) => {
setDirty(true);
});
return dispose;
}, [store]);

Ydesign pitfall checklistโ€‹

SymptomCauseFix
Component doesn't updateForgot observerexport default observer(MyComp)
reaction fires dozens of times per second, overwhelms backendstore.objects changes rapidly during drag / typingAdd { delay: 1000 } to throttle
Still logging after unmountdispose never calledReturn it from useEffect
Changed a field but component doesn't reactField read is stashed inside useMemo / useCallbackMove the read into the render body
store.editor is sometimes nullEditor is created when <Workspace /> mountsUse optional chaining: store.editor?.xxx

Going deeperโ€‹