Skip to contents

This page is specific to R. A tree view is a natural navigation for an app with many pages, like the sidebar of the MUI docs. The reactRouter package adds URL routing to Shiny apps, Quarto documents and static HTML pages. Combined with muiTreeView, it creates multi-page apps without a Shiny server: the demos of this page run in your browser.

install.packages("reactRouter")

Tree as sidebar navigation

Put a NavLink() in the label of each TreeItem(), and an Outlet() where the page should be shown. Each child Route() defines a page. MUI docs

# A link that fills the whole label, so that a click anywhere on it navigates
nav_link <- function(to, label) {
  NavLink(
    to = to,
    style = list(display = "block", color = "inherit", textDecoration = "none"),
    label
  )
}

page <- function(title, ...) {
  Box(Typography(title, variant = "h5", gutterBottom = TRUE), ...)
}

RouterProvider(
  router = createMemoryRouter(
    Route(
      path = "/",
      element = Paper(
        variant = "outlined",
        sx = list(display = "flex", minHeight = 260),
        Box(
          sx = list(width = 220, borderRight = 1, borderColor = "divider", p = 1),
          SimpleTreeView(
            defaultExpandedItems = list("components"),
            defaultSelectedItems = "home",
            TreeItem(itemId = "home", label = nav_link("/", "Home")),
            TreeItem(
              itemId = "components",
              label = "Components",
              TreeItem(itemId = "simple", label = nav_link("/simple-tree-view", "Simple Tree View")),
              TreeItem(itemId = "rich", label = nav_link("/rich-tree-view", "Rich Tree View"))
            ),
            TreeItem(itemId = "about", label = nav_link("/about", "About"))
          )
        ),
        Box(sx = list(p = 2, flexGrow = 1), Outlet())
      ),
      Route(
        index = TRUE,
        element = page("Home", Typography("Pick a page in the tree."))
      ),
      Route(
        path = "simple-tree-view",
        element = page("Simple Tree View", Typography("Items are TreeItem() children."))
      ),
      Route(
        path = "rich-tree-view",
        element = page("Rich Tree View", Typography("Items are data, built with asMuiTree()."))
      ),
      Route(
        path = "about",
        element = page("About", Typography("muiTreeView wraps MUI X Tree View for R."))
      )
    )
  )
)

On this page, createMemoryRouter() keeps the current page in memory, so that several demos can live on the same page. In an app, use createHashRouter() instead: the URL then shows the current page (e.g. /#/about), which users can bookmark and share. See Router selection in the reactRouter docs.

Nested routes and dynamic segments

A folder of the tree can match a route with a dynamic segment, such as docs/:file: one route displays all the files, and useParams() reads the file name from the URL.

RouterProvider(
  router = createMemoryRouter(
    Route(
      path = "/",
      element = Paper(
        variant = "outlined",
        sx = list(display = "flex", minHeight = 200),
        Box(
          sx = list(width = 220, borderRight = 1, borderColor = "divider", p = 1),
          SimpleTreeView(
            defaultExpandedItems = list("docs"),
            TreeItem(
              itemId = "docs",
              label = "docs",
              TreeItem(itemId = "readme", label = nav_link("/docs/README.md", "README.md")),
              TreeItem(itemId = "news", label = nav_link("/docs/NEWS.md", "NEWS.md")),
              TreeItem(itemId = "license", label = nav_link("/docs/LICENSE.md", "LICENSE.md"))
            )
          )
        ),
        Box(sx = list(p = 2, flexGrow = 1), Outlet())
      ),
      Route(index = TRUE, element = Typography("Open a file.")),
      Route(
        path = "docs/:file",
        element = Typography("You opened ", useParams(tags$code(), selector = "file"))
      )
    )
  )
)

Pages from a data frame

The navigation can also be built from a data frame, with one page per row. Here, one reference page per function of the muiMaterial, muiCharts and reactRouter packages, grouped by package: the tree is built with split() and lapply(), as in Simple Tree View - Items, and the route reference/:package/:fn shows the row of the selected function. The data frame is given to the route loader as JSON with jsonlite::toJSON().

fns <- rbind(
  data.frame(package = "muiMaterial", group = "Layout", fn = c("Box", "Stack", "Grid")),
  data.frame(package = "muiMaterial", group = "Inputs", fn = c("Button", "TextField", "Slider", "Switch")),
  data.frame(package = "muiMaterial", group = "Navigation", fn = c("Link", "Tabs", "Drawer")),
  data.frame(package = "muiCharts", group = "Charts", fn = c("BarChart", "LineChart", "PieChart", "Gauge")),
  data.frame(package = "muiCharts", group = "Building blocks", fn = c("ChartsContainer", "ChartsXAxis", "ChartsLegend")),
  data.frame(package = "reactRouter", group = "Routes", fn = c("RouterProvider", "Route", "Outlet")),
  data.frame(package = "reactRouter", group = "Navigation", fn = c("Link", "NavLink", "Navigate")),
  data.frame(package = "reactRouter", group = "Hooks", fn = c("useParams", "useLoaderData", "useNavigate"))
)
fns$call <- paste0(fns$package, "::", fns$fn, "()")
head(fns)
#>       package  group        fn                     call
#> 1 muiMaterial Layout       Box       muiMaterial::Box()
#> 2 muiMaterial Layout     Stack     muiMaterial::Stack()
#> 3 muiMaterial Layout      Grid      muiMaterial::Grid()
#> 4 muiMaterial Inputs    Button    muiMaterial::Button()
#> 5 muiMaterial Inputs TextField muiMaterial::TextField()
#> 6 muiMaterial Inputs    Slider    muiMaterial::Slider()

fns_by_package <- split(fns, fns$package)

fn_tree <- SimpleTreeView(
  lapply(names(fns_by_package), function(package) {
    rows <- fns_by_package[[package]]
    TreeItem(
      itemId = package,
      label = package,
      lapply(seq_len(nrow(rows)), function(i) {
        TreeItem(
          itemId = paste0(package, "::", rows$fn[i]),
          label = nav_link(paste0("/reference/", package, "/", rows$fn[i]), rows$fn[i])
        )
      })
    )
  })
)

RouterProvider(
  router = createMemoryRouter(
    Route(
      path = "/",
      element = Paper(
        variant = "outlined",
        sx = list(display = "flex", height = 320),
        Box(
          sx = list(width = 220, borderRight = 1, borderColor = "divider", p = 1, overflowY = "auto"),
          fn_tree
        ),
        Box(sx = list(p = 2, flexGrow = 1), Outlet())
      ),
      Route(index = TRUE, element = Typography("Pick a function in the tree.")),
      Route(
        path = "reference/:package/:fn",
        loader = JS(sprintf(
          "({ params }) => %s.find((row) => row.package === params.package && row.fn === params.fn)",
          jsonlite::toJSON(fns, dataframe = "rows")
        )),
        element = Box(
          useLoaderData(Typography(variant = "h5", component = "code"), selector = "call"),
          Typography("Package: ", useLoaderData(tags$b(), selector = "package")),
          Typography("Group: ", useLoaderData(tags$b(), selector = "group"))
        )
      )
    )
  )
)

Link exists in muiMaterial and in reactRouter: the route matches both the package and the function name, so each one gets its own page.

Rich Tree View items from a loader

A route loader can also provide the items of a RichTreeView(), with useLoaderData(as = "items"). Here the items are created in R with asMuiTree(), but the loader could also fetch() them from an API. See Data loader in the reactRouter docs.

fns$fn_id <- paste0(fns$package, "::", fns$fn)
fn_items <- asMuiTree(fns, levels = c("package", "fn"), levels_id = c("package", "fn_id"))

RouterProvider(
  router = createMemoryRouter(
    Route(
      path = "/",
      loader = JS(sprintf(
        "async () => %s",
        jsonlite::toJSON(fn_items, auto_unbox = TRUE)
      )),
      hydrateFallbackElement = Typography("Loading..."),
      element = useLoaderData(
        RichTreeView(checkboxSelection = TRUE, multiSelect = TRUE),
        as = "items"
      )
    )
  )
)

With Shiny

reactRouter also works in Shiny apps: the routes render uiOutput()s or any other component, such as a muiCharts chart, and session$clientData$url_hash gives the current page. Use createHashRouter(). See the reactRouter docs for more.

library(shiny)
library(muiTreeView)
library(muiCharts)
library(reactRouter)

nav_link <- function(to, label) {
  NavLink(
    to = to,
    style = list(display = "block", color = "inherit", textDecoration = "none"),
    label
  )
}

ui <- RouterProvider(
  router = createHashRouter(
    Route(
      path = "/",
      element = div(
        style = "display: flex; gap: 16px;",
        SimpleTreeView(
          TreeItem(itemId = "home", label = nav_link("/", "Home")),
          TreeItem(itemId = "chart", label = nav_link("/chart", "Chart"))
        ),
        div(style = "flex: 1;", Outlet())
      ),
      Route(index = TRUE, element = uiOutput("home")),
      Route(
        path = "chart",
        element = BarChart(
          dataset = head(starwars_people, 4),
          xAxis = list(list(scaleType = "band", dataKey = "name")),
          series = list(list(dataKey = "height", label = "Height")),
          width = 500,
          height = 300
        )
      )
    )
  )
)

server <- function(input, output, session) {
  output$home <- renderUI(p("Current page: ", session$clientData$url_hash))
}

shinyApp(ui, server)