A lightweight, modern router for Roku SceneGraph apps. Roku Router maps URL-style paths to components, manages view lifecycles, handles parameters, and supports route guards β enabling dynamic and seamless navigation experiences.
- URL-style navigation for Roku apps
- Dynamic routing with parameter support
- Route guards (
canActivate) for protected screens - View lifecycle hooks for fine-grained control
- Stack management (root routes, suspension, resume)
- Observable router state for debugging or analytics
Requires Roku Promises
Install via ropm:
npx ropm install promises@npm:@rokucommunity/promises
npx ropm install roku-router@npm:@tkss/roku-routerA route defines how your Roku app transitions between views. Routes are typically registered in your main scene.
Each route object can include:
| Property | Type | Required | Description |
|---|---|---|---|
pattern |
string | β | URL-like path pattern ("/details/movies/:id") |
component |
string | β | View component to render (must extend rokuRouter_View) |
isRoot |
boolean | β | Clears stack and resets breadcrumbs when true |
canActivate |
function | β | Guard function to control route access |
Views extending rokuRouter_View can define:
beforeViewOpenβ Called before the view loads (e.g. async setup, API calls)onViewOpenβ Called after previous view is closed/suspendedbeforeViewCloseβ Invoked before a view is destroyedonViewSuspend/onViewResumeβ Handle stack suspensions/resumptionsonRouteUpdateβ Fired when navigating to the same route with updated params/hashhandleFocusβ Defines focus handling when the view becomes active
<component name="MainScene" extends="Scene">
<script type="text/brightscript" uri="pkg:/source/roku_modules/rokurouter/router.brs" />
<script type="text/brightscript" uri="MainScene.bs" />
<children>
<rokuRouter_Outlet id="myOutlet" />
</children>
</component>sub init()
' Initialize the router at your main outlet
rokuRouter.initialize({ outlet: m.top.findNode("myOutlet") })
rokuRouter.addRoutes([
{ pattern: "/", component: "WelcomeScreen" },
{ pattern: "/shows", component: "CatalogScreen", root: true },
{ pattern: "/movies", component: "CatalogScreen", root: true },
{ pattern: "/details/series/:id", component: "DetailsScreen" },
{ pattern: "/details/series/:id/cast", component: "CastDetailsScreen" },
{ pattern: "/details/movies/:id", component: "DetailsScreen" },
{ pattern: "/details/movies/:id/cast", component: "CastDetailsScreen" },
{ pattern: "/:screenName", component: "DefaultScreen" }
])
rokuRouter.navigateTo("/") ' Go to the welcome view
' set the focus to the router
rokuRouter.setFocus({ focus: true })
end sub<component name="WelcomeScreen" extends="rokuRouter_View">
<script type="text/brightscript" uri="pkg:/source/roku_modules/promises/promises.brs" />
<script type="text/brightscript" uri="WelcomeScreen.bs" />
<children>
<Label id="label" />
</children>
</component>sub init()
m.label = m.top.findNode("label")
end sub
' Called before the view is shown
function beforeViewOpen(params as dynamic) as dynamic
m.label.text = "Hello!"
return promises.resolve(invalid)
end functionYou can observe routerState for debugging or analytics:
sub init()
rokuRouter.getRouter().observeField("routerState", "onRouterStateChanged")
end sub
sub onRouterStateChanged(event as Object)
data = event.getData()
print `Router state changed: ${data.id} ${data.type} ${data.state}`
end subRouter State Structure:
{
"id": "",
"type": "",
"state": {
"routeConfig": {},
"queryParams": {},
"routeParams": {},
"hash": ""
}
}Route guards let you allow/deny navigation based on custom logic (e.g., authentication, feature flags).
A guard is simply any node that exposes a canActivate function.
components/Managers/Auth/AuthManager.xml
<?xml version="1.0" encoding="utf-8"?>
<component name="AuthManager" extends="Node">
<interface>
<field id="isLoggedIn" type="boolean" value="false" />
<function name="canActivate" />
</interface>
</component>components/Managers/Auth/AuthManager.bs
import "pkg:/source/router.bs"
' Decide whether navigation should proceed.
' Return true to allow, false or a RedirectCommand to block/redirect.
function canActivate(currentRequest = {} as Object) as Dynamic
if m.top.isLoggedIn then
return true
end if
dialog = createObject("roSGNode", "Dialog")
dialog.title = "You must be logged in"
dialog.optionsDialog = true
dialog.message = "Press * To Dismiss"
m.top.getScene().dialog = dialog
' Redirect unauthenticated users (e.g., to home or login)
return rokuRouter.createRedirectCommand("/login")
end functionCreate an instance and expose it globally (so routes can reference it):
components/Scene/MainScene/MainScene.bs (snippet)
' Create AuthManager and attach to globals
m.global.addFields({
"AuthManager": createObject("roSGNode", "AuthManager")
})
' (Optional) observe auth changes
m.global.AuthManager.observeField("isLoggedIn", "onAuthManagerIsLoggedInChanged")Attach one or more guards to any route using the canActivate array:
rokuRouter.addRoutes([
{ pattern: "/", component: "WelcomeScreen", isRoot: true },
{ pattern: "/login", component: "LoginScreen" },
' Protected content β requires AuthManager.canActivate to allow
{ pattern: "/shows", component: "CatalogScreen", isRoot: true, canActivate: [ m.global.AuthManager ] },
{ pattern: "/movies", component: "CatalogScreen", isRoot: true, keepRootAlive: true, canActivate: [ m.global.AuthManager ] },
{ pattern: "/details/:type/:id", component: "DetailsScreen", canActivate: [ m.global.AuthManager ] },
{ pattern: "/details/:type/:id/cast", component: "CastDetailsScreen", canActivate: [ m.global.AuthManager ] }
])trueβ allow navigationfalseβ block navigation (stay on current view)RedirectCommandβ redirect elsewhere without showing the target route- Create via
rokuRouter.createRedirectCommand("/somewhere")
- Create via
Your guard receives currentRequest with the full navigation context, useful for deep-links or conditional flows:
function canActivate(currentRequest as Object) as Dynamic
' currentRequest.route.pattern, currentRequest.routeParams, currentRequest.queryParams, currentRequest.hash, etc.
if currentRequest?.queryParams?.requiresPro = true and not m.top.isProUser then
return rokuRouter.createRedirectCommand("/upgrade")
end if
return true
end functionYou can implement a reusable feature flag guard for gradual rollouts:
function canActivate(currentRequest as Object) as Dynamic
feature = currentRequest?.routeParams?.feature ' e.g. "/feature/:feature"
if m.global?.features[feature] = true then
return true
end if
return rokuRouter.createRedirectCommand("/")
end function- Toggle login in development:
m.global.AuthManager.isLoggedIn = true - Verify redirects by attempting to navigate to a protected route while logged out:
rokuRouter.navigateTo("/shows")
- Listen to router state changes to confirm block/redirect behavior:
rokuRouter.getRouter().observeField("routerState", "onRouterStateChanged")
The included test project already wires up an
AuthManagerand protects/shows,/movies, and/details/*routes usingcanActivate.
Every view lifecycle receives a route snapshot so your screen logic can react to the URL that triggered navigation.
params is constructed by the router just before the lifecycle is called, and includes:
params.route.routeConfig ' the matched route definition
params.route.routeParams ' extracted from pattern placeholders (e.g. :id, :type)
params.route.queryParams ' parsed from ?key=value pairs
params.route.hash ' parsed from #hash
The snapshot is sourced from the URL you navigated to (e.g. "/details/movies/42?page=2&sort=trending#grid=poster"). The router builds this object and passes it into beforeViewOpen(params), onViewOpen(params), and onRouteUpdate(params).
' CatalogScreen.bs (excerpt)
function beforeViewOpen(params as object) as dynamic
' Read route params (e.g., /:type and /:id)
contentType = params.route.routeParams?.type ' "shows" or "movies"
itemId = params.route.routeParams?.id ' e.g., "42"
' Read query params (?page=2&sort=trending)
pageIndex = val(params.route.queryParams?.page) ' 2
sortKey = params.route.queryParams?.sort ' "trending"
' Optional: hash fragment (#grid=poster)
gridMode = params.route.hash
' Kick off data loading based on URL snapshot
' ... start tasks or fetches here ...
' Return a promise to delay opening until ready,
' or return true to open immediately and manage loading UI yourself.
return promises.resolve(invalid)
end function
' If you navigate to the **same route pattern** with different params or hash,
' `onRouteUpdate(params)` will fire (when `allowReuse` is enabled),
' allowing you to update the view without rebuilding it.
' CatalogScreen.bs (excerpt)
function onRouteUpdate(params as object) as dynamic
oldRoute = params.oldRoute
newRoute = params.newRoute
return promises.resolve(invalid)
end functionThe route snapshot is assembled by the router by parsing:
- the pattern match result β
routeParams - the query string β
queryParams - the hash β
hash
That structured object is then provided to the view lifecycles mentioned above. This keeps your screens URL-driven and easy to test (you can navigate with different URLs and assert behavior based on params).
- Join the Roku Developers Slack
- Report issues or request features via GitHub Issues
Licensed under the MIT License.