import { useCallback, useEffect, useRef, useState } from 'react'; export type AutoSaveStatus = 'idle' | 'saving' | 'saved' | 'error'; type SaveFn = (payload: T) => unknown; export interface UseAutoSave { status: AutoSaveStatus; schedule: (payload: T, delay?: number) => void; flush: () => void; } /** * Debounced auto-save with a status the UI can render. * * const { status, schedule, flush } = useAutoSave(payload => api.save(payload)); * - schedule(payload[, delayMs]) — (re)arms the debounce with the latest payload * - flush() — saves any pending payload immediately (e.g. on blur) * - status — 'idle' | 'saving' | 'saved' | 'error' ('saved' fades to 'idle') * * Pending changes are flushed on unmount so navigating away never loses edits. */ export function useAutoSave(saveFn: SaveFn, defaultDelay = 400): UseAutoSave { const [status, setStatus] = useState('idle'); const timer = useRef | undefined>(undefined); const pending = useRef(null); const saveRef = useRef>(saveFn); saveRef.current = saveFn; const run = useCallback(async (payload: T) => { pending.current = null; setStatus('saving'); try { await saveRef.current(payload); setStatus('saved'); } catch { setStatus('error'); // saveFn is responsible for surfacing the error (toast) } }, []); const schedule = useCallback((payload: T, delay = defaultDelay) => { pending.current = payload; clearTimeout(timer.current); timer.current = setTimeout(() => run(payload), delay); }, [run, defaultDelay]); const flush = useCallback(() => { if (pending.current != null) { clearTimeout(timer.current); run(pending.current); } }, [run]); // Fade the "Saved" confirmation back to idle. useEffect(() => { if (status !== 'saved') return undefined; const t = setTimeout(() => setStatus('idle'), 2000); return () => clearTimeout(t); }, [status]); // Never lose a pending edit on unmount. useEffect(() => () => { clearTimeout(timer.current); if (pending.current != null) { Promise.resolve(saveRef.current(pending.current)).catch(() => {}); } }, []); return { status, schedule, flush }; }