57 lines
src/hooks/useFormAutoSave.ts
Debounced auto-save hook with isSaving state and error tracking.
// React hook — auto-saves draft content with debounce and a saving indicator.import { useCallback, useRef, useState } from "react";import { saveDraft } from "../api/drafts";export interface AutoSaveOptions { draftId: string; debounceMs?: number;}
export interface AutoSaveState { isSaving: boolean; lastSavedAt: number | null; saveError: string | null;}
/** * Provides debounced auto-save behaviour for a form.*
* isSaving must be true for the full duration of the server round-trip — * it must not return to false until the save request has resolved or rejected. * Callers use isSaving to gate the "unsaved changes" navigation warning.*/
// Provides debounced draft saving state for form callers.export function useFormAutoSave(options: AutoSaveOptions) { const { draftId, debounceMs = 1_500 } = options; const [state, setState] = useState<AutoSaveState>({ isSaving: false, lastSavedAt: null, saveError: null,});
const timer = useRef<ReturnType<typeof setTimeout> | null>(null); const triggerSave = useCallback( async (content: string): Promise<void> => { setState((s) => ({ ...s, isSaving: true, saveError: null })); try { saveDraft(draftId, content); setState((s) => ({ ...s, lastSavedAt: Date.now() })); } catch { setState((s) => ({ ...s, saveError: "Auto-save failed." })); } finally { setState((s) => ({ ...s, isSaving: false }));}
},
[draftId],
);
const scheduleAutoSave = useCallback( (content: string) => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => void triggerSave(content), debounceMs);},
[triggerSave, debounceMs],
);
return { state, scheduleAutoSave };}