BillTracker/client/hooks/useAutoSave.js

61 lines
1.9 KiB
JavaScript

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