useAutoSave
Location: src/hooks/useAutoSave.js
Overview
Auto-saves post composer drafts to IndexedDB, ensuring that work-in-progress posts survive browser refreshes and tab closures. Drafts are saved through three mechanisms:
- Debounced save -- 2 seconds after the last change (via
scheduleSave) - Periodic save -- every 30 seconds while content exists
- Blur/visibility save -- when the user switches tabs or leaves the page
On mount, the hook checks for an existing draft and exposes it for restoration. After a successful post, call clear() to remove the draft from storage.
Note: Binary data (images, voice recordings) cannot be stored in IndexedDB through this hook. The draft stores flags (hasImages, hasVoice) indicating that attachments existed.
Signature
function useAutoSave(
options?: { enabled?: boolean }
): {
// State
hasDraft: boolean,
savedDraft: DraftData | null,
lastSaved: Date | null,
isSaving: boolean,
draftChecked: boolean,
// Actions
save: (draft: DraftInput) => Promise<boolean>,
scheduleSave: (draft: DraftInput) => void,
clear: () => Promise<void>,
dismissDraft: () => Promise<void>,
restoreDraft: () => DraftData | null
}
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
options | { enabled?: boolean } | No | Configuration object. enabled defaults to true. Set to false to disable auto-save entirely. |
Return Value
| Property | Type | Description |
|---|---|---|
hasDraft | boolean | true if there is a saved draft available for restoration. |
savedDraft | DraftData | null | The saved draft object, or null if no draft exists. Contains content, selectedIntent, audience, selectedCircleIds, postStyle, hasImages, hasVoice, and savedAt. |
lastSaved | Date | null | Timestamp of the most recent successful save in this session. |
isSaving | boolean | true while a save operation is in progress. |
draftChecked | boolean | true once the initial draft check has completed on mount. Useful to prevent flicker before knowing if a draft exists. |
save | (draft) => Promise<boolean> | Immediately save a draft to IndexedDB. Returns true on success. Ignores empty drafts. |
scheduleSave | (draft) => void | Schedule a debounced save (2-second delay). Call this on every content change. |
clear | () => Promise<void> | Clear the saved draft from IndexedDB and reset all state. Call after a successful post. |
dismissDraft | () => Promise<void> | Dismiss the draft restoration prompt and clear the saved draft. Alias for clear. |
restoreDraft | () => DraftData | null | Return the saved draft and dismiss the "has draft" prompt. Does not clear from IndexedDB (the draft remains saved). |
DraftInput
The save and scheduleSave functions accept a draft object with these fields:
interface DraftInput {
content?: string,
selectedIntent?: string | null,
audience?: string,
selectedCircleIds?: string[],
postStyle?: object | null,
images?: any[], // presence is stored as hasImages boolean
voiceData?: any // presence is stored as hasVoice boolean
}
Usage
function PostComposer() {
const { hasDraft, savedDraft, draftChecked, scheduleSave, clear, restoreDraft } = useAutoSave();
const [content, setContent] = useState('');
// Restore draft prompt
useEffect(() => {
if (draftChecked && hasDraft) {
// Show restoration UI
}
}, [draftChecked, hasDraft]);
const handleRestore = () => {
const draft = restoreDraft();
if (draft) {
setContent(draft.content || '');
}
};
const handleChange = (newContent) => {
setContent(newContent);
scheduleSave({ content: newContent });
};
const handleSubmit = async () => {
// ... submit post ...
await clear(); // Remove draft after successful post
};
return (
<div>
{hasDraft && (
<div className="draft-banner">
<span>You have an unsaved draft.</span>
<button onClick={handleRestore}>Restore</button>
<button onClick={clear}>Discard</button>
</div>
)}
<textarea value={content} onChange={e => handleChange(e.target.value)} />
<button onClick={handleSubmit}>Post</button>
</div>
);
}
Last updated: 2026-02-07