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 };
}