useBeforeUnload conditionally asks the browser to confirm before leaving a page that contains unsaved work. You will manage a beforeunload listener whose lifetime follows a boolean flag. The calling component decides whether work is unsaved; the hook only manages the browser subscription. The browser owns the wording and decides whether a dialog appears.
function useBeforeUnload(shouldWarn?: boolean): void
function SavedEditor() {
useBeforeUnload(false);
// No beforeunload listener is attached.
return null;
}
function DirtyEditor({ isDirty }) {
useBeforeUnload(isDirty);
return null;
}
// false -> true: attach one listener
// true -> true: keep the existing listener
// true -> false: remove the exact listener that was attached
shouldWarn defaults to false.shouldWarn is true, and remove that exact listener when it becomes false or the component unmounts.event.preventDefault() and assign event.returnValue = true for legacy support.window is unavailable.visibilitychange, pagehide, unload, or forced dialogs.The hook makes a browser listener exist for exactly as long as the page has unsaved work.
A user edits a form, then closes the tab before saving. The browser can show its own confirmation dialog if your page cancels the beforeunload event. Your hook must opt into that behavior only while the form is dirty, then cleanly opt out after a save.
Treat shouldWarn as a switch controlling one subscription. React runs the effect when the switch changes: false owns nothing, while true owns one handler and a cleanup that removes the same function.
const { useEffect } = require('react');
function useBeforeUnload() {
useEffect(() => {
window.addEventListener('beforeunload', (event) => {
event.preventDefault();
event.returnValue = true;
});
});
}
This version adds another anonymous handler after every render. It cannot remove any of them because it never keeps the function references, and it warns even when there is no unsaved work. Reading window without a guard also makes the effect unsafe outside a browser.
const { useEffect } = require('react');
function useBeforeUnload(shouldWarn = false) {
useEffect(() => {
if (typeof window === 'undefined' || !shouldWarn) return;
const handleBeforeUnload = (event) => {
event.preventDefault();
// Older browser behavior still checks this legacy property.
event.returnValue = true;
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [shouldWarn]);
}
module.exports = { useBeforeUnload };
The early return means the false and non-browser paths own no side effect. Defining the handler inside the effect gives that effect run one stable reference for both addEventListener and removeEventListener. The dependency array prevents unchanged renders from creating extra subscriptions.
A profile form starts saved, so shouldWarn is false and the effect adds nothing. The user changes their display name, making it true; React runs the effect and attaches one handler. A beforeunload event then reaches that handler, which calls preventDefault() and sets returnValue. After the save finishes, shouldWarn becomes false; React first runs the previous cleanup, removing the exact handler, and the new effect run returns without attaching another.
beforeunload is unreliable, especially when a mobile browser is closed from the app manager; save through a separate lifecycle strategy.beforeunload listeners from its back/forward cache, so subscribe only while work is unsaved.removeEventListener needs the same handler reference that was added; return the cleanup from the same effect run.visibilitychange for a separate autosave path when the document becomes hidden; it solves a different problem from leave confirmation.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
useBeforeUnload conditionally asks the browser to confirm before leaving a page that contains unsaved work. You will manage a beforeunload listener whose lifetime follows a boolean flag. The calling component decides whether work is unsaved; the hook only manages the browser subscription. The browser owns the wording and decides whether a dialog appears.
function useBeforeUnload(shouldWarn?: boolean): void
function SavedEditor() {
useBeforeUnload(false);
// No beforeunload listener is attached.
return null;
}
function DirtyEditor({ isDirty }) {
useBeforeUnload(isDirty);
return null;
}
// false -> true: attach one listener
// true -> true: keep the existing listener
// true -> false: remove the exact listener that was attached
shouldWarn defaults to false.shouldWarn is true, and remove that exact listener when it becomes false or the component unmounts.event.preventDefault() and assign event.returnValue = true for legacy support.window is unavailable.visibilitychange, pagehide, unload, or forced dialogs.The hook makes a browser listener exist for exactly as long as the page has unsaved work.
A user edits a form, then closes the tab before saving. The browser can show its own confirmation dialog if your page cancels the beforeunload event. Your hook must opt into that behavior only while the form is dirty, then cleanly opt out after a save.
Treat shouldWarn as a switch controlling one subscription. React runs the effect when the switch changes: false owns nothing, while true owns one handler and a cleanup that removes the same function.
const { useEffect } = require('react');
function useBeforeUnload() {
useEffect(() => {
window.addEventListener('beforeunload', (event) => {
event.preventDefault();
event.returnValue = true;
});
});
}
This version adds another anonymous handler after every render. It cannot remove any of them because it never keeps the function references, and it warns even when there is no unsaved work. Reading window without a guard also makes the effect unsafe outside a browser.
const { useEffect } = require('react');
function useBeforeUnload(shouldWarn = false) {
useEffect(() => {
if (typeof window === 'undefined' || !shouldWarn) return;
const handleBeforeUnload = (event) => {
event.preventDefault();
// Older browser behavior still checks this legacy property.
event.returnValue = true;
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [shouldWarn]);
}
module.exports = { useBeforeUnload };
The early return means the false and non-browser paths own no side effect. Defining the handler inside the effect gives that effect run one stable reference for both addEventListener and removeEventListener. The dependency array prevents unchanged renders from creating extra subscriptions.
A profile form starts saved, so shouldWarn is false and the effect adds nothing. The user changes their display name, making it true; React runs the effect and attaches one handler. A beforeunload event then reaches that handler, which calls preventDefault() and sets returnValue. After the save finishes, shouldWarn becomes false; React first runs the previous cleanup, removing the exact handler, and the new effect run returns without attaching another.
beforeunload is unreliable, especially when a mobile browser is closed from the app manager; save through a separate lifecycle strategy.beforeunload listeners from its back/forward cache, so subscribe only while work is unsaved.removeEventListener needs the same handler reference that was added; return the cleanup from the same effect run.visibilitychange for a separate autosave path when the document becomes hidden; it solves a different problem from leave confirmation.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.