--- title: "Introduction to reactRouter" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to reactRouter} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ### Install ``` r #remotes::install_github("lgnbhl/reactRouter") # development version install.packages("reactRouter") ``` ## Minimal example ```{r setup} library(reactRouter) ``` You can add URL pages in Quarto document or R shiny like so: ```{r basic} library(reactRouter) HashRouter( NavLink(to = "/", "Main"), NavLink(to = "/analysis", "Analysis"), Routes( Route(path = "/", element = "Main content"), Route(path = "/analysis", element = "Analysis content") ) ) ``` ### Usage with Quarto As React Router provides client routing, you can easily create multiple routes in a Quarto and R markdown documents: ``` {r} # code to run in a Quarto document # example adapted from: https://github.com/remix-run/react-router/tree/dev/examples/basic library(reactRouter) library(htmltools) Layout <- div( # A "layout route" is a good place to put markup you want to # share across all the pages on your site, like navigation. tags$nav( tags$ul( tags$li( reactRouter::Link(to = "/", "Home") ), tags$li( reactRouter::Link(to = "/dashboard", "Dashboard") ), tags$li( reactRouter::Link(to = "/nothing-here", "Nothing Here") ) ) ), tags$hr(), # An renders whatever child route is currently active, # so you can think about this as a placeholder for # the child routes we defined above. reactRouter::Outlet() ) reactRouter::HashRouter( div( h1("Basic Example"), tags$p( paste0('This example demonstrates some of the core features of React Router including nested reactRouter::Route(), reactRouter::Outlet(), reactRouter::Link(), and using a "*" route (aka "splat route") to render a "not found" page when someone visits an unrecognized URL.' ) ), reactRouter::Routes( Route( path = "/", element = Layout, Route( index = TRUE, element = div( tags$h2("Home"), tags$p("Home content") ) ), Route( path = "dashboard", element = div( tags$h2("Dashboard"), tags$p("Dashboard here") ) ), # Using path="*"" means "match anything", so this route # acts like a catch-all for URLs that we don't have explicit # routes for. Route( path = "*", element = div( tags$h2("Nothing to see here!"), tags$p( Link(to = "/", "Go to the home page") ) ) ) ) ) ) ) ``` ### Usage with R Shiny Below a simple example using R Shiny: ```r library(reactRouter) library(shiny) library(DT) ui <- fluidPage( reactRouter::HashRouter( titlePanel("reactRouter"), sidebarLayout( sidebarPanel( reactRouter::NavLink( to = "/", style = JS('({isActive}) => { return isActive ? {color: "red", textDecoration:"none"} : {}; }'), "Home"), br(), reactRouter::NavLink( to = "/analysis", style = JS('({isActive}) => { return isActive ? {color: "red", textDecoration: "none"} : {}; }'), "Analysis" ), br(), reactRouter::NavLink( to = "/about", style = JS('({ isActive }) => { return isActive ? { color: "red", textDecoration: "none" } : {}; }'), "About" ) ), mainPanel( reactRouter::Routes( reactRouter::Route( path = "/", element = div( tags$h1("Home page"), tags$h4("A basic example of reactRouter."), uiOutput(outputId = "contentHome") ) ), reactRouter::Route( path = "/analysis", element = div( tags$h1("Analysis"), DT::DTOutput("table") ) ), reactRouter::Route( path = "/about", element = div( uiOutput(outputId = "contentAbout") ) ), reactRouter::Route(path = "*", element = div(tags$p("Error 404"))) ) ) ) ) ) server <- function(input, output, session) { output$contentHome <- renderUI({ p("Content home") }) output$table <- renderDT({ data.frame(x = c(1, 2), y = c(3, 4)) }) output$contentAbout <- renderUI({ div( tags$h1("About"), p("Content about") ) }) } shinyApp(ui, server) ``` ### Usage with MUI Material UI Below an example using MUI Material UI components from the **shinyMaterialUI** R package: ```r # remotes::install_github("lgnbhl/shinyMaterialUI") library(shiny) library(shinyMaterialUI) library(reactRouter) ui <- reactRouter::HashRouter( CssBaseline( Typography("reactRouter with shinyMaterialUI", variant = "h5", m = 2), Stack( direction = "row", spacing = 2, p = 2, Paper( MenuList( reactRouter::NavLink( to = "/", style = JS('({isActive}) => { return isActive ? {color: "red", textDecoration:"none"} : { textDecoration: "none" }; }'), MenuItem( "home" ) ), br(), reactRouter::NavLink( to = "/analysis", style = JS('({isActive}) => { return isActive ? {color: "red", textDecoration: "none"} : { textDecoration: "none" }; }'), MenuItem( "Analysis" ) ), br(), reactRouter::NavLink( to = "/about", style = JS('({ isActive }) => { return isActive ? { color: "red", textDecoration: "none" } : { textDecoration: "none" }; }'), MenuItem( "About" ) ) ) ), Box( reactRouter::Routes( reactRouter::Route( path = "/", element = div( tags$h1("Home page"), tags$h4("A basic example of reactRouter with shinyMaterialUI."), uiOutput(outputId = "contentHome") ) ), reactRouter::Route( path = "/analysis", element = div( tags$h1("Analysis"), uiOutput(outputId = "contentAnalysis") ) ), reactRouter::Route( path = "/about", element = uiOutput(outputId = "contentAbout") ), reactRouter::Route(path = "*", element = div(tags$p("Error 404"))) ) ) ) ) ) server <- function(input, output, session) { output$contentHome <- renderUI({ p("Content home") }) output$contentAnalysis <- renderUI({ p("Content analysis") }) output$contentAbout <- renderUI({ p("Content about") }) } shinyApp(ui, server) ``` ### Server side rendering issue If some pages have the same HTML structure, shiny will not update the content rendered in the server when clicking on a new page. Here an minimal example of the issue: ```r library(shiny) library(reactRouter) ui <- HashRouter( NavLink.shinyInput(inputId = "linkMain", to = "/", "Main"), br(), NavLink.shinyInput(inputId = "linkOther", to = "/other", "Other"), Routes( Route( path = "/", element = div(uiOutput(outputId = "contentMain")) ), Route( path = "/other", element = div(uiOutput(outputId = "contentOther")) ) ) ) server <- function(input, output, session) { output$contentMain <- renderUI( { p("Content home") } ) output$contentOther <- renderUI( { p("New content") }) } if (interactive()) { shinyApp(ui = ui, server = server) } ``` A simple workaround is to have a different HTML structure for each page. ```r library(shiny) library(reactRouter) ui <- HashRouter( NavLink.shinyInput(inputId = "linkMain", to = "/", "Main"), br(), NavLink.shinyInput(inputId = "linkOther", to = "/other", "Other"), Routes( Route( path = "/", element = div(uiOutput(outputId = "contentMain")) ), Route( path = "/other", element = div( # add title to have a different HTML structure h1("NOW VISIBLE"), uiOutput(outputId = "contentOther") ) ) ) ) server <- function(input, output, session) { output$contentMain <- renderUI( { p("Content home") } ) output$contentOther <- renderUI( { p("New content") }) } if (interactive()) { shinyApp(ui = ui, server = server) } ``` Another workaround is to reload the session when clicking on a page link using `NavLink.shinyInput()` instead of `NavLink()`. This is equivalent to clicking on the refresh button of your browser. This workaround has multiple downsides, in particular loose all current session objects when reloading the session. ```r library(shiny) library(reactRouter) ui <- HashRouter( NavLink.shinyInput(inputId = "linkMain", to = "/", "Main"), br(), NavLink.shinyInput(inputId = "linkOther", to = "/other", "Other"), Routes( Route( path = "/", element = div(uiOutput(outputId = "contentMain")) ), Route( path = "/other", element = div( uiOutput(outputId = "contentOther") ) ) ) ) server <- function(input, output, session) { # Reload session when user clicks on new page to refresh output content. # It is equivalent to click on the refresh button in the browser. observeEvent(c(input$linkMain, input$linkOther), { session$reload() }) output$contentMain <- renderUI( { p("Content home") } ) output$contentOther <- renderUI( { p("New content") }) } if (interactive()) { shinyApp(ui = ui, server = server) } ``` A third workaround is to render all the UI from the server. ```r library(shiny) library(reactRouter) ui <- uiOutput(outputId = "main") server <- function(input, output, session) { output$main <- renderUI({ HashRouter( NavLink.shinyInput(inputId = "linkMain", to = "/", "Main"), br(), NavLink.shinyInput(inputId = "linkOther", to = "/other", "Other"), Routes( Route( path = "/", element = p("Content home") ), Route( path = "/other", element = div( p("New content"), p(print(JS("location.reload()"))) ) ) ) ) }) } if (interactive()) { shinyApp(ui = ui, server = server) } ``` More examples of this issue are given [here](https://github.com/lgnbhl/reactRouter/issues/1). Please contact me if you have a solution to this issue. ### Usage with Shiny modules ```r # adapted from example of shiny.router # https://github.com/Appsilon/shiny.router/tree/main/examples/shiny_modules library(shiny) library(reactRouter) # This creates UI for each page. page <- function(title, content, id) { ns <- NS(id) div( titlePanel(title), p(content), textOutput(ns("click_me")) ) } # Both sample pages. root_page <- page("Home page", "Home page clicks", "root") second_page <- page("Second page", "2nd page clicks", "second") server_module <- function(id, clicks, power = 1) { moduleServer(id, function(input, output, session) { output$click_me <- renderText({ as.numeric(clicks())^power }) }) } # Create output for our router in main UI of Shiny app. ui <- reactRouter::HashRouter( NavLink.shinyInput(inputId = "linkMain", to = "/", "Main"), br(), NavLink.shinyInput(inputId = "linkOther", to = "/other", "Other"), actionButton("clicks", "Click me!"), Routes( Route( path = "/", element = div( root_page ) ), Route( path = "/other", element = div( second_page ) ) ) ) # Plug router into Shiny server. server <- function(input, output, session) { # reload the session to make the modules work # Not desired behavior as clicks object lost observeEvent(c(input$linkMain, input$linkOther), { session$reload() }) clicks <- reactive({ input$clicks }) server_module("root", clicks = clicks, power = 1) server_module("second", clicks = clicks, power = 2) } # Run server in a standard way. shinyApp(ui, server) ``` ### Alternatives - [shiny.router](https://appsilon.github.io/shiny.router/) implements a custom hash routing for shiny. - [brochure](https://github.com/ColinFay/brochure) provide a mechanism for creating natively multi-page shiny applications (but is still WIP). ### More information reactRouter implements React Router [v.6.30.0](https://reactrouter.com/6.30.0). More info about how to use React Router can be found in the [official website](https://reactrouter.com/6.30.0).