Skip to contents

The reactRouter package provides multiple ways to add routing. RouterProvider() is the simplest entry point: just pass your Route() elements and pick a router type.

RouterProvider() — the simplest way to get started

RouterProvider() is a unified wrapper that covers the most common use cases through its type argument:

  • type = "hash" (default) — hash-based routing (/#/about). Works in Shiny, Quarto, and static HTML. Routes are visible in the URL, so users can bookmark pages and use browser back/forward.
  • type = "memory" — in-memory routing. The URL never changes. Ideal for embedded widgets, multi-step wizards, or multiple independent routers on the same page.
  • type = "browser" — HTML5 History API (/about). Not recommended for use with R (see BrowserRouter below).
library(reactRouter)
library(htmltools)

RouterProvider(
  # type = "hash" by default — change to type = "memory" if needed
  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.")))
  )
)

The lower-level functions (createHashRouter(), createMemoryRouter(), HashRouter(), MemoryRouter()) are also available and give you more control. They are described below.

Quick reference

Router / shortcut URL style Works on file:// Recommended
RouterProvider(type = "hash") /#/about yes yes — simplest
RouterProvider(type = "memory") in-memory (no URL change) yes yes — simplest
createHashRouter() /#/about yes yes
createMemoryRouter() in-memory (no URL change) yes yes
HashRouter() /#/about yes no (legacy)
MemoryRouter() in-memory (no URL change) yes no (legacy)
RouterProvider(type = "browser") / BrowserRouter() /about no no

createHashRouter is the best default choice for R Shiny apps and static sites (Quarto, R Markdown HTML output). It uses the URL hash (the # fragment) for routing, so routes like /#/about are visible in the address bar. This means users can bookmark pages, share deep links, and use the browser back/forward buttons — all without requiring a server that handles URL rewriting.

Why it works well with Shiny:

  • Hash changes (/#/about) never trigger a real HTTP request, so Shiny’s server is not affected.
  • session$clientData$url_hash updates reactively whenever the route changes, letting the server know the current page.

Why it is preferred over HashRouter:

  • Unlocks the full data router API: loader, action, errorElement, useLoaderData, useNavigation, useFetcher, and deferred data.
  • This is the model React Router is actively developing; HashRouter is a legacy compatibility wrapper.
library(reactRouter)
library(htmltools)

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.")))
  )
)

Note: the default reloadDocument = FALSE is used here so that React Router handles navigation client-side without triggering a full page reload.


createMemoryRouter keeps the routing state entirely in memory. It does not read or modify the browser URL at all, which means:

  • It always starts at /, regardless of what the real URL is.
  • 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 see or share. 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.

createMemoryRouter works in Shiny apps, static HTML pages (file://), and Quarto output blocks.

library(reactRouter)
library(htmltools)

createMemoryRouter(
  createRoutesFromElements(
    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.


Legacy: HashRouter() and MemoryRouter() — component API

HashRouter and MemoryRouter are the older, component-based equivalents of createHashRouter and createMemoryRouter. They remain fully supported in this package and are safe to use in existing code.

However, RouterProvider() (or 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)

BrowserRouter 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://): On opening, the URL path is a file system path (e.g. /C:/Users/.../file.html), not /. No route matches, so 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. Additionally, client-side navigation with reloadDocument = FALSE means the Shiny server has no way to know the current route without extra JavaScript workarounds.
  • Refreshing on a sub-route: Even when served from a proper HTTP server, refreshing the browser on /about will 404 unless the server is configured to rewrite all routes to index.html.

BrowserRouter 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.


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