import { useCallback, useEffect, useRef, useState } from 'react'; /** * 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, defaultDelay = 400) { const [status, setStatus] = useState('idle'); const timer = useRef(null); const pending = useRef(null); const saveRef = useRef(saveFn); saveRef.current = saveFn; const run = useCallback(async (payload) => { 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, 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 }; }