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.
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: component API — still works, but RouterProvider() is preferred
library(shiny)
library(reactRouter)
ui <- HashRouter(
NavLink(to = "/", reloadDocument = TRUE, "Home"), br(),
NavLink(to = "/other", reloadDocument = TRUE, "Other"),
Routes(
Route(path = "/", element = uiOutput("uiHome")),
Route(path = "/other", element = uiOutput("uiOther"))
)
)
server <- function(input, output, session) {
url_hash <- reactive(session$clientData$url_hash)
output$uiHome <- renderUI({ p("Home — hash: ", url_hash()) })
output$uiOther <- renderUI({ p("Other — hash: ", url_hash()) })
}
shinyApp(ui, server)Understanding reloadDocument
Link() and NavLink() accept a
reloadDocument prop that controls how navigation works. The
default is FALSE, matching React Router’s own default.
reloadDocument = FALSE (default)
React Router handles navigation entirely on the client side — it swaps the displayed route component without reloading the page. This is the correct behavior for:
- Static sites and Quarto documents — there is no server to re-initialize. Client-side navigation provides smooth, instant route switching.
-
createMemoryRouter()andMemoryRouter()— a full page reload would reset the in-memory routing state back to/, breaking navigation entirely. -
Data router loaders and actions —
loaderandactionfunctions only execute during client-side navigations. A full page reload bypasses React Router and loaders will not run.
reloadDocument = TRUE
Clicking a link triggers a full page reload, as if
the user clicked a regular <a href>. This is
necessary in Shiny apps with server-rendered content
because:
- Shiny’s server needs to re-initialize to read the new URL hash and
render the correct
uiOutput()/renderUI()content. - Without a reload, server-rendered UI will not update properly since the Shiny session was started with the old route.
Set reloadDocument = TRUE whenever your routes contain
server-rendered output (uiOutput, plotOutput,
etc.):
Quick reference
| Context | reloadDocument |
Why |
|---|---|---|
| Static site / Quarto / R Markdown |
FALSE (default) |
No server; client-side navigation is smooth |
createMemoryRouter() / MemoryRouter()
|
FALSE (default) |
Reload resets in-memory state to /
|
Data router with loader/action
|
FALSE (default) |
Loaders only run on client-side navigations |
Shiny app with uiOutput/renderUI
|
TRUE |
Server must re-initialize to render new content |
