vue3 reactive input values
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
- 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.
- I doubt
Shinyproper would ever pursue a reactiveJSstate since it would have to choose which reactive state engine it would use. Is there potential forShinyto make the choice based on active community and well-tested, stable JS dependencies forShinyto 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. - 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?
- How might we best communicate the improvement from incorporating reactive JS input state?
- 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/[email protected]/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/[email protected]/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)
)
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.
-
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
carsdataset are serialised and stored twice. -
These are then not reactive. Ideally I would have the
carsdataset 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.
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/[email protected]/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.
(If you think this approach could work I can rename and move the package to this org)
@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.
@JohnCoene https://github.com/vue-r/vueR/issues/4 and https://gist.github.com/timelyportfolio/edd70a7e40c54442aaccd5f529427fdc potentially related for jsdata purposes.
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)
)