withFormUnsavedWarning
extensionInstallation
npx jsrepo add github/reatom/reusables withFormUnsavedWarning Copy the source code below and save it to the specified file path in your project.
import { action, onEvent, type Action, type Form } from '@reatom/core'
interface FormUnsavedWarningExt {
preventNavigation: Action<[BeforeUnloadEvent], void>
waitUnsavedWarning: Action<[], void>
}
/**
* Form extension that warns the user before leaving the page when the form has
* unsaved changes, using the browser's `beforeunload` event.
*
* Returns a `waitUnsavedWarning` action that should be called in
* `reatomFactoryComponent` or a route loader to register the listener.
*
* Also exposes `preventNavigation` for SPA router integration via
* `withCallHook`.
*/
export const withFormUnsavedWarning =
<Target extends Form<any>>(
checkUnsaved: (form: Target) => boolean = (form) => form.focus().dirty,
) =>
(target: Target): FormUnsavedWarningExt => {
const preventNavigation = action((event: BeforeUnloadEvent) => {
if (checkUnsaved(target)) {
event.preventDefault()
}
}, `${target.name}.preventNavigation`)
const waitUnsavedWarning = action(() => {
if (typeof window !== 'undefined') {
onEvent(window, 'beforeunload', preventNavigation)
}
}, `${target.name}.waitUnsavedWarning`)
return { preventNavigation, waitUnsavedWarning }
} 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:
preventNavigationaction that callsevent.preventDefault()whencheckUnsaved(form)returnstruewaitUnsavedWarningaction that registers thebeforeunloadlistener viaonEvent. Call inreatomFactoryComponentor a route loader.- SSR-safe:
waitUnsavedWarningskips the listener whenwindowis not available
Parameters
checkUnsaved(optional):(form: Form) => boolean— predicate called onbeforeunloadto decide whether to warn. Defaults toform => form.focus().dirty.
Returns
Extension object with:
preventNavigation:Action<[BeforeUnloadEvent], void>— action that prevents navigation when the form has unsaved changes. Can be extended withwithCallHookto add custom side effects (e.g. SPA router navigation guards).waitUnsavedWarning:Action<[], void>— registers thebeforeunloadlistener. Call inreatomFactoryComponentor 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')
}),
)