61 lines
1.9 KiB
JavaScript
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 };
|
||
|
|
}
|