← All reusables

withFormUnsavedWarning

extension

Installation

npx jsrepo add github/reatom/reusables withFormUnsavedWarning

Documentation

withFormUnsavedWarning

Warns the user before leaving the page when the form has unsaved changes, using the browser's beforeunload event.

The problem

When a user edits a form and accidentally closes the tab or navigates away, their unsaved changes are lost with no warning. Managing beforeunload listeners manually is error-prone — you need to wire them up in sync with the form lifecycle.

withFormUnsavedWarning adds a beforeunload listener that checks the form's dirty state and prevents the default browser action when unsaved changes are present.

withFormUnsavedWarning(checkUnsaved?)

Creates a form extension that returns:

  • preventNavigation action that calls event.preventDefault() when checkUnsaved(form) returns true
  • waitUnsavedWarning action that registers the beforeunload listener via onEvent. Call in reatomFactoryComponent or a route loader.
  • SSR-safe: waitUnsavedWarning skips the listener when window is not available

Parameters

  • checkUnsaved (optional): (form: Form) => boolean — predicate called on beforeunload to decide whether to warn. Defaults to form => form.focus().dirty.

Returns

Extension object with:

  • preventNavigation: Action<[BeforeUnloadEvent], void> — action that prevents navigation when the form has unsaved changes. Can be extended with withCallHook to add custom side effects (e.g. SPA router navigation guards).
  • waitUnsavedWarning: Action<[], void> — registers the beforeunload listener. Call in reatomFactoryComponent or a route loader.

Example

import { reatomForm } from '@reatom/core'
import { withFormUnsavedWarning } from '#reatom/extension/with-form-unsaved-warning'

const settingsForm = reatomForm(
  { name: '', email: '' },
  {
    onSubmit: async (state) => {
      await api.saveSettings(state)
    },
  },
).extend(withFormUnsavedWarning())

// in reatomFactoryComponent or route loader:
settingsForm.waitUnsavedWarning()

// With custom check:
const profileForm = reatomForm(
  { bio: '' },
  { onSubmit: async (state) => await api.saveProfile(state) },
).extend(withFormUnsavedWarning((form) => form.fields.bio.focus().dirty))

// in reatomFactoryComponent or route loader:
profileForm.waitUnsavedWarning()

Hooking into navigation prevention

Use withCallHook on the exposed preventNavigation action to run custom logic (e.g. block SPA router navigation):

import { reatomForm, withCallHook } from '@reatom/core'
import { withFormUnsavedWarning } from '#reatom/extension/with-form-unsaved-warning'

const form = reatomForm(
  { name: '' },
  { onSubmit: async (state) => await api.save(state) },
).extend(withFormUnsavedWarning())

form.preventNavigation.extend(
  withCallHook(() => {
    // e.g. router.block(() => 'You have unsaved changes')
  }),
)

Example

import { reatomForm, withCallHook } from '@reatom/core'

import { withFormUnsavedWarning } from './with-form-unsaved-warning'

// Basic: browser "Leave site?" dialog when form is dirty
export const settingsForm = reatomForm(
  { name: '', email: '' },
  {
    onSubmit: async (state) => {
      console.log('Saved', state)
    },
  },
).extend(withFormUnsavedWarning())

// Call in reatomFactoryComponent or a route loader:
settingsForm.waitUnsavedWarning()

// With custom predicate: only warn for specific fields
export const profileForm = reatomForm(
  { bio: '', avatar: '' },
  {
    onSubmit: async (state) => {
      console.log('Saved', state)
    },
  },
).extend(withFormUnsavedWarning((form) => form.fields.bio.focus().dirty))

// Call in reatomFactoryComponent or a route loader:
profileForm.waitUnsavedWarning()

// Hook into preventNavigation for SPA router integration
profileForm.preventNavigation.extend(
  withCallHook(() => {
    console.log('Navigation prevented — form has unsaved changes')
    // e.g. router.block(() => 'You have unsaved changes')
  }),
)