canDeactivate Guards
Block navigation away from a route based on application state. canDeactivate guards mirror the existing canActivate API and are the standard way to implement "unsaved changes" dialogs and playback protection in Roku Router.
Why canDeactivate?
Roku Router already supports canActivate guards to protect routes from being entered. However there is currently no equivalent mechanism to protect routes from being left. This creates a gap for common application patterns that require the user to confirm or complete an action before navigation is allowed to proceed.
Two particularly common cases on Roku are:
- Unsaved changes — a user has edited a form or profile screen and attempts to navigate away without saving. The application needs to intercept that navigation and present a confirmation dialog before allowing the route to deactivate.
- Playback protection — a user is in an active playback or purchase flow and presses back. Silently navigating away could interrupt a transaction or cause a poor user experience; the application needs to block or confirm the navigation first.
Without canDeactivate, developers work around this by intercepting remote key events directly on the view, which is fragile, inconsistent with the routing model, and bypassed entirely by programmatic navigateTo() calls. A first-class canDeactivate guard resolves this cleanly and consistently.
Route Configuration
Add an optional canDeactivate array to any route definition, alongside canActivate:
{ pattern: "/player/:id", component: "PlayerScreen",
canActivate: [m.global.AuthManager]
canDeactivate: [m.global.PlaybackGuard]
}
canDeactivate is optional — omitting it leaves deactivation behaviour unchanged
canDeactivate accepts an array of one or more guard references
- A route may define both
canActivate and canDeactivate independently
Usage
Playback protection
' PlaybackGuard.brs
function canDeactivate(ctx as Object) as Boolean
' ctx.currentRoute - the route being left
' ctx.targetRoute - the route being navigated to
if m.global.PlaybackManager.isPlaying then
' Show a confirmation dialog and block until resolved.
' Return true to allow, false to block.
return m.global.DialogManager.confirm("Stop playback and leave?")
end if
return true
end function
Unsaved changes
{ pattern: "/profile/edit", component: "ProfileEditView",
canDeactivate: [m.global.UnsavedChangesGuard]
}
' UnsavedChangesGuard.brs
function canDeactivate(ctx as Object) as Boolean
if m.global.FormState.isDirty then
return m.global.DialogManager.confirm("Discard unsaved changes?")
end if
return true
end function
Guard Evaluation
Each guard in the canDeactivate array is called in order with a context object:
| Context key |
Value |
ctx.currentRoute |
The route being left |
ctx.targetRoute |
The route being navigated to (Invalid if exiting the app via back()) |
- If any guard returns
false, navigation is cancelled immediately — subsequent guards in the array are not evaluated
- If all guards return
true, navigation proceeds normally through the rest of the routing pipeline
- Guards are synchronous and return a plain
Boolean — async patterns (e.g. showing a dialog) must block the BrightScript execution thread until the user responds before returning
Cancellation Behaviour
When a canDeactivate guard returns false:
- The current route remains active and fully mounted — no lifecycle hooks (
_beforeViewClose etc.) are triggered on it
- The destination route is not mounted and no history stack changes are made
- The router surfaces a cancellation signal (return value or router event) so the calling view can respond (e.g. restore focus to a button)
How the Pipeline Works
canDeactivate is evaluated before any other routing logic — there is no point evaluating the destination if the current route cannot be left.
canDeactivate guards on the current route
canActivate guards on the destination route
_beforeViewClose on the current view
- View deactivation / removal
- Destination view creation and
_beforeViewLoad / _onViewLoaded lifecycle
canDeactivate is triggered consistently by both navigateTo() and back() — the guard cannot be bypassed through either path. Deep link navigations that replace the current route also trigger canDeactivate.
Notes
- Async / promise-returning
canDeactivate guards are not currently supported — synchronous Boolean only
- A per-navigation bypass mechanism (e.g. a force flag to skip guards) is not currently supported and may be added as a follow-up
- A global fallback
canDeactivate guard applied to all routes is not currently supported
canDeactivate Guards
Block navigation away from a route based on application state.
canDeactivateguards mirror the existingcanActivateAPI and are the standard way to implement "unsaved changes" dialogs and playback protection in Roku Router.Why canDeactivate?
Roku Router already supports
canActivateguards to protect routes from being entered. However there is currently no equivalent mechanism to protect routes from being left. This creates a gap for common application patterns that require the user to confirm or complete an action before navigation is allowed to proceed.Two particularly common cases on Roku are:
Without
canDeactivate, developers work around this by intercepting remote key events directly on the view, which is fragile, inconsistent with the routing model, and bypassed entirely by programmaticnavigateTo()calls. A first-classcanDeactivateguard resolves this cleanly and consistently.Route Configuration
Add an optional
canDeactivatearray to any route definition, alongsidecanActivate:{ pattern: "/player/:id", component: "PlayerScreen", canActivate: [m.global.AuthManager] canDeactivate: [m.global.PlaybackGuard] }canDeactivateis optional — omitting it leaves deactivation behaviour unchangedcanDeactivateaccepts an array of one or more guard referencescanActivateandcanDeactivateindependentlyUsage
Playback protection
Unsaved changes
{ pattern: "/profile/edit", component: "ProfileEditView", canDeactivate: [m.global.UnsavedChangesGuard] }Guard Evaluation
Each guard in the
canDeactivatearray is called in order with a context object:ctx.currentRoutectx.targetRouteInvalidif exiting the app viaback())false, navigation is cancelled immediately — subsequent guards in the array are not evaluatedtrue, navigation proceeds normally through the rest of the routing pipelineBoolean— async patterns (e.g. showing a dialog) must block the BrightScript execution thread until the user responds before returningCancellation Behaviour
When a
canDeactivateguard returnsfalse:_beforeViewCloseetc.) are triggered on itHow the Pipeline Works
canDeactivateis evaluated before any other routing logic — there is no point evaluating the destination if the current route cannot be left.canDeactivateguards on the current routecanActivateguards on the destination route_beforeViewCloseon the current view_beforeViewLoad/_onViewLoadedlifecyclecanDeactivateis triggered consistently by bothnavigateTo()andback()— the guard cannot be bypassed through either path. Deep link navigations that replace the current route also triggercanDeactivate.Notes
canDeactivateguards are not currently supported — synchronousBooleanonlycanDeactivateguard applied to all routes is not currently supported