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.

Predictive back

Android's system-level back preview — where the outgoing screen shrinks to show what is behind it — is off unless you ask for it:

# whitehall.toml
[app]
predictive_back = true

That adds android:enableOnBackInvokedCallback="true" to the manifest. See Configuration.

See Also