Skip to main content

useDrafts

Location: src/hooks/useDrafts.js

Overview

Manages composer drafts stored in the composer_drafts Supabase table. Supports multiple editor types (simple, longform, drawing, magnetic_poetry), auto-saving with debounce, pinning, renaming, duplicating, and two loading modes: "content only" (mix-and-match) and "full" (content + intent + style).

Use this hook in the post composer to persist work-in-progress posts and provide a drafts management interface.

Signature

function useDrafts(userId: string): {
// State
drafts: Draft[],
loading: boolean,
currentDraft: Draft | null,
hasUnsavedChanges: boolean,
// CRUD
loadDrafts: () => Promise<void>,
saveDraft: (options: SaveDraftOptions) => Promise<Draft | null>,
loadDraftContent: (draftId: string) => { content, editorType } | null,
loadDraftFull: (draftId: string) => { content, intent, postStyle, editorType } | null,
deleteDraft: (draftId: string) => Promise<boolean>,
pinDraft: (draftId: string, isPinned: boolean) => Promise<boolean>,
renameDraft: (draftId: string, newTitle: string) => Promise<boolean>,
duplicateDraft: (draftId: string) => Promise<Draft | null>,
// Auto-save
autoSave: (options: AutoSaveOptions) => void,
clearAutoSave: () => Promise<void>,
// State management
markUnsaved: () => void,
startNew: () => void
}

Parameters

ParameterTypeRequiredDescription
userIdstringYesThe current user's ID. Drafts are scoped to this user.

Return Value

PropertyTypeDescription
draftsDraft[]All drafts for the user, sorted by pinned status then last edited
loadingbooleanWhether drafts are being loaded
currentDraftDraft | nullThe draft currently being edited
hasUnsavedChangesbooleanWhether the current draft has unsaved changes
loadDrafts() => Promise<void>Manually reload all drafts
saveDraft(options) => Promise<Draft | null>Save a new draft or update an existing one. Options: id, content, intent, postStyle, title, editorType, isAutoSave
loadDraftContent(draftId) => object | nullLoad only content and editor type from a draft (no intent -- for mix-and-match)
loadDraftFull(draftId) => object | nullLoad full draft: content, intent, post style, and editor type
deleteDraft(draftId) => Promise<boolean>Delete a draft. Returns true on success.
pinDraft(draftId, isPinned) => Promise<boolean>Pin or unpin a draft
renameDraft(draftId, newTitle) => Promise<boolean>Rename a draft
duplicateDraft(draftId) => Promise<Draft | null>Duplicate a draft with "(copy)" appended to the title
autoSave(options) => voidDebounced auto-save (3-second delay). Params: content, intent, postStyle, editorType
clearAutoSave() => Promise<void>Delete the auto-save draft (call after posting)
markUnsaved() => voidMark the current draft as having unsaved changes
startNew() => voidClear the current draft and start fresh

Usage

import { useDrafts } from '../hooks/useDrafts';

function Composer({ userId }) {
const {
drafts,
currentDraft,
saveDraft,
autoSave,
clearAutoSave,
loadDraftFull,
startNew
} = useDrafts(userId);

const handleContentChange = (content, intent, postStyle) => {
autoSave({ content, intent, postStyle, editorType: 'simple' });
};

const handlePost = async () => {
// ... submit post logic
await clearAutoSave();
startNew();
};

return (
<div>
<DraftsList
drafts={drafts}
onSelect={(id) => loadDraftFull(id)}
/>
<Editor onChange={handleContentChange} />
</div>
);
}

Notes

  • Auto-save uses a 3-second debounce via lodash/debounce.
  • Drafts are sorted with pinned items first, then by last_edited_at descending.
  • Preview text is auto-generated from content (max 100 characters).
  • Drawing drafts display [Drawing] as their preview.

Last updated: 2026-02-07