Documentation

Navigation Hooks

Run code as a screen is left or arrived at — and, when you need to, stop a navigation before it happens.

$beforeNavigate

Runs on the way out. In its simplest form it is a place to save and clean up:

$beforeNavigate {
  Db.save()
  analytics.track("left_screen")
}

Blocking a navigation

Take a cancel parameter and the hook can refuse. This is the unsaved-changes guard:

$beforeNavigate(({ cancel }) => {
  if (hasUnsavedChanges) {
    showDialog = true
    cancel()
  }
})
The two forms behave differently, and the difference is visible to users. The block form runs during disposal, which is late enough that Android's back preview animation still works. The cancel form has to run before the navigation commits, which means the system preview cannot play. If you want a gesture-driven animation and a guard, drive the animation with $back instead.
FormWhen it runsSystem back preview
$beforeNavigate { … }During disposalWorks
$beforeNavigate(({ cancel }) => …)Before navigatingSuppressed

Parameters

ParameterTypeMeaning
cancel() -> UnitCall to stop the navigation
fromNavigationTargetThe screen being left
toNavigationTargetWhere it was heading

$afterNavigate

Runs on the way in — analytics, focus, anything that should happen per arrival:

$afterNavigate {
  analytics.track("screen_view")
}

$afterNavigate(({ from, to }) => {
  analytics.track("navigated", from?.path, to.path)
})
ParameterTypeMeaning
fromNavigationTarget?Previous screen — null on first load
toNavigationTargetThe screen just arrived at
$afterNavigate runs on every arrival, including coming back to a screen that was already on the stack. $onMount runs when the screen is composed. For "track a screen view" you want this one; for "load once" you want $onMount.

The $back driver

Back is also a gesture, and a gesture has a progress value. $back exposes it as a morph driver, so a screen can animate under the user's thumb rather than snapping when they let go:

<Box scale:back={[1, 0.95]} opacity:back={[1, 0.8]}>
  <Content />
</Box>

The first value is where the property sits at rest, the second where it lands at a completed back gesture. Everything in between follows the drag — but only on a screen that also has a $beforeNavigate with cancel in its signature. Without one, the system owns the gesture and $back sees only its end, snapping straight to the second value.

Predictive back

Android's system-level back preview — where the outgoing screen shrinks to show what is behind it — is on by default, which adds android:enableOnBackInvokedCallback="true" to the manifest. Turn it off:

# whitehall.toml
[android]
predictive_back = false

This turns the gesture off outright, so a $back driver in the same project has nothing to follow — a predictive-back-off warning naming both. See Configuration.

See Also