vue-r / vueR

vue.js for R
https://vue-r.github.io/vueR
Other
140 stars 14 forks source link

vue3 reactive input values #6

Open timelyportfolio opened 4 years ago

timelyportfolio commented 4 years ago

I have always wondered with both vue or mobx + Shiny how our workflows/architecture might change if Shiny JavaScript state in Shiny.shinyapp.$inputValues was reactive instead of a plain object. In earlier versions of JavaScript without proxy, this idea is very limited in potential usage since added and deleted object properties are not tracked. However, with proxy and the newest versions of mobx and vue, we can track added or effectively replace Shiny.shinyapp.$inputValues with a reactive version early in the session llife and reap the full benefits of JavaScript reactivity fairly cleanly.

Questions

  1. I have not tested real-life usage with large complex apps, but in theory it seems there is no impact. Am I missing something fundamental that would prove this idea is not as feasible as it seems.
  2. I doubt Shiny proper would ever pursue a reactive JS state since it would have to choose which reactive state engine it would use. Is there potential for Shiny to make the choice based on active community and well-tested, stable JS dependencies for Shiny to have a reactive JS engine as its input state? Which library would most likely meet the requirements? I would think choosing an existing library would be better than developing one from scratch.
  3. I feel like the concept of reactivity is quickly engrained in most Shiny developers and the reactivity concepts would translate easily to JavaScript. What would get in the way of quick understanding for R Shiny developers? What might be intimidating or difficult?
  4. How might we best communicate the improvement from incorporating reactive JS input state?
  5. What tools/tooling could we provide to ease the integration?

Code

library(htmltools)
library(vueR)
library(shiny)

# experiment with standalone vue reactivity in bare page
#   reference:
#     https://vuejs.org/v2/guide/reactivity.html
#     https://dev.to/jinjiang/understanding-reactivity-in-vue-3-0-1jni
browsable(
  tagList(
    tags$head(
      tags$script(src = "https://unpkg.com/@vue/reactivity@3.0.0-rc.5/dist/reactivity.global.js"),
    ),
    tags$p("we should see a number starting at 0 and increasing by one each second"),
    tags$div(id = "reporter"),
    tags$script(HTML(
"
let data = {x: 0};
let data_reactive = VueReactivity.reactive(data)  // could also use ref for primitive value
console.log(data, data_reactive)

VueReactivity.effect(() => {
  console.log(data_reactive.x)
  document.getElementById('reporter').innerText = data_reactive.x
})
setInterval(function() {data_reactive.x++}, 1000)

"
    ))
  )
)

# experiment with Shiny inputValues and vue-next
#   reference:
#     https://vuejs.org/v2/guide/reactivity.html
#     https://dev.to/jinjiang/understanding-reactivity-in-vue-3-0-1jni
ui <- tagList(
  tags$head(
    tags$script(src = "https://unpkg.com/@vue/reactivity@3.0.0-rc.5/dist/reactivity.global.js"),
  ),
  tags$div(
    tags$h3("Increment with JavaScript"),
    tags$span("Shiny: "),
    textOutput("reporterR", inline = TRUE),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS"
    )
  ),
  tags$div(
    tags$h3("Increment with R/Shiny"),
    tags$span("Shiny (used numeric input for convenience): "),
    numericInput(inputId = 'x2', label = "", value = 0),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS2"
    )
  ),
  tags$script(HTML(
"
$(document).on('shiny:connected', function() {

  // once Shiny connected replace Shiny inputValues with reactive Shiny inputValues
  Shiny.shinyapp.$inputValues = VueReactivity.reactive(Shiny.shinyapp.$inputValues)

  // do our counter using Shiny.setInputValue from JavaScript
  Shiny.setInputValue('x', 0) // initialize with 0
  VueReactivity.effect(() => {
    console.log('javascript', Shiny.shinyapp.$inputValues.x)
    document.getElementById('reporterJS').innerText = Shiny.shinyapp.$inputValues.x
  })
  setInterval(
    function() {
      Shiny.setInputValue('x', Shiny.shinyapp.$inputValues.x + 1) //increment by 1
    },
    1000
  )

  // react to counter implemented in Shiny
  VueReactivity.effect(() => {
    console.log('shiny', Shiny.shinyapp.$inputValues['x2:shiny.number'])
    document.getElementById('reporterJS2').innerText = Shiny.shinyapp.$inputValues['x2:shiny.number']
  })

})
"
  ))
)

server <- function(input, output, session) {
  x2 <- 0  # use this for state of Shiny counter
  output$reporterR <- renderText({input$x})

  observe({
    invalidateLater(1000, session = session)
    x2 <<- x2 + 1 # <<- or assign required to update parent
    updateNumericInput(inputId = "x2", value = x2, session = session)
  })
}

shinyApp(
  ui = ui,
  server = server,
  options = list(launch.browser = rstudioapi::viewer)
)
JohnCoene commented 4 years ago

This is genius: VueReactivity.reactive(Shiny.shinyapp.$inputValues), I did not know this was possible and indeed, it does not seem to break anything. I have not tested but perhaps it breaks namespaces (shiny::ns())?

Perhaps somewhat interestingly I was thinking along the same lines but rather differently; the idea of making shiny inputs reactive had not occurred to me. I was just exploring possibilities in private repo to essentially mimic reactiveValues JavaScript-side. It's still too early to share as there's not much functional yet but will send that your way when done.

1) I might be completely wrong about this but sharing data from R server to JS/front-end can be made more efficient: two htmlwidgets using the cars dataset are serialised and stored twice.

2) These are then not reactive. Ideally I would have the cars dataset as stored JavaScript object and a proxy of it in Shiny so I can dynamically interact with it and simply have it referenced for use in htmlwidgets and elsewhere. So I can very easily change that reactive (e.g.: add/remove rows) and see the changes reflected in all the htmlwidgets/shiny inputs that make use of that dataset.

I'm not sure I make complete sense.

JohnCoene commented 4 years ago

I invited you to the repo in question (jsdata). I genuinely do not know if what it aims to achieve is even a good idea to begin with, feel free to tell me (and be honest about it too).

library(htmltools)
library(shiny)
library(jsdata)

random_string <- function(){
  paste0(sample(letters, 10), collapse = "")
}

string <- as_jsdata(random_string(), id = "string")

ui <- fluidPage(
  tags$head(
    tags$script(src = "https://unpkg.com/@vue/reactivity@3.0.0-rc.5/dist/reactivity.global.js"),
  ),
  useJsdata(),
  includeDataset(string),
  tags$script(
    HTML("datasets._datasets = datasets._datasets.map(set => VueReactivity.reactive(set));",
    "$(document).on('shiny:connected', function() {",
    "VueReactivity.effect(() => {
      document.getElementById('test').innerText = datasets.getDataset('string')
    })",
    "});")
  ),
  h1(id = "test")
)

server <- function(input, output){

  observe({
    invalidateLater(2000)
    new_string <- as_jsdata(random_string(), id = "string")
    update_dataset(new_string)
  })
}

shinyApp(ui, server)

My thinking was that this might make it somewhat easier to make existing htmlwidgets support reactivity.

JohnCoene commented 4 years ago

(If you think this approach could work I can rename and move the package to this org)

timelyportfolio commented 4 years ago

@JohnCoene I really like the idea of jsdata but I think it likely operates best as a standalone. I don't know if you would want jsdata locked into vue-next reactivity. mobx would be another very good solution. I wonder if jsdata should provide a non-reactive foundation and then extensions could be built to supply the reactivity layer. Both mobx and vue reactivity are set for a new release soon.

One example @frissanalytics and I discussed was using vuex to manage data state in JavaScript with R doing the data manipulation.

timelyportfolio commented 4 years ago

@JohnCoene https://github.com/vue-r/vueR/issues/4 and https://gist.github.com/timelyportfolio/edd70a7e40c54442aaccd5f529427fdc potentially related for jsdata purposes.

timelyportfolio commented 3 years ago

valtio

After more thought, experimentation, and testing, I actually think valtio might be better than all mentioned above. I made a standalone build at valtio_standalone for easier testing without a modern (also complicated) JavaScript build toolchain.

example from above but with valtio

# experiment with Shiny inputValues and valtio
#   reference:
#     https://github.com/pmndrs/valtio
ui <- tagList(
  tags$head(
    tags$script(src = "valtio.js"),
  ),
  tags$div(
    tags$h3("Increment with JavaScript"),
    tags$span("Shiny: "),
    textOutput("reporterR", inline = TRUE),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS"
    ),
    tags$span("valtio computed: "),
    tags$span(
      id = "reporterComputed"
    )
  ),
  tags$div(
    tags$h3("Increment with R/Shiny"),
    tags$span("Shiny (used numeric input for convenience): "),
    numericInput(inputId = 'x2', label = "", value = 0),
    tags$span("JavaScript: "),
    tags$span(
      id = "reporterJS2"
    )
  ),
  tags$script(HTML(
"
$(document).on('shiny:connected', function() {

  // once Shiny connected replace Shiny inputValues with reactive Shiny inputValues
  Shiny.shinyapp.$inputValues = valtio.proxy(Shiny.shinyapp.$inputValues)

  // do our counter using Shiny.setInputValue from JavaScript
  Shiny.setInputValue('x', 0) // initialize with 0

  // test valtio computed
  valtio.addComputed(Shiny.shinyapp.$inputValues, {
    doubled: snap => snap.x * 2,
  })

  valtio.subscribeKey(Shiny.shinyapp.$inputValues, 'x', (v) => {
    console.log('javascript', v)
    document.getElementById('reporterJS').innerText = v
  })

  valtio.subscribeKey(Shiny.shinyapp.$inputValues, 'doubled', (v) => {
    console.log('javascript', v)
    document.getElementById('reporterComputed').innerText = v
  })

  setInterval(
    function() {
      Shiny.setInputValue('x', Shiny.shinyapp.$inputValues.x + 1) //increment by 1
    },
    1000
  )

  // react to counter implemented in Shiny
  valtio.subscribeKey(Shiny.shinyapp.$inputValues, 'x2:shiny.number', (v) => {
    console.log('shiny', v)
    document.getElementById('reporterJS2').innerText = v
  })

})
"
  ))
)

server <- function(input, output, session) {
  x2 <- 0  # use this for state of Shiny counter
  output$reporterR <- renderText({input$x})

  observe({
    invalidateLater(1000, session = session)
    x2 <<- x2 + 1 # <<- or assign required to update parent
    updateNumericInput(inputId = "x2", value = x2, session = session)
  })
}

shinyApp(
  ui = ui,
  server = server,
  options = list(launch.browser = rstudioapi::viewer)
)