Persistence in react-mobx-manager used to be simple and a bit wasteful. Every persisted store had a MobX reaction on toJS(store), and toJS walks every observable the store can reach, including other stores referenced from its fields. So a loading flag flipping in one store could make another store serialize itself again. Then every save wrote the whole storage value: one JSON blob holding all persisted stores.
Version 4.6.0 (released 30 September, PR #53, promoted to latest in #54) reworks this. Short version: it saves less, and less often.
What the listener watches now
The change listener tracks only the fields that are actually persisted. That is the union of the attributes you route to storages, or all exported fields when you use '*'. Stores referenced from those fields are no longer traversed.
So if a persisted store also has short-lived fields, keep them out of storage. They stop causing persist work:
import { Manager, makeExported } from '@lomray/react-mobx-manager'; export default Manager.persistStore(FeedStore, 'feed', { behaviour: 'include', attributes: { local: ['items', 'filters'] }, }); // or inside the store constructor makeExported(this, { isFetching: 'excluded' }); Saves are batched
Stores that change within options.persistDelay (100 ms by default) are serialized once, and each storage gets one write. A store whose persisted state did not change is not written again.
const manager = new Manager({ storage, options: { persistDelay: 300 }, }); This is a behaviour change. A save can now land up to persistDelay after the change. Need the old timing? Set persistDelay: 0. On the server the manager always saves immediately.
The queue is written right away in four cases. The page is hidden (pagehide, or visibilitychange to hidden). A store with queued changes is destroyed. You call manager.destroy(). Or you call manager.flushPersist() yourself. React Native has no page events, so flush when the app goes to background:
import { AppState } from 'react-native'; AppState.addEventListener('change', (state) => { if (state !== 'active') { void manager.flushPersist(); } }); flushPersist() resolves when the writes finish, to false if something failed or persistence is disabled. In tests that read storage right after a change, await it first.
One key per store: KeyValueStorage
LocalStorage, SessionStorage, CookieStorage and AsyncStorage keep all persisted stores in one value, and their data format did not change in 4.6.0. If you have many or large stores, KeyValueStorage saves each store under its own key and writes only the stores that changed. Values also stay small. The docs point out why that matters on Android: a single AsyncStorage row of around 2 MB may fail to load.
import AsyncStorage from '@react-native-async-storage/async-storage'; import { Manager } from '@lomray/react-mobx-manager'; import MobxAsyncStorage from '@lomray/react-mobx-manager/storages/async-storage'; import CombinedStorage from '@lomray/react-mobx-manager/storages/combined-storage'; import KeyValueStorage from '@lomray/react-mobx-manager/storages/key-value-storage'; const manager = new Manager({ storage: new CombinedStorage({ local: new KeyValueStorage({ storage: AsyncStorage, // stores saved by the previous setup are moved on the first start migrateFrom: new MobxAsyncStorage({ storage: AsyncStorage }), }), }), }); Any backend with getItem, setItem and removeItem works, sync or async. MMKV, for example:
import { createMMKV } from 'react-native-mmkv'; const mmkv = createMMKV(); const storage = new KeyValueStorage({ storage: { getItem: (key) => mmkv.getString(key), setItem: (key, value) => mmkv.set(key, value), removeItem: (key) => mmkv.remove(key), }, }); Stores land under stores:<storeId> next to a stores-keys index. prefix changes the prefix.
Migration is conservative
migrateFrom is read only when the new storage has no index yet, which means the first start. The old data stays where it was by default, so rolling back to an older app version still finds it. Pass shouldRemoveMigrated: true to remove it right after migration. If the migrated stores cannot be saved, the app still starts with the old data, and every store is saved again on the next change.
IndexedDB for the web
IndexedDBStorage saves each store as its own record. Writes are asynchronous, and the quota is much larger than the roughly 5 MB localStorage gives you.
import CombinedStorage from '@lomray/react-mobx-manager/storages/combined-storage'; import IndexedDBStorage from '@lomray/react-mobx-manager/storages/indexed-db-storage'; import LocalStorage from '@lomray/react-mobx-manager/storages/local-storage'; const storage = new CombinedStorage({ local: new IndexedDBStorage({ // moved on the first start and used when IndexedDB is not available migrateFrom: new LocalStorage(), }), }); State comes back asynchronously in manager.init(). Wait for it before rendering persisted stores, for example with shouldInit on StoreManagerProvider. If IndexedDB cannot be opened, the storage reads and writes migrateFrom instead.
Smaller things
- Custom storages can implement an optional
saveChanges({ value, changes, toJSON })to write only what changed. Plainsetstill works. -
CookieStoragewarns once in the console when the value goes over 4 KB. - A store that cannot be serialized (a BigInt, say) no longer breaks saving for the others.
Numbers, with a caveat
PR #53 includes a synthetic benchmark: 24 persisted stores, about 750 KB of state, stores referencing other stores, frequent loading-flag changes, an AsyncStorage-like backend, run in Node in client mode.
| before | after, single value | after, KeyValueStorage | |
|---|---|---|---|
| CPU spent on persistence | ~3.8 s | ~0.1-0.2 s | ~0.2 s |
| storage writes | 92 | 11 | 71 (small) |
| bytes written | 47 MB | 7.6 MB | 1.4 MB |
The final persisted state matched the previous version. It is one synthetic setup, and your app will show different numbers.
Links
- Release v4.6.0: https://github.com/Lomray-Software/react-mobx-manager/releases/tag/v4.6.0
- PR #53 with the full change list: https://github.com/Lomray-Software/react-mobx-manager/pull/53
- Persistence recipes: https://lomray-software.github.io/react-mobx-manager/examples/recipes
- Manager API: https://lomray-software.github.io/react-mobx-manager/api/manager
Tests for 4.6.0 pass on MobX 6.16 and 7.0. Install: npm i @lomray/react-mobx-manager @lomray/consistent-suspense