UNPKG

react-usedrafty

Version:

📝 A React hook to auto-save and restore form state using localStorage or sessionStorage.

1 lines 7.74 kB
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nexport interface UseDraftyOptions<T> {\n useSession?: boolean; // Use sessionStorage instead of localStorage\n debounce?: number; // Debounce time in ms\n warnOnLeave?: boolean | (() => string | boolean); // Warning message or function\n onRestore?: (draft: T) => void; // Callback when draft is restored\n router?: any; // Router object (Next.js, React Router, etc.)\n}\n\nexport interface UseDraftyReturn {\n saveDraft: () => void;\n clearDraft: (opts?: { submitted?: boolean }) => void;\n hasDraft: boolean;\n isDirty: boolean;\n}\n\nfunction useDrafty<T extends Record<string, any>>(\n storageKey: string,\n currentFormState: T,\n updateFormState: (state: T) => void,\n options?: UseDraftyOptions<T>\n): UseDraftyReturn {\n const {\n useSession = false,\n debounce = 300,\n warnOnLeave = false,\n onRestore,\n router,\n } = options || {};\n\n const storage: Storage = useSession ? sessionStorage : localStorage;\n const submittedFlagKey = `submitted:${storageKey}`;\n const debounceTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isRestored = useRef(false);\n\n const [hasDraft, setHasDraft] = useState(false);\n const [initialDraft, setInitialDraft] = useState<T | null>(null);\n\n /** Restore saved draft unless marked submitted */\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n\n try {\n const wasSubmitted = storage.getItem(submittedFlagKey) === \"true\";\n if (wasSubmitted) {\n console.info(`[useDrafty] Skipping restore for \"${storageKey}\" (submitted)`);\n return;\n }\n\n const saved = storage.getItem(storageKey);\n if (saved) {\n const parsed: T = JSON.parse(saved);\n updateFormState(parsed);\n setInitialDraft(parsed);\n setHasDraft(true);\n onRestore?.(parsed);\n console.info(`[useDrafty] Draft restored for \"${storageKey}\"`);\n }\n } catch (e) {\n console.warn(\"[useDrafty] Failed to load saved draft:\", e);\n } finally {\n isRestored.current = true;\n }\n }, [storageKey]);\n\n /** Auto-save with debounce after restore */\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n if (!isRestored.current) return;\n\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n debounceTimer.current = setTimeout(() => {\n saveDraft();\n }, debounce);\n\n return () => {\n if (debounceTimer.current) clearTimeout(debounceTimer.current);\n };\n }, [currentFormState, debounce]);\n\n /** Warn on browser/tab close */\n useEffect(() => {\n if (!warnOnLeave) return;\n const getMessage = () => {\n const res = typeof warnOnLeave === \"function\" ? warnOnLeave() : warnOnLeave;\n return typeof res === \"string\" ? res : \"You have unsaved changes.\";\n };\n const beforeUnloadHandler = (e: BeforeUnloadEvent) => {\n const message = getMessage();\n e.preventDefault();\n e.returnValue = message;\n return message;\n };\n window.addEventListener(\"beforeunload\", beforeUnloadHandler);\n return () => window.removeEventListener(\"beforeunload\", beforeUnloadHandler);\n }, [warnOnLeave]);\n\n /** Auto-clear on SPA navigation if router provided */\n useEffect(() => {\n if (!router) return;\n const clearOnNavigation = () => {\n clearDraft();\n console.info(`[useDrafty] Draft cleared for \"${storageKey}\" on navigation`);\n };\n\n // Next.js Pages Router\n if (router.events?.on) {\n router.events.on(\"routeChangeStart\", clearOnNavigation);\n return () => router.events.off(\"routeChangeStart\", clearOnNavigation);\n }\n\n // React Router\n if (router.block) {\n const unblock = router.block(() => {\n clearOnNavigation();\n return true;\n });\n return unblock;\n }\n }, [router]);\n\n /** Save draft */\n const saveDraft = () => {\n try {\n storage.setItem(storageKey, JSON.stringify(currentFormState));\n setHasDraft(true);\n storage.removeItem(submittedFlagKey); // If editing again, remove submit flag\n } catch (e) {\n console.warn(\"[useDrafty] Failed to save draft:\", e);\n }\n };\n\n /** Clear draft (optionally mark submitted) */\n const clearDraft = (opts?: { submitted?: boolean }) => {\n try {\n storage.removeItem(storageKey);\n if (opts?.submitted) {\n storage.setItem(submittedFlagKey, \"true\");\n } else {\n storage.removeItem(submittedFlagKey);\n }\n setHasDraft(false);\n setInitialDraft(null);\n } catch (e) {\n console.warn(\"[useDrafty] Failed to clear draft:\", e);\n }\n };\n\n /** Dirty check */\n const isDirty = JSON.stringify(initialDraft) !== JSON.stringify(currentFormState);\n\n return { saveDraft, clearDraft, hasDraft, isDirty };\n}\n\nexport default useDrafty;\nexport { useDrafty };\n"],"mappings":"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","names":["useEffect","useRef","useState","useDrafty","storageKey","currentFormState","updateFormState","options","useSession","debounce","warnOnLeave","onRestore","router","storage","submittedFlagKey","debounceTimer","isRestored","hasDraft","setHasDraft","initialDraft","setInitialDraft","saved","parsed","saveDraft","getMessage","res","beforeUnloadHandler","e","message","_a","clearOnNavigation","clearDraft","opts","isDirty","index_default"]}