@oratis/use-draft-autosave
v1.0.0
Published
Debounced localStorage draft-autosave hook for React. SSR-safe, zero dependencies, configurable storage backend and key prefix.
Maintainers
Readme
use-draft-autosave
Never lose a half-written form again. A tiny (~0.5 kB), dependency-free
React hook that debounces the current form value into localStorage and gives
you back what you saved — so a refresh, an accidental tab close, or a crash
doesn't wipe the user's work.
- 🪶 Zero dependencies, ~0.5 kB min+gzip
- ⏱️ Debounced writes (configurable delay)
- 🖥️ SSR-safe — no
windowaccess during render, exposes ahydratedflag - 🔌 Pluggable storage —
localStorage,sessionStorage, or your own - 🧩 Fully typed generic value, ships ESM + CJS +
.d.ts
Install
npm install @oratis/use-draft-autosave
# or: pnpm add / yarn add / bun addreact >= 16.8 is a peer dependency.
Usage
The hook is a pure sink: you own the value, and it persists whatever you
pass. Hydrate your initial state from loadDraft, then feed state back in.
import { useState } from "react";
import { useDraftAutosave, loadDraft, clearDraft } from "@oratis/use-draft-autosave";
function CommentBox() {
// 1. Seed state from a previously saved draft (runs once).
const [text, setText] = useState(() => loadDraft<string>("comment") ?? "");
// 2. Autosave on every change, debounced.
const { hydrated, lastSavedAt, clear } = useDraftAutosave("comment", text);
async function submit() {
await postComment(text);
clear(); // drop the draft once it's safely submitted
setText("");
}
return (
<div>
<textarea value={text} onChange={(e) => setText(e.target.value)} />
{hydrated && lastSavedAt && (
<small>Draft saved {new Date(lastSavedAt).toLocaleTimeString()}</small>
)}
<button onClick={submit}>Post</button>
</div>
);
}Works with objects too — the value is JSON-serialized:
const [form, setForm] = useState(
() => loadDraft<Profile>("profile") ?? { name: "", bio: "" }
);
useDraftAutosave("profile", form, { delay: 500 });API
useDraftAutosave(key, value, options?)
Debounced autosave of value under ${prefix}${key}.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| delay | number | 1000 | Debounce delay in ms before a write. |
| skipInitial | boolean | true | Don't re-save the value you just hydrated from a draft. |
| prefix | string | "draft-" | Key prefix for every storage entry. |
| storage | () => Storage \| null | window.localStorage | Storage backend resolver. Return null to no-op. |
Returns:
| Field | Type | Description |
| --- | --- | --- |
| hydrated | boolean | true once mounted on the client. Gate storage-derived UI on this to avoid hydration mismatches. |
| lastSavedAt | number \| null | Epoch ms of the last successful write. |
| clear() | () => void | Remove the draft and reset lastSavedAt. |
loadDraft<T>(key, options?) → T | null
Read a saved draft. Returns null when missing, corrupt, or during SSR.
Accepts prefix / storage in options.
clearDraft(key, options?)
Remove a saved draft. No-op when storage is unavailable.
Why a sink, not a source?
Hooks that both own and persist state tend to fight your component over who
holds the truth (initial values, resets, controlled inputs). useDraftAutosave
sidesteps that: you own the state, hydrate it from loadDraft once, and the
hook only ever writes. Predictable, and it composes with any state library.
Next.js / SSR
Reading storage during render causes hydration mismatches. Seed with
useState(() => loadDraft(...)) (the initializer runs on the client for
client components) and gate any "saved at" UI behind the returned hydrated
flag, as shown above.
