--- title: "Coming from Shiny" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Coming from Shiny} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} # Convert a data frame for JSON serialisation result <- RDesk::rdesk_df_to_list(head(mtcars, 3)) length(result$rows) # 3 result$cols[1:3] # first three column names ``` If you know Shiny, you can learn RDesk in an afternoon. Your R data logic moves across unchanged. What changes is the delivery layer -- instead of a browser, you get a native Windows window. ## The mental model shift Shiny uses a **reactive graph** -- inputs change, outputs update automatically. RDesk uses **explicit message passing** -- the UI sends a message, R handles it and pushes a result back. This feels more like writing an API handler than a Shiny server. ## Side-by-side patterns ### Responding to user input **Shiny** ```r server <- function(input, output, session) { output$plot <- renderPlot({ filtered <- mtcars[mtcars$cyl == input$cyl, ] plot(filtered$wt, filtered$mpg) }) } ``` **RDesk** ```r app$on_message("filter", async(function(payload) { filtered <- mtcars[mtcars$cyl == payload$cyl, ] list(chart = rdesk_plot_to_base64( plot(filtered$wt, filtered$mpg) )) }, app = app)) ``` The key difference: RDesk does not re-run automatically. The UI explicitly calls `rdesk.send("filter", {cyl: 6})` and receives `filter_result` back. ### Sending data to the UI **Shiny** ```r output$table <- renderTable({ mtcars }) ``` **RDesk** ```r # In R -- push data explicitly app$send("table_data", list( rows = lapply(seq_len(nrow(mtcars)), function(i) as.list(mtcars[i,])), cols = names(mtcars) )) ``` ```javascript // In JavaScript -- receive it rdesk.on("table_data", function(data) { renderTable(data.rows, data.cols); }); ``` ### File downloads **Shiny** ```r output$download <- downloadHandler( filename = "data.csv", content = function(file) write.csv(mtcars, file) ) ``` **RDesk** ```r app$on_message("save_csv", function(payload) { path <- app$dialog_save( title = "Save CSV", filters = "CSV files (*.csv)|*.csv" ) if (!is.null(path)) write.csv(mtcars, path, row.names = FALSE) }) ``` ### Native menus (no Shiny equivalent) ```r app$set_menu(list( File = list( "Open..." = function() app$dialog_open(), "Save..." = function() app$dialog_save(), "---", "Exit" = app$quit ), Help = list( "About" = function() app$toast("MyApp v1.0", type = "info") ) )) ``` ### Async processing **Shiny** (with future/promises) ```r library(future) plan(multisession) observeEvent(input$run, { future({ slow_model(input$data) }) %...>% (function(result) { output$result <- renderText(result) }) }) ``` **RDesk** ```r app$on_message("run_model", async(function(payload) { slow_model(payload$data) }, app = app, loading_message = "Running model...")) ``` RDesk's `async()` handles the loading overlay, cancellation, and result routing automatically. ## What you can reuse unchanged Everything that does not touch Shiny inputs/outputs moves across directly: - All data loading and transformation code - All ggplot2 chart code (render with `rdesk_plot_to_base64()`) - All statistical modelling code - All file reading and writing code - All helper functions ## What you must rewrite | Shiny pattern | RDesk equivalent | |---|---| | `input$x` | `payload$x` inside `on_message()` | | `reactive({...})` | Call helper functions explicitly | | `renderPlot({...})` | `rdesk_plot_to_base64()` + `app$send()` | | `renderTable({...})` | `app$send()` with list of rows | | `observe({...})` | `app$on_message()` handler | | `showNotification()` | `app$toast()` | ## Practical migration path 1. Keep all your data and modelling R files unchanged 2. Replace `ui.R` with `www/index.html` (plain HTML -- no Shiny DSL) 3. Replace `server.R` handlers one by one using `app$on_message()` 4. Replace `renderPlot` calls with `rdesk_plot_to_base64() + app$send()` 5. Run `source("app.R")` and test each handler as you migrate