Offline persistence
A fully offline editor — no server, no internet required. Documents auto-save to IndexedDB (a native browser API, no added dependency — the whole editor stays zero-runtime-dependency) and reload themselves on the next visit. This is what powers this site's own My Notes app.
Quick start: usePersistedDocument
import { useEditor, NoteloomEditor, usePersistedDocument } from 'noteloom';
function App() {
const editor = useEditor({ doc: myStarterDoc });
const { isLoaded } = usePersistedDocument({ store: editor.store, docId: 'my-document-id' });
if (!isLoaded) return <p>Loading…</p>;
return <NoteloomEditor editor={editor} />;
}On mount, this loads whatever was last saved under docId (if anything) and replaces the store's content with it; every edit after that — typing, structural changes, even changes arriving from a collaborating peer via CollabSession — is auto-saved back, debounced (default 500ms of quiet) so a full-document write doesn't fire on every keystroke. Different docIds are stored independently, so one browser can hold many separate documents (e.g. keyed by page/route).
Options & return value
| Option | Type | Required | What it does |
|---|---|---|---|
store | EditorStore | History | Yes | The store to load into / persist from — must already exist (created via useEditor() or your own useMemo). This hook does not construct one. |
docId | string | Yes | Which saved document to load/save. Changing it re-runs the load for the new id. |
debounceMs | number | No — default 500 | How long to wait after the last change before writing to IndexedDB. |
onError | (err) => void | No | Called if a load or save fails (e.g. IndexedDB unavailable in a private-browsing context). |
Returns { isLoaded } — false until the initial IndexedDB read resolves, so you can show a loading state instead of briefly flashing default content that's about to be replaced.
Lower-level pieces
If usePersistedDocument's all-in-one behavior doesn't fit — a non-React host app, custom load/save timing, wanting to list/delete saved documents from a "My Notes"-style index page — the raw operations it's built on are all exported individually:
import {
savePersistedDocument,
loadPersistedDocument,
deletePersistedDocument,
listPersistedDocumentIds,
} from 'noteloom';
await savePersistedDocument('my-document-id', editor.store.toJSON());
const doc = await loadPersistedDocument('my-document-id'); // null if nothing saved yet
await deletePersistedDocument('my-document-id');
const ids = await listPersistedDocumentIds(); // every docId currently stored| Function | Signature | What it does |
|---|---|---|
savePersistedDocument | (docId, doc) => Promise<void> | Writes a full document JSON (the same shape store.toJSON() returns) under docId. |
loadPersistedDocument | (docId) => Promise<doc | null> | Reads it back — null if nothing was ever saved under that id. |
deletePersistedDocument | (docId) => Promise<void> | Removes a saved document entirely (e.g. a "Delete note" action). |
listPersistedDocumentIds | () => Promise<string[]> | Every docId currently stored — enough to render a "My Notes" list page without keeping your own separate index. |
Just the debounced auto-save half, if you want to handle the initial load yourself:
import { createAutoPersistence } from 'noteloom';
// You load the initial document yourself (e.g. already have it from an API);
// this just wires the debounced auto-save half.
const { stop, flush } = createAutoPersistence({
store: editor.store,
docId: 'my-document-id',
debounceMs: 500, // default, shown explicitly
onError: (err) => console.error('Failed to save', err),
});
// later, when the store is no longer in use:
flush(); // persist any pending change before tearing down
stop();createAutoPersistence fires on every store mutation, local or remote — it doesn't distinguish, so a document being live-collaborated on stays persisted the same as one edited solo. stop() unsubscribes and cancels any pending debounced save, but does not flush a pending save first — call flush() beforehand if the most recent edit must be persisted before tearing down (e.g. navigating away).
CollabSession (see Live collaboration) — a collaborated-on document also gets saved locally, so it survives even after every peer disconnects. This only makes the editing work offline; if the app itself is loaded from a dev server or web host, opening it for the very first time (or after clearing cache) still needs that host reachable once — the next section covers that separate concern.Offline app shell (PWA)
usePersistedDocument makes the document offline-capable; it doesn't make the app itself loadable with no network at all — that needs a service worker precaching the HTML/JS/CSS, which is a build-level concern (the exact list of files to cache is whatever your bundler outputs), not something a runtime library can inject. noteloom doesn't ship a service worker implementation for that reason. Instead:
- Use a standard PWA build plugin —
vite-plugin-pwais the common choice for Vite, and requires no noteloom-specific configuration. useServiceWorkerUpdate()(exported from the package) is the one genuinely reusable piece:
import { useServiceWorkerUpdate } from 'noteloom';
function UpdateBanner() {
const { updateAvailable, applyUpdate } = useServiceWorkerUpdate();
if (!updateAvailable) return null;
return <button onClick={applyUpdate}>Update available — reload</button>;
}It watches for a newly-installed service worker sitting in the "waiting" state (the standard signal a fresh build is ready) and gives you { updateAvailable, applyUpdate } to surface and act on it. Works with any service worker registration, however it got there — it only observes, it doesn't register one itself.
vite preview), load it once online, then disconnect entirely and reload — the app shell still loads, and editing/persistence both keep working, since IndexedDB has no network dependency of its own.localStorage-backed store, predating usePersistedDocument), nothing ever sent to a server. For a new app, usePersistedDocument above is the more direct path to the same result.