leave - greydragon888/real-router GitHub Wiki
Subscribe to confirmed route departures. Fires after canDeactivate guards pass but before canActivate guards run — guaranteeing the callback only executes when the user has actually left.
router.subscribeLeave(listener: LeaveFn): Unsubscribe
type LeaveFn = (state: LeaveState) => void | Promise<void>
type LeaveState = { route: State; nextRoute: State; signal: AbortSignal }-
route— the route being departed (current route at the time of the callback) -
nextRoute— the destination route -
signal—AbortSignalfor cooperative cancellation (aborted on concurrent navigation,router.stop(), orrouter.dispose()) - Returns an
Unsubscribefunction - Returning a
Promise<void>blocks the navigation pipeline until resolved — activation guards run only after all leave listeners complete
| Scenario | Fires? |
|---|---|
canDeactivate returns true |
✅ Yes |
canDeactivate returns false |
❌ No |
No canDeactivate guard (zero-guard path) |
✅ Yes |
| First navigation (no previous route) | ❌ No |
navigateToNotFound() |
❌ No |
Use subscribeLeave() for side-effects that must only run when a route departure is confirmed:
- Exit animations (async) — animate out the current page before the new one mounts
- View Transitions API (async) — coordinate CSS transitions with route changes
- Scroll position preservation (sync)
- Form draft saving (sync)
- Fetch request abort (sync)
- Analytics / time-on-page tracking (sync)
Contract: async
subscribeLeaveis intended for short-lived navigation side-effects (exit animations, View Transitions). For server requests use guards; for data fetching use plugins. An async listener blocks the navigation pipeline — a long-running listener means a stuck navigation.
subscribeLeave() |
subscribe() |
|
|---|---|---|
| Fires | Before state changes | After state changes |
| Timing | LEAVE_APPROVED phase | TRANSITION_SUCCESS |
router.getState() |
Still returns current route | Returns new route |
| Guaranteed | Only when departure confirmed | On every successful navigation |
| Use for | Side-effects on leaving | Side-effects on arriving |
import { useEffect } from "react";
import { useNavigator } from "@real-router/react";
function SettingsPage() {
const navigator = useNavigator();
const [draft, setDraft] = useState("");
useEffect(
() =>
navigator.subscribeLeave(({ route }) => {
if (route.name === "settings" && draft) {
localStorage.setItem("settings:draft", draft);
}
}),
[navigator], // subscribe once — draft read via closure ref pattern
);
}<script setup>
import { watchEffect } from "vue";
import { useNavigator } from "@real-router/vue";
const navigator = useNavigator();
watchEffect((onCleanup) => {
const unsub = navigator.subscribeLeave(({ route }) => {
if (route.name === "settings") {
sessionStorage.setItem("scroll", String(window.scrollY));
}
});
onCleanup(unsub);
});
</script>import { createEffect, onCleanup } from "solid-js";
import { useNavigator } from "@real-router/solid";
function Page() {
const navigator = useNavigator();
createEffect(() => {
const unsub = navigator.subscribeLeave(({ route }) => {
if (route.name === "reports") {
sessionStorage.setItem("scroll", String(window.scrollY));
}
});
onCleanup(unsub);
});
}<script>
import { useNavigator } from "@real-router/svelte";
const navigator = useNavigator();
$effect(() => {
return navigator.subscribeLeave(({ route }) => {
if (route.name === "settings") {
localStorage.setItem("draft", displayName);
}
});
});
</script>Query whether the FSM is currently in the LEAVE_APPROVED phase:
router.isLeaveApproved(): booleanReturns true only between deactivation and activation guard execution. Useful in plugins that need to distinguish sub-phases of a navigation.
router.subscribeLeave(async ({ signal }) => {
await animateOut(document.querySelector(".page"), { signal });
});router.subscribeLeave(async () => {
await document.startViewTransition(() => {
/* ... */
}).finished;
});router.subscribeLeave(async ({ signal }) => {
const timeoutSignal = AbortSignal.timeout(500);
const combined = AbortSignal.any([signal, timeoutSignal]);
await animateOut(element, { signal: combined });
});Errors in subscribeLeave() callbacks propagate as TRANSITION_ERROR. Each listener runs independently — a failure in one does not prevent others from executing (Promise.allSettled semantics). The first error is re-thrown after all listeners complete.
router.subscribeLeave(({ route }) => {
try {
analytics.track("page_leave", { from: route.name });
} catch {
// analytics failure must not block navigation
}
});When a concurrent navigation starts during an async leave listener, the signal is aborted. Well-behaved listeners should check signal.aborted or pass signal to cancellable APIs:
router.subscribeLeave(async ({ signal }) => {
if (signal.aborted) return;
await fadeOut(element, { signal }); // throws AbortError if signal fires
});- subscribe() — symmetric method for arrival side-effects
- Navigation Lifecycle — full pipeline with LEAVE_APPROVED phase
-
Plugin Architecture —
onTransitionLeaveApproveplugin hook