restate / v1.0.0
Save when you decide
Keep a store in IndexedDB or MMKV, load it once at startup, save explicitly or automatically, and show unsaved changes.
On this page
Declare a saved storeWait once at startupSave, revert and resetShow unsaved changes and progressTry itSave automaticallyBefore the page closesHandle storage errorsDeclare a saved store
Replace restate with persistedRestate and give it a key. Components keep using the same hook and set.
The saved value is restored automatically after creation. Editing changes memory; nothing is written until you save or turn on autoSave.
import { persistedRestate } from '@kkwaenggwari/restate';
export const useDraft = persistedRestate('', {
key: 'article-draft',
validate: (value) => typeof value === 'string',
});Wait once at startup
At the client entry, import the app and its stores, then wait for restateManager.ready before rendering. Components can then read saved values directly.
Storage that is unavailable, such as during server rendering or in a blocked browser, is not a failure: those stores work in memory. ready rejects only when records cannot be read, migrated or validated.
import { createRoot } from 'react-dom/client';
import { restateManager } from '@kkwaenggwari/restate';
import App from './App';
async function start() {
const root = createRoot(document.getElementById('root'));
try {
await restateManager.ready;
root.render(<App />);
} catch (error) {
root.render(<p>Could not load saved data.</p>);
console.error(error);
}
}
start();Save, revert and reset
save writes the current value, restore returns to the saved value, and resetAll clears both memory and storage. Each returns a Promise.
await useDraft.save(); // write the current value
await useDraft.restore(); // return to the saved value
await useDraft.resetAndClearStorage(); // initial value in memory, record removedShow unsaved changes and progress
useStatus re-renders when the persistence status changes. dirty is true while memory differs from the value last loaded, saved or restored. operation names the running storage operation.
Pass a selector to re-render only when one field changes.
function SaveButton() {
const { dirty, operation, error } = useDraft.useStatus();
const saving = operation === 'save';
return (
<>
<button disabled={!dirty || saving} onClick={() => useDraft.save().catch(() => {})}>
{saving ? 'Saving…' : 'Save'}
</button>
{error != null && <p>Could not save your draft.</p>}
</>
);
}Try it
Type, raise the storage latency and save. Replay the example to see the saved draft restored.
Preparing the example…
Save automatically
For settings and drafts that should always persist, opt in once. autoSave: true saves after each change; { delayMs } waits until changes pause.
A burst of changes becomes one write, and a change back to the saved value writes nothing. Failures appear in status().error. A component-owned scope writes a pending change when it unmounts.
export const useTheme = persistedRestate('light', { key: 'theme', autoSave: true });
export const useDraft = persistedRestate('', {
key: 'article-draft',
autoSave: { delayMs: 500 },
});Before the page closes
Read the same status outside React. Warn before leaving with unsaved changes, or save everything when the page is hidden.
window.addEventListener('beforeunload', (event) => {
if (useDraft.status().dirty) event.preventDefault();
});
window.addEventListener('pagehide', () => {
restateManager.saveAll();
});Handle storage errors
Storage errors reject the Promise of the operation. Every restate error has a code and, for saved stores, the key of the record.
try {
await useDraft.save();
} catch (error) {
if (error.code === 'STORAGE_UNAVAILABLE') showNotice('Saving is off in this browser.');
else if (error.code === 'STORAGE_VALUE_TOO_LARGE') showNotice('This draft is too large.');
else showNotice(`Could not save "${error.key ?? 'draft'}".`);
}