SkateSpots/src/hooks/useFormDraft.ts

67 lines
2 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
/**
* Autosave a long form so it isn't lost on navigation or reload.
*
* - Persists `snapshot` to localStorage under `<prefix><key>` (content-debounced:
* the serialized string only changes when the draft actually changes).
* - A saved draft is only offered as **resumable** when it *differs* from the
* current form state — so merely opening an existing record (whose prefill
* matches the saved draft) never shows a spurious "resume" banner, and a
* draft can never silently clobber a freshly loaded record.
* - Call `clear()` after a successful publish.
*
* Used by the Lab profile forms (`useLabDraft`) and the create-spot form.
*/
export function useFormDraft<T>(
prefix: string,
key: string,
snapshot: T,
apply: (draft: T) => void,
) {
const storageKey = prefix + key;
const [loaded, setLoaded] = useState<T | null>(null);
const firstRun = useRef(true);
const serialized = JSON.stringify(snapshot);
// Load any saved draft once.
useEffect(() => {
try {
const raw = localStorage.getItem(storageKey);
if (raw) setLoaded(JSON.parse(raw) as T);
} catch {
// ignore malformed/blocked storage
}
}, [storageKey]);
// Persist on change (skip the initial empty paint).
useEffect(() => {
if (firstRun.current) {
firstRun.current = false;
return;
}
try {
localStorage.setItem(storageKey, serialized);
} catch {
// storage may be full/blocked — a lost draft is not fatal
}
}, [serialized, storageKey]);
const resume = () => {
if (!loaded) return;
apply(loaded);
setLoaded(null);
};
const discard = () => {
setLoaded(null);
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
};
const clear = () => {
try { localStorage.removeItem(storageKey); } catch { /* ignore */ }
};
const resumable = loaded !== null && JSON.stringify(loaded) !== serialized;
return { resumable, resume, discard, clear };
}