Building & Publishing a Cross-Framework React Hook — 📝react-usedrafty

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 localStorage or sessionStorage.
  • 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 location changes.

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.ts files 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 /example from the package.

Step 5 — Local Testing

We:

  • Added an /example folder 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 (isDirty flag).

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!