Live demo: cancel navigation on dirty edits (navigation guards)
Edit the text, then try to leave. The links above, the useNavigate button and the browser's own back/forward buttons all stop at the same confirm; save or discard, and none of them do.
Other page
Getting here while the editor was dirty took a confirmation - whether by link, by useNavigate, or by the browser's back button.
CancelNavigationApp.tsx
import { atom } from "jotai";
import { rootRouteAtom, navigationGuardAtom, staticRouteAtom } from "jarl-atoms";
import { Link, Route, Switch, useAtom, useAtomValue, useNavigate, useNavigationGuard, useSetAtom } from "jarl-react";
// The page this demo is mounted on, so its whole tree below is plain module-level atoms.
const cancelNavigationRoot = rootRouteAtom({ basePath: "/demos/cancel-navigation" });
const otherRoute = staticRouteAtom("other", { parent: cancelNavigationRoot });
const SAVED = "Dear reviewer,";
// Editor state lives in module-level atoms, not component state: the guard is an atom too, and
// reads the same dirty flag whichever page of the demo is on screen.
const savedAtom = atom(SAVED);
const draftAtom = atom(SAVED);
const dirtyAtom = atom((get) => get(draftAtom) !== get(savedAtom));
const saveAtom = atom(null, (get, set) => set(savedAtom, get(draftAtom)));
const discardAtom = atom(null, (get, set) => set(draftAtom, get(savedAtom)));
/**
* The guard itself: a plain derived atom returning the confirm message while the draft differs
* from what was saved, and `null` once it doesn't. `useNavigationGuard` below is what makes it
* bite - nothing else in this demo knows the guard exists.
*/
const unsavedEditsGuard = navigationGuardAtom((get) =>
get(dirtyAtom) ? "You have unsaved edits. Leave anyway?" : null,
);
const Editor = () => {
const [draft, setDraft] = useAtom(draftAtom);
const dirty = useAtomValue(dirtyAtom);
const save = useSetAtom(saveAtom);
const discard = useSetAtom(discardAtom);
const goOther = useNavigate(otherRoute);
return (
<div>
<h3>Editor</h3>
<p>
<textarea rows={3} cols={40} value={draft} onChange={(event) => setDraft(event.target.value)} />
</p>
<p>
<button type="button" disabled={!dirty} onClick={() => save()}>
Save
</button>{" "}
<button type="button" disabled={!dirty} onClick={() => discard()}>
Discard
</button>{" "}
<button type="button" onClick={() => goOther({})}>
Leave via useNavigate
</button>
</p>
<p>{dirty ? "Unsaved edits - every route change is confirmed first." : "Saved - navigation is unguarded."}</p>
</div>
);
};
const Other = () => (
<div>
<h3>Other page</h3>
<p>
Getting here while the editor was dirty took a confirmation - whether by link, by <code>useNavigate</code>, or by
the browser's back button.
</p>
</div>
);
const CancelNavigationNotFound = () => (
<div>
<h3>Not found</h3>
<p>
<Link route={cancelNavigationRoot} to={{}}>
Back to the editor
</Link>
</p>
</div>
);
/**
* Demo of vetoing navigation while a form has unsaved edits. The blocking is done by a
* `navigationGuardAtom`, registered for the lifetime of this component with `useNavigationGuard`,
* so it covers every way the URL can move - the ordinary `Link`s below, the `useNavigate` button,
* a direct route-atom write, and the browser's own back/forward buttons - rather than one link
* component's `onClick`.
*/
export const CancelNavigationApp = () => {
useNavigationGuard(unsavedEditsGuard);
return (
<>
<nav>
<Link route={cancelNavigationRoot} to={{}} exact>
Editor
</Link>
<Link route={otherRoute} to={{}}>
Other page
</Link>
</nav>
<p>
Edit the text, then try to leave. The links above, the <code>useNavigate</code> button and the browser's
own back/forward buttons all stop at the same confirm; save or discard, and none of them do.
</p>
<Switch fallback={<CancelNavigationNotFound />}>
<Route on={cancelNavigationRoot} exact>
<Editor />
</Route>
<Route on={otherRoute} exact>
<Other />
</Route>
</Switch>
</>
);
};
export default CancelNavigationApp;