Appsilon / shiny.router

A minimalistic router for your Shiny apps.
http://appsilon.github.io/shiny.router
Other
256 stars 31 forks source link

Question: How to use shiny.router with moduleServer? #91

Open zilch42 opened 2 years ago

zilch42 commented 2 years ago

hi there,

I have a reasonably large app using shiny.fluent and shiny.router (thanks for these packages they are really great), with a structure similar to the code below:

library(shiny)
library(shiny.fluent)
library(shiny.router)

# navigation pane objects
navigation_styles <- list(
  root = list(height = "100%", width = "30%", boxSizing = "border-box", 
              border = "1px solid #eee", overflowY = "auto"))

link_groups <- list(
  list(
    links = list(
      list(name = 'Home', url = '#!/', key = 'home'),
      list(name = "Documents", key = "documents", url = '#!/documents'),
      list(name = "Pages", key = "pages", url = '#!/pages')
    )))

# page UIs
home_page_ui <- tagList(p("Homepage"))

documents_page_ui <- function(id){
  ns <- NS(id)
  tagList(textOutput(ns("title")))
}

pages_page_ui <- function(id){
  ns <- NS(id)
  tagList(textOutput(ns("title")))
}

# page servers
documents_page_server <- function(id = "documents1"){
  moduleServer( id, function(input, output, session){
    ns <- session$ns

    output$title <- renderText("Documents Page")
  })
}

pages_page_server <- function(id = "pages1"){
  moduleServer( id, function(input, output, session){
    ns <- session$ns

    output$title <- renderText("Pages Page")
  })
}

# Creates router. We provide routing path, a UI as
# well as a server-side callback for each page.
router <- make_router(
  route("/", home_page_ui),
  route("documents", documents_page_ui("documents1")),
  route("pages", pages_page_ui("pages1"))
)

shinyApp(
  ui = fluidPage(
    Nav(
      groups = link_groups,
      selectedKey = "home",
      styles = navigation_styles
    ), 
    fluidPage(
      router$ui
    )
  ),

  server = function(input, output, session) {
    router$server(input, output, session)

    # page servers
    documents_page_server("documents1")
    pages_page_server("pages1")
  }
)

Each of my pages are inside modules because they are quite similar and it helps me avoid namespace conflicts. Notice that my page servers are just in the main server function, not in the router.

This works okay, but suffers from the issue of every page reloading when something changes, not just the current page ( #70). From what I understand from reading other issues, this issue has been fixed in the current version of shiny.router, but requires calling the page server from the router - ie. using route(path, ui, server = ACTUAL_SERVER_FUNCTION). Is that correct?

I can only get that to work if the pages are not modules that use moduleServer(). Is there any way to get route to work with modules? And is there any way to pass parameters (e.g. a set of reactive values) to those server modules if it does work?

Cheers

jakubsob commented 2 years ago

Hello @zilch42, thanks for reaching out!

If I recall correctly there's no documentation on how to use shiny.router with modules, but please take a look at this modification of your example:

library(shiny)
library(shiny.fluent)
library(shiny.router)

# navigation pane objects
navigation_styles <- list(
  root = list(height = "100%", width = "30%", boxSizing = "border-box", 
              border = "1px solid #eee", overflowY = "auto"))

link_groups <- list(
  list(
    links = list(
      list(name = 'Home', url = '#!/', key = 'home'),
      list(name = "Documents", key = "documents", url = '#!/documents'),
      list(name = "Pages", key = "pages", url = '#!/pages')
    )))

# page UIs
home_page_ui <- tagList(p("Homepage"))

documents_page_ui <- function(id){
  ns <- NS(id)
  tagList(textOutput(ns("title")))
}

pages_page_ui <- function(id){
  ns <- NS(id)
  tagList(textOutput(ns("title")))
}

# page servers
documents_page_server <- function(documents_id = "documents1", documents_text = reactive("placeholder")){
  moduleServer(documents_id, function(input, output, session){
    ns <- session$ns

    output$title <- renderText(documents_text())
  })
}

pages_page_server <- function(pages_id = "pages1", pages_text = reactive("placeholder")){
  moduleServer(pages_id, function(input, output, session){
    ns <- session$ns

    output$title <- renderText(pages_text())
  })
}

# Creates router. We provide routing path, a UI as
# well as a server-side callback for each page.
router <- make_router(
  route("/", home_page_ui),
  route("documents", documents_page_ui("documents1"), documents_page_server),
  route("pages", pages_page_ui("pages1"), pages_page_server)
)

shinyApp(
  ui = fluidPage(
    Nav(
      groups = link_groups,
      selectedKey = "home",
      styles = navigation_styles
    ), 
    fluidPage(
      router$ui
    )
  ),

  server = function(input, output, session) {
    router$server(
      input,
      output,
      session,
      pages_id = "pages1",
      documents_id = "documents1",
      pages_text = reactive("Pages Page"),
      documents_text = reactive("Documents Page")
    )
  }
)

In router$server function code there's this line:

lapply(routes, function(route) route$server(input, output, 
        session, ...))

which hints us that we can use ellipsis to pass arguments to server functions. We just need to make those arguments names unique, that's why I've used documents_id and pages_id instead of usual id.

Does this example help you solve your issue?

zilch42 commented 2 years ago

Thanks @jakubsob. I've applied that method to my app and it works so that's great. I haven't noticed any change in behaviour though. The app still seems to load all pages initially not just the page being viewed, and if I change a reactive value (eg a data filter) that affects other pages they all recalculate at once regardless of which page is currently being viewed. Is that currently still expected behaviour? That's what I was hoping this method would solve

FMKerckhof commented 2 years ago

It may be a while after the creation of this issue but I would like to second it. Is shiny.router still supported/developed? Appsilon/rhino for example proposes the use of moduleServer and modules as well but doesn't mention shiny.router.

jakubsob commented 2 years ago

Hi @FMKerckhof, shiny.router is passively maintained now. This means that we don't expect to add any new features soon. shiny.router is a package for routing - you can create an app that uses routing or not depending on your requirements. It's an optional feature and rhino doesn't encourage you nor it disencourages you from using it.

We'll try to reinvestigate this issue and come back with a solution

mviertel commented 2 years ago

Hi @jakubsob ,

Thanks for your clarifications especially with respect to feature development.

In the package description you do however mention to make it possible tocustomize loading full session or just visible part in the future. That is a big topic I am struggling with because I usually create dashboards that do have database queries but also use global variables. So, these change the full session and all the queries get refreshed. You guys have a solution for that in your backpack?

Best Michael

fernandoroa commented 2 years ago

Related (modules rhino router): https://github.com/Appsilon/NLPShiny/blob/main/app/layouts/pages.R check out corresponding modules on view folder, related: https://stackoverflow.com/questions/71753556/how-to-make-conditionalpanel-work-with-box-with-rhino

without rhino: https://github.com/Appsilon/possible_example