
Advanced hooks: useBlocker and useOutletContext
Source:vignettes/advanced-hooks.Rmd
advanced-hooks.RmdOverview
This vignette covers two hooks that address specific routing patterns beyond basic data loading and navigation:
| Hook | What it does |
|---|---|
useBlocker() |
Intercept navigation — e.g. warn about unsaved changes |
useOutletContext() |
Read data passed from a parent route through
Outlet(context = ...)
|
Both hooks require a data router: RouterProvider() with
createHashRouter(), createBrowserRouter(), or
createMemoryRouter().
Two ways to inject hook values
Every hook wrapper in reactRouter offers two
mutually exclusive ways to turn a hook value into UI. Pick whichever is
more ergonomic for the case at hand — the API is identical across all
hooks (useLoaderData(), useParams(),
useBlocker(), useOutletContext(), …).
1. R-idiomatic (into + as +
selector) — the default. The hook value is cloned
into an existing component as a single prop. Good for displaying one
field, populating a data grid’s rows, or feeding an input’s
value:
useOutletContext(tags$span(), selector = "user.name")2. Native render-prop
(render = JS(...)) — the escape hatch. Pass a JS
function (value) => ReactNode. Mirrors the official
React Router pattern and is the right choice when one prop is not enough
— e.g. combining several fields, conditional rendering, or template
strings:
useOutletContext(render = JS("u => `${u.name} (${u.role})`"))When render is supplied, into,
as, and selector are ignored — apply any
selection inside the JS function directly.
useBlocker()
useBlocker() intercepts navigation when a JavaScript
condition returns true. It exposes a state
field:
| State | Meaning |
|---|---|
"unblocked" |
No navigation is being intercepted (default) |
"blocked" |
A navigation was intercepted — show a confirmation UI |
"proceeding" |
User confirmed; navigation is in progress |
Arguments
| Argument | Required | Description |
|---|---|---|
into |
yes* | Component that receives the value |
as |
no | Prop to inject into. Defaults to "children"
|
selector |
no | Field from the blocker object. Defaults to "state"
|
render |
no |
JS() function (value) => ReactNode.
When supplied, replaces
into/as/selector. |
shouldBlock |
no | A JS() function receiving
{ currentLocation, nextLocation, historyAction } and
returning true to block or false to allow.
Defaults to FALSE (never blocks). |
* into is required unless render is
supplied.
Show blocker state
useBlocker(
tags$span(),
shouldBlock = JS(
"({ currentLocation, nextLocation }) =>
currentLocation.pathname !== nextLocation.pathname"
)
)
# renders "unblocked", "blocked", or "proceeding"Conditional UI with render
Because selector = "state" only exposes the state
string, use render when you need to branch on the full
blocker object (e.g. show the target URL in the confirmation
prompt):
useBlocker(
shouldBlock = should_block,
render = JS(
"b => b.state === 'blocked'
? `Leave for ${b.location.pathname}?`
: null"
)
)Full example
The render function receives the full blocker object,
including b.proceed() and b.reset() methods.
Use them inside onClick handlers to build a real
confirmation dialog:
library(reactRouter)
library(htmltools)
should_block <- JS(
"({ currentLocation, nextLocation }) =>
currentLocation.pathname !== nextLocation.pathname"
)
blocker_ui <- JS(
"b => {
const state = React.createElement('code', null, b.state);
if (b.state !== 'blocked') return state;
return React.createElement(React.Fragment, null,
state, ' — ',
React.createElement('button', { onClick: () => b.proceed() }, 'Leave'),
' ',
React.createElement('button', { onClick: () => b.reset() }, 'Stay')
);
}"
)
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = div(
tags$nav(tags$ul(
tags$li(NavLink(to = "/", "Home")),
tags$li(NavLink(to = "/other", "Other page"))
)),
tags$hr(),
Outlet()
),
Route(
index = TRUE,
element = div(
tags$h3("Home — blocker active"),
tags$p(
tags$strong("Blocker state: "),
useBlocker(shouldBlock = should_block, render = blocker_ui)
),
tags$p(
style = "color: #555; font-size: 0.9em;",
"Click 'Other page'. The state changes to ",
tags$code('"blocked"'), " and two buttons appear.",
tags$br(),
tags$em("Leave"), " calls ", tags$code("b.proceed()"),
" to confirm navigation;",
tags$br(),
tags$em("Stay"), " calls ", tags$code("b.reset()"),
" to cancel and return to ", tags$code('"unblocked"'), "."
)
)
),
Route(
path = "other",
element = div(
tags$h3("Other page"),
tags$p("No blocker here. Navigate freely.")
)
)
)
)
)
#> Warning: The `reloadDocument` argument of `NavLink()` default is now FALSE as of
#> reactRouter 0.2.0.
#> ℹ The default of `reloadDocument` was TRUE in version 0.1.1. It is now FALSE.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
useOutletContext()
Parent routes can pass data to child routes through
Outlet(context = ...). Any child route reads that data with
useOutletContext(), avoiding prop-drilling through deeply
nested layouts.
Arguments
| Argument | Required | Description |
|---|---|---|
into |
yes* | Component that receives the value |
as |
no | Prop to inject into. Defaults to "children"
|
selector |
no | Field to extract from the context object. Dotted paths like
"user.name" navigate nested objects. NULL
(default) injects the full context. |
render |
no |
JS() function (context) => ReactNode.
When supplied, replaces
into/as/selector. |
* into is required unless render is
supplied.
Passing context from a parent route
Add a context argument to Outlet():
Route(
path = "/",
element = div(
# Pass a named list — any R object that serialises to JSON
Outlet(context = list(user = "Alice", role = "admin")),
NavLink(to = "/profile", "Profile")
),
Route(
path = "profile",
element = div(
tags$p("Name: ", useOutletContext(tags$span(), selector = "user")),
tags$p("Role: ", useOutletContext(tags$span(), selector = "role"))
)
)
)Full example
library(reactRouter)
library(htmltools)
current_user <- list(name = "Alice", role = "admin", email = "[email protected]")
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = div(
tags$nav(tags$ul(
tags$li(NavLink(to = "/", "Dashboard")),
tags$li(NavLink(to = "/profile", "Profile")),
tags$li(NavLink(to = "/settings","Settings"))
)),
tags$hr(),
Outlet(context = current_user)
),
Route(
index = TRUE,
element = div(
tags$h3("Dashboard"),
tags$p(
"Welcome back, ",
tags$strong(useOutletContext(tags$span(), selector = "name")),
"!"
),
tags$p("Your role: ", useOutletContext(tags$code(), selector = "role"))
)
),
Route(
path = "profile",
element = div(
tags$h3("Profile"),
tags$p("Name: ", useOutletContext(tags$span(), selector = "name")),
tags$p("Email: ", useOutletContext(tags$span(), selector = "email")),
tags$p("Role: ", useOutletContext(tags$span(), selector = "role")),
tags$details(
tags$summary("Full context (JSON)"),
tags$pre(useOutletContext(tags$span()))
)
)
),
Route(
path = "settings",
element = div(
tags$h3("Settings"),
tags$p(
"Editing settings for ",
tags$strong(useOutletContext(tags$span(), selector = "name"))
)
)
)
)
)
)The context is passed once at the layout level and is available to
every child route without repeating it in each element.
Combining fields with render
selector extracts a single field. When you need to
compose several fields into one output — a greeting, a formatted label,
a conditional element — use render instead:
useOutletContext(
render = JS(
"u => u.role === 'admin'
? `${u.name} (admin — ${u.email})`
: u.name"
)
)This matches the React Router v7 idiom where the component itself decides how to consume the hook value, at the cost of a small amount of inline JS.