Prevent accidental form data loss and enhance user experience — one small, powerful hook at a time.
Introduction
In this article, we’ll walk through the journey of creating, enhancing, testing, and publishing a universally compatible React hook for saving form drafts in the browser: react-usedrafty.
Our goal was to:
- Build a plug-and-play solution for auto-saving and restoring form state.
- Make it framework-agnostic, working with React, Next.js, and React Router projects.
- Keep it lightweight with zero dependencies.
- Add real-world features like route-change warnings and customizable leave prompts.
Step 1 — Hook Requirements
From the initial idea, these requirements were set:
- Auto-save form data to
localStorageorsessionStorage. - Restore data on page reload.
- Warn the user before leaving if there are unsaved changes.
- Support Next.js and React Router route change detection.
- Provide clean API and TypeScript types.
Step 2 — Core Hook Implementation
We built the useDrafty hook in TypeScript, then configured the build to export both ESM and CJS formats for maximum compatibility.
Key features in the core:
- Storage type selection (
local/session). - Debounce saving.
- Restore on mount.
- Dirty-state detection.
- Configurable warnOnLeave with custom messages.
- Route change prevention via injected router instance.
Example API usage:
tsxCopyEdituseDrafty("contact-form", formState, setFormState, {
storage: "local",
delay: 1000,
warnOnUnload: true,
unloadMessage: "You have unsaved changes!",
router: nextRouterOrReactRouter,
onRestore: (data) => console.log("Draft restored:", data)
});
Step 3 — Adding Router Awareness
We avoided hard dependencies on Next.js or React Router by letting the user pass their router object.
Internally:
- For Next.js, we hook into
router.events.on("routeChangeStart", cb). - For React Router, we watch
locationchanges.
This way:
- No extra packages are required.
- The hook works without any router if that feature is not needed.
Step 4 — Packaging for the World
We ensured compatibility by:
- Targeting ESNext but compiling to both ESM & CJS.
- Generating
.d.tsfiles for TypeScript users. - Writing a package.json with proper
"exports"mapping.
Example "exports":
jsonCopyEdit"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
We also configured:
npm run build→ Generates ESM, CJS, DTS..npmignore→ Excludes/examplefrom the package.
Step 5 — Local Testing
We:
- Added an
/examplefolder using Vite & React for quick testing. - Made sure the package works by linking locally via:
bashCopyEditnpm link
npm link react-usedrafty
- Verified in plain React, Next.js, and React Router apps.
Step 6 — Publishing to npm
Version management steps:
bashCopyEdit# Update version
npm version patch # or minor/major
# Publish
npm publish --access public
💡 Mistake: If you bump the wrong version, you can revert with:
bashCopyEditnpm version 1.2.2 --no-git-tag-version
Step 7 — Documentation
We created a detailed README including:
- Feature list.
- Install instructions.
- Basic & advanced usage.
- API reference.
- Changelog with new features (e.g.,
warnOnUnload, router support).
Step 8 — What’s New in the Latest Version
From this latest dev cycle, we added:
- Router-aware leave warnings (Next.js + React Router).
- Custom restore callback
onRestore. - Universal build that works across major JS environments.
- Debounce control for save frequency.
- Clear API for dirty-state detection (
isDirtyflag).
Conclusion
By following this process, we built a hook that:
- Works in React, Next.js, CRA, and other environments.
- Handles both browser unload and in-app route changes.
- Ships with full TypeScript types.
- Has local examples for quick testing.
- Is ready for npm publication with a clean API and docs.
This approach can be applied to any React utility you want to share with the community while keeping it framework-flexible.
Made with ❤️& Fire in heart
PRs and feedback welcome!