The reactRouter package provides multiple ways to
add routing. RouterProvider() is the recommended entry
point: pass a router built with createHashRouter(),
createMemoryRouter(), or createBrowserRouter()
to its router argument.
RouterProvider() — recommended entry point
RouterProvider() renders a data router. It mirrors the
React Router v7 composition pattern: you build a router with one of the
create*Router() functions and pass it to the
router argument.
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = div(
NavLink(to = "/", "Home"),
NavLink(to = "/analysis", "Analysis"),
Outlet()
),
Route(index = TRUE, element = "Main content"),
Route(path = "analysis", element = "Analysis content")
)
)
)Arguments
router — required. A router object
produced by createHashRouter(),
createMemoryRouter(), or
createBrowserRouter().
fallbackElement — optional. An element
shown while the initial route’s loader is resolving
(i.e. the loading state before the first route renders). Example:
RouterProvider(
router = createHashRouter( ... ),
fallbackElement = div("Loading…")
)Choosing a router
The three create*Router() functions differ only in how
they interact with the browser URL:
| Function | URL style | Works on file://
|
Recommended |
|---|---|---|---|
createHashRouter() |
/#/about |
yes | yes — default choice |
createMemoryRouter() |
in-memory (no URL change) | yes | yes — for embedded / multi-router pages |
createBrowserRouter() |
/about |
no | no (see below) |
createHashRouter() — recommended default
createHashRouter() uses the URL hash
(/#/about) for routing. Hash changes never trigger a real
HTTP request, so the router works in Shiny apps, Quarto documents, and
static HTML pages without any server configuration.
Why it works well with Shiny:
- Hash changes do not affect Shiny’s HTTP layer.
-
session$clientData$url_hashupdates reactively whenever the route changes.
Why it is preferred over the legacy
HashRouter():
- Unlocks the full data router API:
loader,action,errorElement,useLoaderData,useNavigation,useFetcher, and deferred data viaAwait. - This is the model React Router is actively developing;
HashRouter()is a legacy compatibility wrapper.
library(reactRouter)
library(htmltools)
RouterProvider(
router = createHashRouter(
Route(
path = "/",
element = div(
tags$nav(
tags$ul(
tags$li(NavLink(to = "/", "Home")),
tags$li(NavLink(to = "/about", "About"))
)
),
tags$hr(),
Outlet()
),
Route(index = TRUE, element = div(h2("Home"), p("Welcome."))),
Route(path = "about", element = div(h2("About"), p("About page.")))
)
)
)
createMemoryRouter() — recommended for in-memory
routing
createMemoryRouter() keeps routing state entirely in
memory. It never reads or modifies the browser URL, which means:
- It always starts at
/, regardless of the real URL. - Navigation updates the in-memory location but the browser address bar does not change.
- Browser back/forward buttons and bookmarks do not reflect the current route.
This makes it ideal for internal-only navigation — multi-step wizards, tabbed panels, or any UI where the route is an implementation detail rather than something the user should share or bookmark. It is also the right choice when you need multiple independent routers on the same page (e.g. several routed widgets in a Shiny dashboard), since they don’t touch the URL and therefore can’t conflict with each other.
library(reactRouter)
library(htmltools)
RouterProvider(
router = createMemoryRouter(
Route(
path = "/",
element = div(
tags$nav(
tags$ul(
tags$li(NavLink(to = "/", "Home")),
tags$li(NavLink(to = "/about", "About"))
)
),
tags$hr(),
Outlet()
),
Route(index = TRUE, element = div(h2("Home"), p("Welcome."))),
Route(path = "about", element = div(h2("About"), p("About page.")))
)
)
)Choosing between hash and memory routers:
| Scenario | Recommended router |
|---|---|
| Shiny app with bookmarkable/shareable routes | createHashRouter() |
| Static site (Quarto, R Markdown) with multiple pages | createHashRouter() |
| Shiny app with internal-only navigation (wizards, tabbed panels) | createMemoryRouter() |
| Multiple independent routed widgets on the same page | createMemoryRouter() |
| Embedded widget where the URL should not change | createMemoryRouter() |
In short: if the route should be visible in the URL (bookmarks, deep
links, back/forward), use createHashRouter(). If routing is
purely an internal UI concern, use
createMemoryRouter().
createBrowserRouter() — not recommended in R
createBrowserRouter() uses the HTML5 History API
(pushState) for clean URLs (e.g. /about
instead of /#/about). It is the standard router in full
React web applications, but it is not well-suited for use with
this R package:
-
Static files (
file://): The URL path is a file system path (e.g./C:/Users/.../file.html), not/. No route matches and the app immediately shows a 404. -
Shiny apps: Clean URLs like
/aboutrequire the server to rewrite all paths back to/. Shiny does not do this by default. -
Refreshing on a sub-route: Even when served from a
proper HTTP server, refreshing on
/aboutwill 404 unless the server is configured to rewrite all routes toindex.html.
createBrowserRouter() only works correctly in a full web
application deployment (e.g. React served by Node.js, Express, or Nginx
with URL rewriting). That scenario is outside the scope of this R
package.
If you nevertheless decide to use createBrowserRouter()
and your Shiny app is mounted under a sub-path (e.g. on Posit Connect at
/content/abc/ or behind a reverse proxy), set
basename so the router knows where the app starts.
Otherwise every route 404s because the router compares the full path
(/content/abc/about) against patterns rooted at
/.
RouterProvider(
router = createBrowserRouter(
Route(path = "/", element = ...),
basename = "/content/abc" # match your deployment prefix
)
)createHashRouter() and createMemoryRouter()
do not need basename — neither uses the URL path.
Legacy: HashRouter() and MemoryRouter() —
component API
HashRouter() and MemoryRouter() are the
older, component-based equivalents of createHashRouter()
and createMemoryRouter(). They remain fully
supported and are safe to use in existing code.
However, RouterProvider() with
createHashRouter() / createMemoryRouter() is
preferred for new code because the component-based API:
- Does not support
loader,action,errorElement, or any data router features. - Is not the direction React Router is developing toward.
Use HashRouter() or MemoryRouter() only if
you have a specific reason to stay with the component API
(e.g. migrating an existing app incrementally).
Legacy router caveat: re-rendering parent UI remounts route elements
Route() attaches a fresh random React key
to its element on every R-side call. The data router
(createHashRouter() / RouterProvider()) builds
the route tree once on mount and is unaffected. The component-API
routers (HashRouter() / MemoryRouter() +
Routes() + Route()) are different: if the host
UI is re-rendered — typically via shiny::renderUI() /
shiny::uiOutput() — Route() is re-evaluated,
every route element gets a new key, and React unmounts and remounts the
matched element on every parent render even when the URL has not
changed. Any client-side state inside the route element (form input,
scroll position, third-party widget state) is reset.
Workarounds:
-
Preferred: switch to
RouterProvider(router = createHashRouter(...)). The data router builds the route tree once and is immune. - If you must keep the legacy component API, render the router at the
top level of your
ui(not insiderenderUI) so it is built once.
Understanding reloadDocument
Link() and NavLink() accept a
reloadDocument prop. The default is FALSE,
matching React Router’s own default, and that default is correct for
almost every use of this package — including Shiny apps with
server-rendered output.
reloadDocument = FALSE (default, recommended)
React Router intercepts the click and updates the route entirely on the client side, without reloading the page. This is the right behavior for:
- Static sites and Quarto documents — there is no server to re-initialize.
-
createMemoryRouter()/MemoryRouter()— a full page reload would reset the in-memory routing state back to/. -
Data router
loader/action— these only run during client-side navigations; a full reload bypasses them. -
Shiny apps with
uiOutput,renderUI,plotOutput, or htmlwidgets. Shiny output bindings re-attach automatically when React Router mounts the new route’s element, and reactives that depend on the URL —useLocation(),useParams(),useSearchParams(), or Shiny’s own reactivesession$clientData$url_hash— update without any reload. The bundledshiny.fluentexample (reactRouterExample("shiny.fluent")) navigates between routes containing liveecharts4rOutput()anduiOutput()and uses the defaultreloadDocument = FALSEthroughout.
A full reload would actually be worse for Shiny: it tears
down the session, drops shared reactives (e.g. a
hero_selected reactive used across routes), and re-runs
every initialization step on every click.
reloadDocument = TRUE (rare)
Setting reloadDocument = TRUE skips React Router’s
client-side navigation and lets the browser handle the
<a> click natively. What this actually does depends
on the router:
| Router | Rendered href | Effect of reloadDocument = TRUE
|
|---|---|---|
createHashRouter() / HashRouter()
|
#/path |
Effectively a no-op — browsers don’t reload on hash-only changes.
The URL hash updates and React Router still picks it up via
hashchange. |
createMemoryRouter() / MemoryRouter()
|
(none — no real <a href>) |
No effect; memory routers don’t use real URLs. |
createBrowserRouter() |
/path |
Triggers a full HTTP request to /path. Requires the
server to serve the app on every route, which Shiny does not do by
default — the request will 404. |
In practice this means reloadDocument = TRUE is rarely
the right choice in this package. Leave it at the default.
Quick reference
| Context | reloadDocument |
|---|---|
| Static site / Quarto / R Markdown |
FALSE (default) |
createMemoryRouter() / MemoryRouter()
|
FALSE (default) |
Data router with loader / action
|
FALSE (default) |
Shiny app with uiOutput / renderUI /
plotOutput
|
FALSE (default) |
