0

I have a shiny app which has many text inputs. I could not get the save data part right, for example, to save to a local drive. Any suggestions?

     server = function(input, output) {
      values <- reactiveValues()
      #Initial Dataframe 
      values$df <- data.frame(matrix(ncol=4,nrow=0, dimnames=list(NULL, c("Name", "date","Traning",  "certificate"))))

      FinalData =observe({
        if(input$submit >0) {
        isolate(values$df <- rbind(values$df,data.frame("name" = input$name,"date" = input$date, 
                           "training" = input$training, "certificate" = input$certificate)))
    # saveRDS(values$df)
    # saveRDS(FinalData)
                   }})

    #display the inputs
     output$Combined_table = renderDataTable({values$df})
}
)
py_dust
  • 19
  • 1
  • 7

1 Answers1

0

Try this demonstration:

library(shiny)
.log <- function(...) message(format(Sys.time(), format = "[ %H:%M:%S ]"), " ", ...)
.read <- function(path) if (file.exists(path)) return(readRDS(path))
shinyApp(
  ui = fluidPage(
    textInput("txt", "Text: "),
    actionButton("btn", "Submit"),
    tableOutput("tbl")
  ),
  server = function(input, output, session) {
    .log("hello world")
    rv <- reactiveValues()
    rv$df <- data.frame(row = 0L, word = "a", stringsAsFactors = FALSE)[0,]
    observeEvent(req(input$btn), {
      .log("submit!")
      rv$df <- rbind(rv$df,
                     data.frame(row = input$btn, word = input$txt,
                                stringsAsFactors = FALSE))
      .log("saveRDS: ", nrow(rv$df))
      saveRDS(rv$df, "local.rds")
    })
    filedata <- reactiveFileReader(1000, session, "local.rds", .read)
    output$tbl <- renderTable(filedata())
  }
)

sample shiny app

The engineering of this app:

  • I use a reactiveValues like you did, in order to keep the in-memory data. (Note: iteratively adding rows to a frame is bad in the long-run. If this is low-volume adding, then you're probably fine, but it scales badly. Each time a row is added, it copies the entire frame, doubling memory consumption.)
  • I pre-fill the $df with a zero-row frame, just for formatting. Nothing fancy here.
  • observe and observeEvent do not return something you are interested in, it should be operating completely by side-effect. It does return something, but it is really only meaningful to shiny internals.
  • saveRDS as you do, nothing fancy, but it works.
  • I added a shiny::reactiveFileReader in order to demonstrate that the file was being saved. When the shiny table shows an update, it's because (1) the data was added to the underlying frame; (2) the frame was saved to the "local.rds" file; then (3) reactiveFileReader noticed that the underlying file exists and has changed, causing (4) it to call my .read function to read the contents and return it as reactive data into filedata. This block is completely unnecessary in general, just for demonstration here.
  • I create a function .read for this reactiveFileReader that is resilient to the file not existing first. If the file does not exist, it invisibly returns NULL. There may be better ways to do this.
r2evans
  • 141,215
  • 6
  • 77
  • 149
  • Thanks r2evans. I was wondering if it will be working when upload the app to server, especially in same case some people may use the app at same time. Is there a chance that the inputs from one person may override the inputs from other people? let me try your code. – py_dust Apr 16 '20 at 02:26
  • Yes that's possible. It sounds like you need something like a database or redis (or similar) server to handle concurrent access. If you're talking just a few at a time, you might be able to use sqlite (or duckdb) instead. – r2evans Apr 16 '20 at 13:33
  • I have been reading "Persistent data storage with Shiny" by Deam Attali. He mentioned some remote solutions. I do not know if it works with microsoft SQL server which I have access – py_dust Apr 16 '20 at 14:28
  • With a DBMS available, you can certainly make this more amenable to multiple users. I suggest you create a table that hold all of your information (plus, perhaps, an `Id` field, others might be useful), and `DBI::dbWriteTable(..., append=TRUE)` that information. Instead of `reactiveFileReader`, if you need to periodically check, then `observe({..., invalidateLater(3000);})` will re-trigger the block every 3 seconds (tune to your needs, realizing it's running a query every time, definitely ways to do this wrong and right). – r2evans Apr 16 '20 at 14:57
  • (1) Dean is smart and talented. (2) DBMS is a good way to go, good reading to start talking "database access" (if you haven't already started): [`DBI`](https://github.com/r-dbi/DBI), [`odbc`](https://github.com/r-dbi/odbc), and [RStudio's "DB" stuff](https://db.rstudio.com/). – r2evans Apr 16 '20 at 18:48
  • r2evans, I tried your way using observeEvent(req(input$btn) which solve one of my bugs. My code will save the data without click submit button when I change name text input after previous submit button clicked (may be because if(input$submit >0)) i guess. what is "req" after oberveEvent ? what it does? Thanks – py_dust Apr 17 '20 at 02:40
  • If it (my code) saves the data without pressing the submit button, then either (1) you've changed something in the code, or (2) `shiny` has failed. The use of `observeEvent(req(input$btn), ...)` "guarantees" that the only thing that can trigger saving the data is `input$btn`. For your second question, see [`?req`](https://shiny.rstudio.com/reference/shiny/latest/req.html). – r2evans Apr 17 '20 at 03:45
  • 1
    that "guarantees" is what I am looking for. Appreciate ! – py_dust Apr 17 '20 at 14:04