Skip to content

Installation ​

bash
npm install @statesync/mobx @statesync/core

Purpose ​

@statesync/mobx applies snapshots to a MobX observable store. Mutates the store in place — the reference never changes, so autorun, reaction, and observer() keep working.

API ​

createMobXSnapshotApplier(store, options?)

OptionTypeDefaultDescription
mode'patch' | 'replace''patch'Apply strategy (see below)
runInAction(fn: () => void) => void—Wraps mutations in a MobX action (pass runInAction from mobx)
pickKeysReadonlyArray<keyof State>—Only update these keys (mutually exclusive with omitKeys)
omitKeysReadonlyArray<keyof State>—Protect these keys from updates
toState(data, ctx) => Partial<State>identityMap snapshot data to state shape. ctx contains { store }
strictbooleantrueThrow if toState returns a non-object

Store interface ​

The adapter uses a type alias — no mobx import required:

ts
type MobXStoreLike<State> = State;

Any MobX observable (class with makeAutoObservable, object from observable()) satisfies this.

runInAction ​

MobX requires mutations inside action or runInAction when enforceActions is 'always' or 'observed'. Pass runInAction from mobx to ensure compatibility:

ts
import { runInAction } from 'mobx';

const applier = createMobXSnapshotApplier(store, { runInAction });

All property assignments happen inside a single runInAction call, so MobX batches updates and triggers reactions once.

When omitted, the adapter assigns keys directly — this works when enforceActions is 'never', but will throw if strict mode is enabled.

Apply semantics ​

ModeBehavior
'patch' (default)store[key] = value for each filtered key inside runInAction
'replace'delete store[key] for stale keys, then store[key] = value — all inside a single runInAction

Why not replace the store?

MobX's reactivity relies on the original observable reference. Replacing it would break all autorun and observer() subscriptions. The adapter always mutates in place.

Replace mode and class instances

Replace mode uses delete to remove stale keys. This works well with plain observables (observable({})), but may behave unexpectedly with class instances where properties are defined on the prototype. For class-based stores, prefer 'patch' mode.

Example ​

ts
import { makeAutoObservable, runInAction } from 'mobx';
import { createMobXSnapshotApplier } from '@statesync/mobx';
import { createRevisionSync } from '@statesync/core';

class SettingsStore {
  theme = 'light';
  locale = 'en';
  notifications = true;
  isMenuOpen = false;

  constructor() { makeAutoObservable(this); }
}

const store = new SettingsStore();

const applier = createMobXSnapshotApplier(store, {
  runInAction,
  omitKeys: ['isMenuOpen'],
});

const sync = createRevisionSync({
  topic: 'user-prefs',
  subscriber,
  provider,
  applier,
});

await sync.start();

With toState mapping ​

ts
interface PrefsDTO {
  ui_theme: string;
  ui_locale: string;
}

const applier = createMobXSnapshotApplier(store, {
  runInAction,
  toState: (data: PrefsDTO, { store }) => ({
    theme: data.ui_theme,
    locale: data.ui_locale,
  }),
});

See also ​

Released under the MIT License.