Skip to Content
Course content

139: Custom JavaScript Integration in Shiny

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact scenario play out a dozen times. You want your Shiny app to do something that R just isn't built for—like listening for a specific browser event or manipulating the DOM in real-time—so you write a bit of JavaScript. You put it in your UI, you call it from your server, and then... nothing happens. No error in the R console, but the app just sits there.

Take a look at this snippet. The goal here is to trigger an R message whenever the user resizes their browser window.

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  tags$script("
    window.onresize = function() {
      var width = window.innerWidth;
      console.log('Window resized to: ' + width);
      // I want R to know the width changed!
      resizeEvent = width; 
    };
  "),
  textOutput("resize_text")
)

server <- function(input, output) {
  observeEvent(input$resizeEvent, {
    output$resize_text <- renderText({
      paste("The JS told me the width is now:", input$resizeEvent)
    })
  })
}

shinyApp(ui, server)

The "Invisible" Variable Problem

If you run this, you'll see the width updating in your browser's developer console, but the R output will stay blank. Why? Because you've created a JavaScript variable called resizeEvent, but you haven't actually told Shiny to send that value across the network to the R session.

In a standard JS project, variables just exist in memory. But Shiny is a client-server architecture. The JavaScript is running in the user's browser (the client), and the R code is running on a server. They are separated by a network boundary. Simply assigning a value to a variable in JS doesn't magically teleport that value into the R input object.

Bridging the Gap with Shiny.setInputValue

To get data from the browser into R, you have to use a specific Shiny JavaScript API method: Shiny.setInputValue(). This function tells the browser to package the data and send it over the websocket to your R server, where it then appears as an entry in the input list.

Here is the fix. We replace the simple variable assignment with the Shiny API call:

library(shiny)
library(shinyjs)

ui <- fluidPage(
  useShinyjs(),
  tags$script("
    window.onresize = function() {
      var width = window.innerWidth;
      // This is the magic line:
      Shiny.setInputValue('resizeEvent', width);
    };
  "),
  textOutput("resize_text")
)

server <- function(input, output) {
  # Now input$resizeEvent actually exists!
  output$resize_text <- renderText({
    req(input$resizeEvent)
    paste("The JS told me the width is now:", input$resizeEvent)
  })
}

shinyApp(ui, server)

Now, whenever the window resizes, JS fires Shiny.setInputValue('resizeEvent', width). Shiny catches this on the server side, updates input$resizeEvent, and triggers any observeEvent or reactive expression depending on it. I added a req() in the server because input$resizeEvent is NULL until the first resize happens, and we don't want the app to throw an error on startup.

Managing Complexity with External JS Files

Writing JS inside tags$script() is fine for five lines of code, but once you start building actual features, your UI file becomes a nightmare to read. I highly recommend moving your JavaScript into a separate file.

Create a folder named www in your app directory (Shiny looks for this folder by default). Put your code in www/custom_logic.js. Then, in your UI, simply call it using tags$head(tags$script(src = "custom_logic.js")).

This doesn't just clean up the code; it allows you to use a proper IDE with JavaScript syntax highlighting and linting, which will save you from the "missing semicolon" headaches that are so common when embedding scripts in R strings.




📋 Practical Task

Exercise: Implementing a Custom JS "Copy to Clipboard" Button

In this exercise, you will integrate a custom JavaScript function that allows a user to copy a generated string from the Shiny UI to their system clipboard. Since R cannot access the user's system clipboard directly via the browser, you must bridge the two using JavaScript.

Requirements:

  • Create a Shiny app with a textInput where the user can type something.
  • Add an actionButton labeled "Copy to Clipboard".
  • Write a JavaScript function (either in tags$script or a www file) that uses the navigator.clipboard.writeText() API to copy the content of the text input.
  • The Twist: After the JavaScript successfully copies the text, it must use Shiny.setInputValue() to notify the R server that the copy operation was successful.
  • The R server should then display a temporary "Copied!" message in the UI using showNotification().
Rating
0 0

There are no comments for now.

to be the first to leave a comment.