createBackendStore — backend-synced preferences
createBackendStore wraps any React Admin Store and mirrors a whitelisted subset of its keys to a backend
API. Reads stay local; writes are debounced and pushed asynchronously. It is what powers cross-device
synchronization of user preferences (theme, locale, dashboard layout, datagrid column setups, saved queries).
import {createBackendStore, BackendApi} from 'habarico';
import {localStorageStore} from 'react-admin';
const api: BackendApi = {
list: () => fetch('/user-preferences/').then((r) => r.json()),
put: (key, value) => fetch(`/user-preferences/${key}/`, {method: 'PUT', body: JSON.stringify(value)}).then(() => {}),
delete: (key) => fetch(`/user-preferences/${key}/`, {method: 'DELETE'}).then(() => {}),
deleteAll: () => fetch('/user-preferences/', {method: 'DELETE'}).then(() => {}),
};
const store = createBackendStore(localStorageStore(), {
api,
whitelist: ['theme', 'locale', 'preferences.*.columns'],
debounceMs: 500, // default 500ms
retryBaseMs: 1000, // default 1s
retryMaxMs: 30000, // default 30s
onError: (err) => console.warn(err),
});
Behaviour:
- Hydrate on setup — on
store.setup()the wrapper callsapi.list()and writes every returned key into the base store. On failure it retries with exponential backoff (retryBaseMs * 2^attempt, capped atretryMaxMs), except when the error carriesstatus401 or 403: a rejected credential is not retried.store.teardown()cancels a pending retry; the nextstore.setup()hydrates afresh. - Debounced PUT —
setItemwrites through to the base store immediately, then schedules a singleapi.put(key, value)afterdebounceMs. Repeated writes coalesce. - DELETE on remove —
removeItem/removeItemscancel any pending PUT and callapi.delete(key)for every whitelisted key removed. resetstays local —store.reset()cancels pending PUTs and resets the base store; the backend keeps its copy, so a logout never removes the user's preferences from the server.- One-shot migration — on the first hydrate, any whitelisted local key that has no remote counterpart is
PUT to the backend, then a
__backendSync.migratedguard prevents it from running again. - Whitelist is glob-based — see
globMatch.
PREFERENCES_WHITELIST
The default whitelist used by SchemaAdmin covers:
export const PREFERENCES_WHITELIST: string[] = [
'theme',
'locale',
'preferences.dashboard',
'preferences.layout.app.*',
'preferences.**.columns',
'preferences.**.columnWidths',
'**.savedQueries',
];
Note the ** in the column entries: a column preference key carries the resource name, which itself
contains a dot (preferences.eshop/product.columns for a plain list, and a longer path for a list
box), so a single * would not reach it.
Anything not listed — *.listParams, *.availableColumns, *.omit, preferences.layout.filtersCollapsed,
derived datagrid metadata — stays local-only.
See also
- globMatch — how the whitelist patterns are matched
- Schema provider — the four optional methods that back
BackendApi - User preferences — the backend side