Skip to main content

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:

  1. Debounced save -- 2 seconds after the last change (via scheduleSave)
  2. Periodic save -- every 30 seconds while content exists
  3. 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

ParameterTypeRequiredDescription
options{ enabled?: boolean }NoConfiguration object. enabled defaults to true. Set to false to disable auto-save entirely.

Return Value

PropertyTypeDescription
hasDraftbooleantrue if there is a saved draft available for restoration.
savedDraftDraftData | nullThe saved draft object, or null if no draft exists. Contains content, selectedIntent, audience, selectedCircleIds, postStyle, hasImages, hasVoice, and savedAt.
lastSavedDate | nullTimestamp of the most recent successful save in this session.
isSavingbooleantrue while a save operation is in progress.
draftCheckedbooleantrue 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) => voidSchedule 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 | nullReturn 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