Skip to contents

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() 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_hash updates 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 via Await.
  • 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() 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() 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 /about require 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 /about will 404 unless the server is configured to rewrite all routes to index.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() and MemoryRouter() — a full page reload would reset the in-memory routing state back to /, breaking navigation entirely.
  • Data router loaders and actionsloader and action functions 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.):

NavLink(to = "/about", reloadDocument = TRUE, "About")
Link(to = "/",         reloadDocument = TRUE, "Home")

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