Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add call back when renderUI is done #3348

Open
lz100 opened this issue Mar 25, 2021 · 18 comments
Open

add call back when renderUI is done #3348

lz100 opened this issue Mar 25, 2021 · 18 comments

Comments

@lz100
Copy link

lz100 commented Mar 25, 2021

If we use renderUI to load some large UI and call the rendered UI associated server functions after the renderUI, it will likely cause problems because the UI is just sent to the client and input-binding registration is still ongoing.

Simple example:

ui <- fluidPage(
    actionButton("a", "add UI"),
    uiOutput("ui_out")
)

loadserver <- function(input, output, session){
    if(input$n1000 > 0) print(1)
    #### many other things
}

server <- function(input, output, session) {
    observeEvent(input$a, {
        output$ui_out <- renderUI({
            lapply(1:1000, function(i) {
                numericInput(paste0("n", i), paste0("n", i), value = 0)
            })
        })
        loadserver(input, output, session)
    })
}

shinyApp(ui, server)

I know I can add req to prevent the error, but please don't struggle with this. Just imagine loadserver comes from a package and you can't do anything in the function. The normal preloading has no problem (load the required UI and server on start), but only causes problems with this dynamic loading. So, let's just say I want to wait until the UI loading and input-binding is complete and do something in the server.

Can we have some options:

  1. Make renderUI sync ("blocking") before executing the next line. Now it is more like async, it only sends out the UI as a message and goes on to the next line. It doesn't know if the input-binding is complete or the expected inputs are registered in the current shiny session. Add an option like renderUI(wait = TRUE)
  2. I tried to watch for "shiny:value" event on js, but it only tells me the client has received the new UI, but still, it doesn't tell me the loading is complete or shiny session has all the new inputs registered. If we could have all inputs to be registered in "shiny:value", I can watch for them.
//shiny:value
 {
  toBeBound : [id1, id2, ...],
  ...
};

var binds;
var bound = 0;
$(document).on('shiny:value', function(e) {
    binds = e.toBeBound;
});

$(document).on('shiny:bound', function(e) {
    if (binds.includes(e.target.id)) bound ++
});

setInterval(function() {
  if (bound == binds.length) {
    // load server
  }
}, 1000);
@daattali
Copy link
Contributor

Regarding 1: I'm not associated with the shiny team so this isn't official, but from my understanding it's not possible to have shiny <--> javascript code be synchronous. It would be great if that was technically possible, I myself also would have many uses for that. For eample I have packages that communicate with javascript, but it's impossible to ask javascript for a value and retrieve it in the next line as a blocking request, the only solution AFAIK is to tell javascript to calculate a value and it will send it back to shiny as an input that I need to listen to, which is essentially a callback.

@lz100
Copy link
Author

lz100 commented Mar 29, 2021

Thanks Dean, I understand this part. You see in my second point, I am not asking the js to be synchronous, but telling me all the events that will happen and I can write my own code to watch and wait for these events. There are already callbacks like "shiny:value" or "shiny:bound". I would like to know how many "shiny:bound" will be triggered and their IDs by a "shiny:value" event, more specifically, in this case, triggered by renderUI event.

@jcheng5
Copy link
Member

jcheng5 commented Mar 29, 2021

You can add freezeReactiveValue to your renderUI:

        output$ui_out <- renderUI({
            lapply(1:1000, function(i) {
                freezeReactiveValue(input, paste0("n", i))
                numericInput(paste0("n", i), paste0("n", i), value = 0)
            })
        })

This will cause any attempt to read input$nXXXX to basically req(FALSE) until after the UI has had a chance to init those inputs.

@lz100
Copy link
Author

lz100 commented Mar 29, 2021

Thanks for the suggestion @jcheng5 . Imagine we are designing an app loads UI and server from users as modules dynamically (by clicking the button), so I have no control what code is in the UI or server module that the user provides. I can't predict what input they will load, then I can't help them to fix the code by req or freezeReactiveValue

I know there are many ways to prevent the error. That's just an example to illustrate that we have the need to wait for the UI to finish loading. Please do not struggle on how to fix the error above. The point is adding a callback when renderUI finishes or telling us the child events to listen to will be very helpful on many applications, both on Shiny server end and client javascript downstream processing.

Here is how far I go: when renderUI sends the message to client, it triggers the ''shiny:value'', I can listen to it with no problem. However, when the actual UI is loaded to the DOM, shinyjs starts to register the inputs from the new UI, it triggers many 'shiny:bound' event. Here, I don't know how many events there are and when the process will end. My temp solution is wait 1s before loading the server and hope everything is done within 1s, which is not an ideal solution. It will be very helpful if we can know: how many the child events ('shiny:bound') are and some sort of identifier of these events that will be triggered directly from the parent event ("shiny:value"). Or somehow triggers a signal on the server when everything is done.

@jcheng5
Copy link
Member

jcheng5 commented Mar 31, 2021

Oh, I understand what you're doing now. OK, that's a good question.

This is the first thing that came to mind, it's not easy but it does work--as long as the UI that's being loaded actually contains an input. Basically this code schedules expr to be evaluated the next time input is set.

execute_at_next_input <- function(expr, session = getDefaultReactiveDomain()) {
  observeEvent(once = TRUE, reactiveValuesToList(session$input), {
    force(expr)
  }, ignoreInit = TRUE)
}

@jcheng5
Copy link
Member

jcheng5 commented Mar 31, 2021

Actually... if you're already writing JavaScript, you could use session$onFlushed() to schedule a shinyjs call that updates an input on the server. And have an observeEvent(once=TRUE) waiting for that input. I think the scheduled shinyjs call should be guaranteed to happen after the new UI has been bound.

In terms of potential changes to Shiny, it does seem like there's something missing here; we have the notion of server busy/idle state, but not on the client.

@lz100
Copy link
Author

lz100 commented Mar 31, 2021

@jcheng5 Thanks a lot. The first method worked!

execute_at_next_input <- function(expr, session = getDefaultReactiveDomain()) {
    observeEvent(once = TRUE, reactiveValuesToList(session$input), {
        print(reactiveValuesToList(session$input))
        force(expr)
    }, ignoreInit = TRUE)
}

ui <- fluidPage(
    actionButton("a", "add UI"),
    uiOutput("ui_out")
)

loadserver <- function(input, output, session){
    if(input$n10 >= 0) print(1)
    #### many other things
}

server <- function(input, output, session) {
    observeEvent(input$a, {
        output$ui_out <- renderUI({
            lapply(1:10, function(i) {
                numericInput(paste0("n", i), paste0("n", i), value = 0)
            })
        })
        execute_at_next_input(loadserver(input, output, session))
    })

}

shinyApp(ui, server)

It actually waits for not only a single one but all the inputs in the next group to be set.
The results show the input binding works async manner? but it doesn't matter, it waits for all of them to be set:

$n9
[1] 0

$n4
[1] 0

$n3
[1] 0

$n7
[1] 0

$n8
[1] 0

$n5
[1] 0

$n1
[1] 0

$n6
[1] 0

$a
[1] 1
attr(,"class")
[1] "integer"                "shinyActionButtonValue"

$n2
[1] 0

$n10
[1] 0

I tested with a much bigger dynamically loaded app and it worked fine. And I can use sendCustomMessage to
let the client know with no problem.

Could this be officially supported? Like I proposed, maybe add a wait argument to renderUI and in the end of the rendering event, add this execute_at_next_input to wait for input binding to be finished?


Not so much luck with the second method:

ui <- fluidPage(
    actionButton("a", "add UI"),
    uiOutput("ui_out")
)

loadserver <- function(input, output, session){
    if(input$n10 >= 0) print(1)
    #### many other things
}

server <- function(input, output, session) {
    observeEvent(input$a, {
        output$ui_out <- renderUI({
            lapply(1:10, function(i) {
                numericInput(paste0("n", i), paste0("n", i), value = 0)
            })
        })
        session$onFlushed( function() {
            message("End flush")
            observeEvent(1, once = TRUE, {
                print(reactiveValuesToList(session$input))
                loadserver(input, output, session)
            })
            
        }, once = TRUE)
    }, ignoreInit = TRUE)
    
}

shinyApp(ui, server)
$a
[1] 1
attr(,"class")
[1] "integer"                "shinyActionButtonValue"

Warning: Error in if: argument is of length zero
  [No stack trace available]

It immediately flushed. It doesn't wait for everything to finish inside renderUI, or did I misunderstand something?

@jcheng5
Copy link
Member

jcheng5 commented Apr 1, 2021

Could this be officially supported? Like I proposed, maybe add a wait argument to renderUI and in the end of the rendering event, add this execute_at_next_input to wait for input binding to be finished?

We can't do something exactly like that--it's not possible to add a wait argument to renderUI, because we don't allow blocking across reactive flushes. That's analogous to asking for a JavaScript call to block until the next tick. We could add execute_at_next_input, but I find that to be pretty niche.

Although I would maybe find it somewhat compelling to have something beyond session$onFlushed; like where onFlushed says "execute this right after outputs are sent to the browser", we could have session$onCurrentFlushProcessedByClient (not a serious naming proposal) that gets invoked when the current reactive cycle's flushed output is done being processed by the browser.

@lz100
Copy link
Author

lz100 commented Apr 1, 2021

I see, anything like so will be nice. Feel free to close the issue. I can just use this hack for now.

@reisner
Copy link

reisner commented Sep 13, 2021

+1 to this being useful. I used it to detect when a renderUI had finished generating an input, and set the focus on that element.

@ismirsehregal
Copy link
Contributor

A related issue and a PR (dealing with a modal instead of renderUI).

@filipakkad
Copy link

I came up with an alternative solution that might be helpful.

So you could introduce a JS-based callback function that would be run at the end of the renderUI. In other words - the last element of the rendered tagList would be the callback, which is just inline JS command run when rendered in the DOM:

library(glue)

#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
  tags$script(
    glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
  )
}

See your modified example:

library(shiny)
library(glue)


#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
  tags$script(
    glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
  )
}

ui <- fluidPage(
  actionButton("a", "add UI"),
  uiOutput("ui_out")
)

loadserver <- function(input, output, session){
  if(input$n1000 >= 0) print(1)
  #### many other things
}

server <- function(input, output, session) {
  observeEvent(input$a, {
    output$ui_out <- renderUI({
      #' Wrap whole output with `tagList`
      tagList(
        lapply(1:1000, function(i) {
          numericInput(paste0("n", i), paste0("n", i), value = 0)
        }),
        #' Inform server that the UI has been already rendered
        rendered_js_callback_ui(input_id = "my_input_for_catching_render")
      )
    })
  })
  
  observeEvent(input$my_input_for_catching_render, {
    loadserver(input, output, session)
  }, ignoreInit = TRUE)
}

shinyApp(ui, server)

Also, it is important to note that the callback function is not going to work with insertUI (meaning the JS command won't be executed) - see example:

library(shiny)
library(glue)


#' Execute JS code once rendered in DOM
rendered_js_callback_ui <- function(input_id, input_value = "Date.now().toString()") {
  tags$script(
    glue_safe("Shiny.setInputValue(\"{input_id}\", {input_value})")
  )
}

ui <- fluidPage(
  actionButton("a", "add UI"),
  uiOutput("ui_out")
)

loadserver <- function(input, output, session){
  if(input$n1000 >= 0) print(1)
  #### many other things
}

server <- function(input, output, session) {
  
  observeEvent(input$a, {
    #' `insertUI` instead of `renderUI`
    insertUI(
      session = session,
      selector = "#ui_out",
      ui = tagList(
        lapply(1:1000, function(i) {
          numericInput(paste0("n", i), paste0("n", i), value = 0)
        }),
        #' Inform server that the UI has been already rendered - it won't work here
        rendered_js_callback_ui(input_id = "my_input_for_catching_render")
      )
    )
  })
  
  observeEvent(input$my_input_for_catching_render, {
    loadserver(input, output, session)
  }, ignoreInit = TRUE)
}

shinyApp(ui, server)

@lz100
Copy link
Author

lz100 commented Nov 3, 2021

good angle of thoughts.

In Joe's solution users only needs to call the function and define the expression inside, but it's observing all inputs and waiting for the next input value to set. When there are a lot of inputs, this can become an expensive function.

filpro's solution only needs to take care of one input, but users need to know advanced knowledge of JS-Shiny-R communication and need to set a new input for every render event. It doesn't feel very user-friendly.

@jcheng5 Since this has drawn quite some attention, do we have any official plan for a clean and easy way to support this feature?

@lz100
Copy link
Author

lz100 commented Jan 20, 2022

Since I have seen these issue quite a few times on StackOverflow 1, 2 myself, I can imagine there are more similar issues that I didn't see. Instead of waiting for the official fix, I wrap this temp fix it into a function in my package spsComps. The function is called onNextInput.

Not on CRAN yet, so you need to install the dev version remotes::install_github("lz100/spsComps").

It works as an on.exit callback to your reactive event and watch for the next input change. Try these examples

library(spsComps)

# Simple example
ui <- fluidPage(
  uiOutput("someui")
)
server <- function(input, output, session) {
  output$someui <- renderUI({
    # we update the text of new rendered text input to 3 random letters
    # after `textInput` is displayed, and it only works for one time.
    onNextInput({
      updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
    })
    textInput("mytext", "some text")
  })
  # if you directly have update event like following line, it won't work
  # updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
}
shinyApp(ui, server)


# complex example with modules
modUI <- function(id) {
  ns <- NS(id)
  textInput(ns("mytext"), "some text")
}
modServer = function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      updateTextInput(inputId = "mytext", value = paste0(sample(letters, 3), collapse = ""))
    }
  )
}
ui = fluidPage(
  actionButton("a", "load module UI"),
  uiOutput("mod_container")
)
server = function(input, output, session) {
  # everytime you click, render a new module UI and update the text value
  # immediately
  observeEvent(input$a, {
    output$mod_container <- renderUI({
      onNextInput(modServer("mod"))
      modUI("mod")
    })
  })
  # Without `onNextInput`, module server call will not work
  # uncomment below and, comment `onNextInput` line to see the difference
  # modServer("mod")
}

shinyApp(ui, server)

@kramerrs
Copy link

I am having this problem also. I add a number of selectInputs in a renderUI. I get a double render of a datatable, One of which can be quite time consuming when all the inputs default to null.

One thing I have tried, is to use req on input$my_select_input. Unfortunately, the truthiness is a problem the selectinput defaults to null when nothing is selected.

Another solution I have tried is to add a selectInput with a default value, then hide it, However, renderUI doesn't seem to respect any hidden() functionality or hide(). Right now I am just leaving a disabled action button on the UI actionButton("shiny sucks>") not exactly, but you get the idea. Why is renderUI so difficult to work with?

Other things I have tried is to take an isolated look at input[[]] eg req(my_select_input %in% isolate(names(input))) to see if the variable is in the list of variables. This seems like it should work, but it causes double rendering anyway. Why does renderUI have so many rough edges?

@MichalLauer
Copy link

Hello,

Additionally, I found a pretty easy solution without any JS code using a global variable that indicates when something should be run after flushing. It should be doing the same stuff described above but with less code :)

server <- function(input, output, session) {

  redrawing <- FALSE
  output$show<- renderUI({
    redrawing <<- TRUE
  
    # render stuff...
  })
    
  session$onFlushed(function() {
    if (redrawing) {
      # do stuff after UI has been rendered
      redrawing <<- FALSE
    }
  }, once = F)
}

@bryce-carson
Copy link

Hello,

Additionally, I found a pretty easy solution without any JS code using a global variable that indicates when something should be run after flushing. It should be doing the same stuff described above but with less code :)

server <- function(input, output, session) {

redrawing <- FALSE
output$show<- renderUI({
redrawing <<- TRUE

# render stuff...

})

session$onFlushed(function() {
if (redrawing) {
# do stuff after UI has been rendered
redrawing <<- FALSE
}
}, once = F)
}

This worked flawlessly for my simple case (use some jQuery to add font-style: italic to a single table row), and it was very understandable.

If you're unfortunate enough to find yourself here, you're in good fortune now. This worked easily.

@ismirsehregal
Copy link
Contributor

The same can be done using reactiveVal or reactiveValues instead of a global variable:

library(shiny)

ui <- fluidPage(
  uiOutput("show"),
  textOutput("rendered_timestamp"),
  actionButton("draw", "Draw UI")
)

server <- function(input, output, session) {
  rv <- reactiveValues(redrawing = FALSE, rendered_ts = "")
  
  output$show <- renderUI({
    rv$redrawing <- TRUE
    click_time <- Sys.time()
    Sys.sleep(3)
    # render stuff...
    div("Clicked:", click_time)
  }) |> bindEvent(input$draw)
  
  session$onFlushed(function() {
    if (isolate(rv$redrawing)) {
      # do stuff after UI has been rendered
      print("output$show was rendered")
      rv$rendered_ts <- paste("Rendered:", Sys.time())
      rv$redrawing <- FALSE
    }
  }, once = FALSE)
  
  output$rendered_timestamp <- renderText({rv$rendered_ts})
}

shinyApp(ui, server)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants