CINXE.COM

Shiny - Persistent data storage in Shiny apps

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head> <meta charset="utf-8"> <meta name="generator" content="quarto-1.4.557"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> <meta name="author" content="Dean Attali"> <meta name="dcterms.date" content="2020-11-23"> <meta name="description" content="Shiny apps often need to save data, either to load it back into a different session or to simply log some information. However, common methods of storing data from R may not work well with Shiny."> <title>Shiny - Persistent data storage in Shiny apps</title> <style> code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} div.columns{display: flex; gap: min(4vw, 1.5em);} div.column{flex: auto; overflow-x: auto;} div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} ul.task-list{list-style: none;} ul.task-list li input[type="checkbox"] { width: 0.8em; margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */ vertical-align: middle; } /* CSS for syntax highlighting */ pre > code.sourceCode { white-space: pre; position: relative; } pre > code.sourceCode > span { line-height: 1.25; } pre > code.sourceCode > span:empty { height: 1.2em; } .sourceCode { overflow: visible; } code.sourceCode > span { color: inherit; text-decoration: inherit; } div.sourceCode { margin: 1em 0; } pre.sourceCode { margin: 0; } @media screen { div.sourceCode { overflow: auto; } } @media print { pre > code.sourceCode { white-space: pre-wrap; } pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; } } pre.numberSource code { counter-reset: source-line 0; } pre.numberSource code > span { position: relative; left: -4em; counter-increment: source-line; } pre.numberSource code > span > a:first-child::before { content: counter(source-line); position: relative; left: -1em; text-align: right; vertical-align: baseline; border: none; display: inline-block; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 0 4px; width: 4em; } pre.numberSource { margin-left: 3em; padding-left: 4px; } div.sourceCode { } @media screen { pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; } } </style> <script src="../../../../site_libs/quarto-nav/quarto-nav.js"></script> <script src="../../../../site_libs/quarto-nav/headroom.min.js"></script> <script src="../../../../site_libs/clipboard/clipboard.min.js"></script> <script src="../../../../site_libs/quarto-search/autocomplete.umd.js"></script> <script src="../../../../site_libs/quarto-search/fuse.min.js"></script> <script src="../../../../site_libs/quarto-search/quarto-search.js"></script> <meta name="quarto:offset" content="../../../../"> <link href="../../../../r/articles/build/layout-guide/index.html" rel="next"> <link href="../../../../r/articles/build/pool-dplyr/index.html" rel="prev"> <link href="../../../../favicon.png" rel="icon" type="image/png"> <script src="../../../../site_libs/quarto-html/quarto.js"></script> <script src="../../../../site_libs/quarto-html/popper.min.js"></script> <script src="../../../../site_libs/quarto-html/tippy.umd.min.js"></script> <link href="../../../../site_libs/quarto-html/tippy.css" rel="stylesheet"> <link href="../../../../site_libs/quarto-html/quarto-syntax-highlighting.css" rel="stylesheet" id="quarto-text-highlighting-styles"> <script src="../../../../site_libs/bootstrap/bootstrap.min.js"></script> <link href="../../../../site_libs/bootstrap/bootstrap-icons.css" rel="stylesheet"> <link href="../../../../site_libs/bootstrap/bootstrap.min.css" rel="stylesheet" id="quarto-bootstrap" data-mode="light"> <script id="quarto-search-options" type="application/json">{ "location": "navbar", "copy-button": false, "collapse-after": 3, "panel-placement": "end", "type": "overlay", "limit": 50, "keyboard-shortcut": [ "f", "/", "s" ], "show-item-context": false, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", "search-copy-link-title": "Copy link to search", "search-hide-matches-text": "Hide additional matches", "search-more-match-text": "more match in this document", "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-text-placeholder": "", "search-detached-cancel-button-title": "Cancel", "search-submit-button-title": "Submit", "search-label": "Search" } }</script> <style>html{ scroll-behavior: smooth; }</style> <!-- Google Tag Manager --> <script> (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-KHBDBW7'); </script> <!-- End Google Tag Manager --> <!-- Add rel="noopener noreferrer" to each target="_blank" --> <script type="text/javascript"> window.addEventListener("load",() => [...document.querySelectorAll("a[target=_blank]")] .forEach(lnk => lnk.setAttribute("rel", "noopener noreferrer")) ); </script> <!-- End Add rel="noopener noreferrer" to each target="_blank" --> <link href="../../../../_lib/font-awesome/css/all.css" rel="stylesheet" type="text/css"> <!-- Shinylive functionality in Components section --> <script type="text/javascript"> (function () { function addShinyliveEditLinks() { const codeWithLink = document.querySelectorAll( ".sourceCode[data-shinylive]" ); codeWithLink.forEach((el) => { el.classList.add("code-with-shinylive-link"); const url = el.dataset.shinylive; // <a class="edit-shinylive-button btn btn-link" href="<%= app.shinylive %>" data-bs-toggle="tooltip" data-bs-title="Edit in Shinylive" data-bs-placement="bottom"> // <i class="bi bi-lightning-fill"></i> <span class="visually-hidden">Edit in Shinylive</span> // </a> const link = document.createElement("a"); link.classList.add("edit-shinylive-button", "btn", "btn-link"); link.href = url; link.dataset.bsToggle = "tooltip"; link.dataset.bsTitle = "Edit in Shinylive"; link.dataset.bsPlacement = "bottom"; const icon = document.createElement("i"); icon.classList.add("bi", "bi-lightning-fill"); link.appendChild(icon); const span = document.createElement("span"); span.classList.add("visually-hidden"); span.innerText = "Edit in Shinylive"; link.appendChild(span); const btnCopy = el.querySelector(".code-copy-button"); if (btnCopy) { btnCopy.parentElement.appendChild(link); } else { if (el.matches("pre")) { el.appendChild(link); } else { el.querySelector("pre").appendChild(link); } } if (window.bootstrap?.Tooltip) { new bootstrap.Tooltip(link); } el.removeAttribute("data-shinylive"); }); } function addWhatsShinyExpressTooltip() { const tooltipContents = `<p><strong>Shiny Express</strong> is a new, streamlined way to write a Shiny app.</p> <p><strong>Shiny Core</strong> refers to the original, functional Shiny syntax, which is still a great way to write Shiny apps.</p> <p class="fw-600" style="font-size:0.85em;"><a href="https://shiny.posit.co/blog/posts/shiny-express/">Read more <i class="bi bi-chevron-right align-text-top"></i></a></p>`; const tooltipDisplay = `<i class="bi bi-question-circle-fill d-inline d-sm-none"></i><span class="d-none d-sm-inline">What's this?</span>`; document .querySelectorAll(".panel-tabset.shiny-mode-tabset") .forEach(function (tabset) { const trigger = document.createElement("div"); trigger.classList.add("what-shiny-express", "text-white"); trigger.tabIndex = 0; trigger.innerHTML = tooltipDisplay; tabset.appendChild(trigger); if (!window.bootstrap?.Popover) { return; } new window.bootstrap.Popover(trigger, { html: true, title: "Express vs Core", content: tooltipContents, placement: "auto", trigger: "focus", container: tabset, }); }); } function handlePanelVariants() { document.querySelectorAll(".panel-pills > .nav").forEach((x) => { x.classList.remove("nav-tabs"); x.classList.add("nav-pills"); }); document.querySelectorAll(".panel-underline > .nav").forEach((x) => { x.classList.remove("nav-tabs"); x.classList.add("nav-underline"); }); document.querySelectorAll(".shiny-mode-tabset > .nav").forEach((x) => { x.classList.remove("nav-tabs"); x.classList.add("nav-underline"); }); } function onLoad() { addShinyliveEditLinks(); addWhatsShinyExpressTooltip(); handlePanelVariants(); } document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", onLoad) : onLoad(); })(); </script> <link rel="stylesheet" href="../../../secondary-menu.css"> <link rel="stylesheet" href="../../articles.css"> <link rel="stylesheet" href="../../../../r/secondary-menu.css"> <meta property="og:title" content="Shiny - Persistent data storage in Shiny apps"> <meta property="og:description" content="Shiny is a package that makes it easy to create interactive web apps using R and Python."> <meta property="og:image" content="https://shiny.posit.co/images/shiny-thumb.png"> <meta property="og:site_name" content="Shiny"> <meta property="og:locale" content="en_US"> <meta name="twitter:title" content="Shiny"> <meta name="twitter:description" content="Shiny is a package that makes it easy to create interactive web apps using R and Python."> <meta name="twitter:image" content="https://shiny.posit.co/images/shiny-thumb.png"> <meta name="twitter:card" content="summary_large_image"> </head> <body class="nav-sidebar floating nav-fixed fullcontent"> <div id="quarto-search-results"></div> <header id="quarto-header" class="headroom fixed-top"> <nav class="navbar navbar-expand-lg " data-bs-theme="dark"> <div class="navbar-container container-fluid"> <div class="navbar-brand-container mx-auto"> <a href="../../../../index.html" class="navbar-brand navbar-brand-logo"> <img src="../../../../images/shiny-solo.png" alt="Shiny logo." class="navbar-logo"> </a> </div> <div id="quarto-search" class="" title="Search"></div> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav navbar-nav-scroll ms-auto"> <li class="nav-item"> <a class="nav-link" href="../../../../index.html"> <span class="menu-text">Home</span></a> </li> <li class="nav-item"> <a class="nav-link" href="../../../../getstarted.html"> <span class="menu-text">Get Started</span></a> </li> <li class="nav-item dropdown "> <a class="nav-link dropdown-toggle" href="#" id="nav-menu-shiny-for-r" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <span class="menu-text">Shiny for R</span> </a> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="nav-menu-shiny-for-r"> <li class="dropdown-header"><img src="../../../../images/r-light.png" class="navbar-sub-logo img-fluid" alt="The R language logo"></li> <li> <a class="dropdown-item" href="../../../../r/getstarted/"> <span class="dropdown-text">Get Started</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/components/"> <span class="dropdown-text">Components</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/layouts/"> <span class="dropdown-text">Layouts</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/articles/index.html"> <span class="dropdown-text">Articles</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/gallery/index.html"> <span class="dropdown-text">Gallery</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/reference/shiny"> <span class="dropdown-text">Reference</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/help.html"> <span class="dropdown-text">Help</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/deploy.html"> <span class="dropdown-text">Deploy</span></a> </li> <li> <a class="dropdown-item" href="../../../../r/contribute.html"> <span class="dropdown-text">Contribute</span></a> </li> <li> <a class="dropdown-item" href="https://github.com/rstudio/shiny" target="_blank"><i class="bi bi-github" role="img"> </i> <span class="dropdown-text"></span></a> </li> </ul> </li> <li class="nav-item"> <a class="nav-link" href="https://shiny.posit.co/py/" target="_blank"> <span class="menu-text">Shiny for Python</span></a> </li> <li class="nav-item"> <a class="nav-link" href="../../../../blog/index.html"> <span class="menu-text">Blog</span></a> </li> </ul> </div> <!-- /navcollapse --> <div class="quarto-navbar-tools"> </div> </div> <!-- /container-fluid --> </nav> <nav class="quarto-secondary-nav"> <div class="container-fluid d-flex"> <button type="button" class="quarto-btn-toggle btn" data-bs-toggle="collapse" data-bs-target=".quarto-sidebar-collapse-item" aria-controls="quarto-sidebar" aria-expanded="false" aria-label="Toggle sidebar navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> <i class="bi bi-layout-text-sidebar-reverse"></i> </button> <nav class="quarto-page-breadcrumbs" aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item">Build</li><li class="breadcrumb-item">Backend</li><li class="breadcrumb-item"><a href="../../../../r/articles/build/overview/index.html">Data</a></li><li class="breadcrumb-item"><a href="../../../../r/articles/build/persistent-data-storage/index.html">Persistent data storage in Shiny apps</a></li></ol></nav> <a class="flex-grow-1" role="button" data-bs-toggle="collapse" data-bs-target=".quarto-sidebar-collapse-item" aria-controls="quarto-sidebar" aria-expanded="false" aria-label="Toggle sidebar navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> </a> </div> </nav> </header> <!-- content --> <div id="quarto-content" class="quarto-container page-columns page-rows-contents page-layout-article page-navbar"> <!-- sidebar --> <nav id="quarto-sidebar" class="sidebar collapse collapse-horizontal quarto-sidebar-collapse-item sidebar-navigation floating overflow-auto"> <div class="sidebar-menu-container"> <ul class="list-unstyled mt-1"> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/" class="sidebar-item-text sidebar-link"> <span class="menu-text"><strong>ARTICLES</strong></span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles#start" class="sidebar-item-text sidebar-link"> <span class="menu-text">Start</span></a> </div> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-1" aria-expanded="true"> <span class="menu-text">Build</span></a> <a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-1" aria-expanded="true" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-1" class="collapse list-unstyled sidebar-section depth1 show"> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-2" aria-expanded="false"> <span class="menu-text">Structure</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-2" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-2" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-3" aria-expanded="false"> <span class="menu-text">Standalone apps</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-3" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-3" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/app-formats/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">App formats and launching apps</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/two-file/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Two-file Shiny apps</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-4" aria-expanded="false"> <span class="menu-text">Interactive documents</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-4" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-4" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/rmarkdown/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Introduction to R Markdown</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/rmd-integration/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">R Markdown integration in the RStudio IDE</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/output-args/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Setting Output args via Render functions</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/interactive-docs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Introduction to interactive documents</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/rm-cheatsheet/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">The R Markdown cheatsheet</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/output-args/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Setting Output args via Render functions</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/generating-reports/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Generating downloadable reports</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-5" aria-expanded="false"> <span class="menu-text">Dashboards</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-5" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-5" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/dashboards/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Dashboards</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-6" aria-expanded="false"> <span class="menu-text">Gadgets</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-6" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-6" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/gadgets/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Shiny Gadgets</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/gadget-ui/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Designing Gadget UI</span></a> </div> </li> </ul> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-7" aria-expanded="true"> <span class="menu-text">Backend</span></a> <a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-7" aria-expanded="true" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-7" class="collapse list-unstyled sidebar-section depth2 show"> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-8" aria-expanded="false"> <span class="menu-text">Reactivity</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-8" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-8" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/reactivity-overview/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Reactivity - An overview</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/isolation/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Stop reactions with isolate()</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/execution-scheduling/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Execution scheduling</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/understanding-reactivity/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to understand reactivity in R</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/client-data/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Learn about your user with session$clientData</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-9" aria-expanded="true"> <span class="menu-text">Data</span></a> <a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-9" aria-expanded="true" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-9" class="collapse list-unstyled sidebar-section depth3 show"> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/overview/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Database basics - dplyr and DBI</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/sql-injections/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">SQL injection prevention</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/pool-basics/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using the pool package (basics)</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/pool-advanced/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using the pool package (advanced)</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/pool-dplyr/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using dplyr and pool to query a database</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/persistent-data-storage/index.html" class="sidebar-item-text sidebar-link active"> <span class="menu-text">Persistent data storage in Shiny apps</span></a> </div> </li> </ul> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-10" aria-expanded="false"> <span class="menu-text">Frontend</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-10" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-10" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-11" aria-expanded="false"> <span class="menu-text">User interface</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-11" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-11" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/layout-guide/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Application layout guide</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/display-modes/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Display modes</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/tabsets/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Tabsets</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/html-tags/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Customize your UI with HTML</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/html-ui/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Build your entire UI with HTML</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/dynamic-ui/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Build a dynamic UI that reacts to user input</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/templates/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">HTML Templates</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/tag-glossary/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Shiny HTML Tags Glossary</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/progress/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Progress indicators</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/modal-dialogs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Modal dialogs</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/notifications/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Notifications</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/themes/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Themes</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/images/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Render images in a Shiny app</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/render-table/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Displaying and customizing static tables</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/datatables/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to use DataTables in a Shiny App</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/action-buttons/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using Action Buttons</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/sliders/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using sliders</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/download/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Help users download data from your app</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/upload/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Help users upload files to your app</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/selectize/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using selectize input</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-12" aria-expanded="false"> <span class="menu-text">Graphics &amp; visualization</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-12" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-12" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/plot-interaction/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Interactive plots</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/selecting-rows-of-data/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Selecting rows of data</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/plot-interaction-advanced/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Interactive plots - advanced</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-13" aria-expanded="false"> <span class="menu-text">Shiny extensions</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-13" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-13" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/htmlwidgets/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">htmlwidgets</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/shinyjs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">JavaScript actions packaged for Shiny apps</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-build-widget/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to build a JavaScript based widget</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-widget-functionality/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to add functionality to JavaScript widgets</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-send-message/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to send messages from the browser to the server and back using Shiny</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-introjs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to develop an interactive, dynamic help system for your app with introJS</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-custom-input/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">How to create custom input bindings</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-dashboard/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Putting everything together to create an interactive dashboard</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-14" aria-expanded="false"> <span class="menu-text">Customizing Shiny</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-14" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-14" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/css/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using custom CSS in your app</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/building-inputs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Build custom input objects</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/building-outputs/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Build custom output objects</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/google-analytics/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Add Google Analytics</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/packaging-javascript/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Packaging JavaScript code for Shiny</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/communicating-with-js/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Communicating with Shiny via JavaScript</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-events/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">JavaScript Events in Shiny</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/build/js-dashboard/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Putting everything together to create an interactive dashboard</span></a> </div> </li> </ul> </li> </ul> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-15" aria-expanded="false"> <span class="menu-text">Improve</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-15" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-15" class="collapse list-unstyled sidebar-section depth1 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-16" aria-expanded="false"> <span class="menu-text">Refactor</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-16" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-16" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-17" aria-expanded="false"> <span class="menu-text">Code quality</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-17" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-17" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/debugging/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Debugging Shiny applications</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/upgrade-R/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Upgrading to a new version of R</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/req/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Handling missing inputs with req(…)</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/scoping/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Scoping rules for Shiny apps</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/reconnecting/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Reconnecting to Shiny apps</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/sanitize-errors/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Sanitizing error messages</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/validation/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Write error messages for your UI with validate</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/unicode/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Unicode characters in Shiny apps</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-18" aria-expanded="false"> <span class="menu-text">Testing</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-18" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-18" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/testing-overview/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Shiny testing overview</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/shinytest/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">shinytest</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/server-function-testing/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Server function testing</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-19" aria-expanded="false"> <span class="menu-text">Modules</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-19" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-19" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/modules/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Modularizing Shiny app code</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/communicate-bet-modules/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Communication between modules</span></a> </div> </li> </ul> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-20" aria-expanded="false"> <span class="menu-text">Scale</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-20" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-20" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-21" aria-expanded="false"> <span class="menu-text">Measure usage</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-21" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-21" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/usage-metrics/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Shiny App Usage Tracking</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/google-analytics/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Add Google Analytics</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-22" aria-expanded="false"> <span class="menu-text">Performance and scalability</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-22" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-22" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/caching/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Using caching in Shiny to maximize performance</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/plot-caching/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Plot Caching</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/profiling/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Profiling your Shiny app</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/performance/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Performance</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/nonblocking/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Non-blocking operations</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-23" aria-expanded="false"> <span class="menu-text">Tuning</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-23" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-23" class="collapse list-unstyled sidebar-section depth3 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/scaling-and-tuning/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Scaling and Performance Tuning with shinyapps.io</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/improve/scaling-and-tuning-ssp-rsc/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Scaling and Performance Tuning with Shiny Server Pro and Posit Connect</span></a> </div> </li> </ul> </li> </ul> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-24" aria-expanded="false"> <span class="menu-text">Share</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-24" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-24" class="collapse list-unstyled sidebar-section depth1 "> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-25" aria-expanded="false"> <span class="menu-text">Deployment</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-25" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-25" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/deployment-web/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Deploying Shiny apps to the web</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/shinyapps/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Getting started with shinyapps.io</span></a> </div> </li> <li class="sidebar-item"> <span class="menu-text">r/articles/share/shinyapps-auth/index.qmd</span> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/custom-domains/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Setting up custom domains on shinyapps.io</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/share-data/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Sharing data across sessions on shinyapps.io</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/migration/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Migrating authentication on shinyapps.io</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/shiny-server/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Introduction to Shiny Server</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/libraries/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Allowing different libraries for different apps on Shiny Server, Shiny Server Pro, and Posit Connect</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/permissions/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Creating user privileges on Posit Connect and Shiny Server Pro</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/admin-deployment/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Administrating Shiny Server, Shiny Server Pro, and Posit Connect</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-26" aria-expanded="false"> <span class="menu-text">Distribution</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-26" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-26" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/deployment-local/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Sharing apps to run locally</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/function/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Save your app as a function</span></a> </div> </li> </ul> </li> <li class="sidebar-item sidebar-item-section"> <div class="sidebar-item-container"> <a class="sidebar-item-text sidebar-link text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-27" aria-expanded="false"> <span class="menu-text">Bookmarking</span></a> <a class="sidebar-item-toggle text-start collapsed" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-27" aria-expanded="false" aria-label="Toggle section"> <i class="bi bi-chevron-right ms-2"></i> </a> </div> <ul id="quarto-sidebar-section-27" class="collapse list-unstyled sidebar-section depth2 "> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/bookmarking-state/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Bookmarking state</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/advanced-bookmarking/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Advanced bookmarking</span></a> </div> </li> <li class="sidebar-item"> <div class="sidebar-item-container"> <a href="../../../../r/articles/share/bookmarking-modules/index.html" class="sidebar-item-text sidebar-link"> <span class="menu-text">Bookmarking and modules</span></a> </div> </li> </ul> </li> </ul> </li> </ul> </div> </nav> <div id="quarto-sidebar-glass" class="quarto-sidebar-collapse-item" data-bs-toggle="collapse" data-bs-target=".quarto-sidebar-collapse-item"></div> <!-- margin-sidebar --> <!-- main --> <main class="content" id="quarto-document-content"> <!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-KHBDBW7" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <!-- End Google Tag Manager (noscript) --> <header id="title-block-header" class="quarto-title-block default"><nav class="quarto-page-breadcrumbs quarto-title-breadcrumbs d-none d-lg-block" aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item">Build</li><li class="breadcrumb-item">Backend</li><li class="breadcrumb-item"><a href="../../../../r/articles/build/overview/index.html">Data</a></li><li class="breadcrumb-item"><a href="../../../../r/articles/build/persistent-data-storage/index.html">Persistent data storage in Shiny apps</a></li></ol></nav> <div class="quarto-title"> <h1 class="title">Persistent data storage in Shiny apps</h1> </div> <div> <div class="description"> Shiny apps often need to save data, either to load it back into a different session or to simply log some information. However, common methods of storing data from R may not work well with Shiny. </div> </div> <div class="quarto-title-meta"> <div> <div class="quarto-title-meta-heading">Author</div> <div class="quarto-title-meta-contents"> <p>Dean Attali </p> </div> </div> <div> <div class="quarto-title-meta-heading">Published</div> <div class="quarto-title-meta-contents"> <p class="date">November 23, 2020</p> </div> </div> </div> </header> <p>Shiny apps often need to save data, either to load it back into a different session or to simply log some information. However, common methods of storing data from R may not work well with Shiny. Functions like <code>write.csv()</code> and <code>saveRDS()</code> save data locally, but consider how <a href="https://www.shinyapps.io/">shinyapps.io</a> works.</p> <p>Shinyapps.io is a popular server for hosting Shiny apps. It is designed to distribute your Shiny app across different servers, which means that if a file is saved during one session on some server, then loading the app again later will probably direct you to a different server where the previously saved file doesn’t exist.</p> <p>On other occasions, you may use data that is too big to store locally with R in an efficient manner.</p> <p>This guide will explain seven methods for storing persistent data remotely with a Shiny app. You will learn how to store:</p> <ul> <li><strong>Arbitrary data</strong> can be stored as a <strong>file</strong> in some sort of a <strong>file system</strong> (<a href="#local">local file system</a>, <a href="#dropbox">Dropbox</a>, <a href="#s3">Amazon S3</a>)</li> <li><strong>Structured rectangular data</strong> can be stored as a <strong>table</strong> in a <strong>relational database or table-storage service</strong> (<a href="#sqlite">SQLite</a>, <a href="#mysql">MySQL</a>, <a href="#gsheets">Google Sheets</a>)</li> <li><strong>Semi-structured data</strong> can be stored as a <strong>collection</strong> in a <strong>NoSQL database</strong> (<a href="#mongodb">MongoDB</a>)</li> </ul> <p>The article explains the theory behind each method, and augments the theory with working examples that will make it clear and easy for you to use these methods in your own apps.</p> <p>As a complement to this article, you can see a <a href="https://daattali.com/shiny/persistent-data-storage/"><strong>live demo of a Shiny app</strong></a> <strong>that uses each of the seven storage methods to save and load data</strong> (<a href="https://github.com/daattali/shiny-server/tree/master/persistent-data-storage">source code on GitHub</a>). This article expands on Jeff Allen’s <a href="../../../../r/articles/share/share-data/">article regarding sharing data across sessions</a>.</p> <section id="table-of-contents" class="level2"> <h2 data-anchor-id="table-of-contents">Table of contents</h2> <ul> <li><a href="#basic">Basic Shiny app without data storage</a></li> <li><a href="#local-vs-remote">Local vs remote storage</a></li> <li><a href="#persistent">Persistent data storage methods</a> <ul> <li><a href="#file">Store arbitrary data in a file</a> <ul> <li><a href="#local">Local file system (<strong>local</strong>)</a></li> <li><a href="#dropbox">Dropbox (<strong>remote</strong>)</a></li> <li><a href="#s3">Amazon S3 (<strong>remote</strong>)</a></li> </ul></li> <li><a href="#table">Store structured data in a table</a> <ul> <li><a href="#sqlite">SQLite (<strong>local</strong>)</a></li> <li><a href="#mysql">MySQL (<strong>local or remote</strong>)</a></li> <li><a href="#gsheets">Google Sheets (<strong>remote</strong>)</a></li> </ul></li> <li><a href="#nosql">Store semi-structured data in a NoSQL database</a> <ul> <li><a href="#mongodb">MongoDB (<strong>local or remote</strong>)</a></li> </ul></li> </ul></li> <li><a href="#conclusion">Conclusion</a></li> </ul> </section> <section id="basic" class="level1"> <h1>Basic Shiny app without data storage</h1> <p>To demonstrate how to store data using each storage type, we’ll start with a simple form-submission Shiny app that</p> <ol type="1"> <li>collects some information from the user</li> <li>stores their response, and</li> <li>shows all previous responses</li> </ol> <p>Initially the app will only save responses within its R session. We will learn later how to modify the app to use each different storage type.</p> <p>Here is the code for the basic app that we will be using as our starting point—copy it into a file named <code>app.R</code>. (In case you didn’t know: Shiny apps don’t <em>have to</em> be broken up into separate <code>ui.R</code> and <code>server.R</code> files, they can be completely defined in one file <a href="../../../../r/articles/build/app-formats/">as this Shiny article explains</a>.)</p> <div class="sourceCode" id="cb1"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(shiny)</span> <span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Define the fields we want to save from the form</span></span> <span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>fields <span class="ot">&lt;-</span> <span class="fu">c</span>(<span class="st">"name"</span>, <span class="st">"used_shiny"</span>, <span class="st">"r_num_years"</span>)</span> <span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span> <span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="co"># Shiny app with 3 fields that the user can submit data for</span></span> <span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="fu">shinyApp</span>(</span> <span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a> <span class="at">ui =</span> <span class="fu">fluidPage</span>(</span> <span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a> DT<span class="sc">::</span><span class="fu">dataTableOutput</span>(<span class="st">"responses"</span>, <span class="at">width =</span> <span class="dv">300</span>), tags<span class="sc">$</span><span class="fu">hr</span>(),</span> <span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">textInput</span>(<span class="st">"name"</span>, <span class="st">"Name"</span>, <span class="st">""</span>),</span> <span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> <span class="fu">checkboxInput</span>(<span class="st">"used_shiny"</span>, <span class="st">"I've built a Shiny app in R before"</span>, <span class="cn">FALSE</span>),</span> <span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">sliderInput</span>(<span class="st">"r_num_years"</span>, <span class="st">"Number of years using R"</span>,</span> <span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> <span class="dv">0</span>, <span class="dv">25</span>, <span class="dv">2</span>, <span class="at">ticks =</span> <span class="cn">FALSE</span>),</span> <span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">actionButton</span>(<span class="st">"submit"</span>, <span class="st">"Submit"</span>)</span> <span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a> ),</span> <span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> <span class="at">server =</span> <span class="cf">function</span>(input, output, session) {</span> <span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> <span class="co"># Whenever a field is filled, aggregate all form data</span></span> <span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> formData <span class="ot">&lt;-</span> <span class="fu">reactive</span>({</span> <span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">sapply</span>(fields, <span class="cf">function</span>(x) input[[x]])</span> <span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a> })</span> <span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a> <span class="co"># When the Submit button is clicked, save the form data</span></span> <span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a> <span class="fu">observeEvent</span>(input<span class="sc">$</span>submit, {</span> <span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a> <span class="fu">saveData</span>(<span class="fu">formData</span>())</span> <span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a> })</span> <span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a> <span class="co"># Show the previous responses</span></span> <span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a> <span class="co"># (update with current response when Submit is clicked)</span></span> <span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a> output<span class="sc">$</span>responses <span class="ot">&lt;-</span> DT<span class="sc">::</span><span class="fu">renderDataTable</span>({</span> <span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a> input<span class="sc">$</span>submit</span> <span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a> <span class="fu">loadData</span>()</span> <span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a> }) </span> <span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a> }</span> <span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a>)</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>The above code is taken from a <a href="http://deanattali.com/2015/06/14/mimicking-google-form-shiny/">guide on how to mimic a Google form with Shiny</a>.</p> <p>The above app is very simple—there is a table that shows all responses, three input fields, and a <strong>Submit</strong> button that will take the data in the input fields and save it. You might notice that there are two functions that are not defined but are used in the app: <code>saveData(data)</code> and <code>loadData()</code>. These two functions are the only code that affects how the data is stored/retrieved, and we will redefine them for each data storage type. In order to make the app work for now, here’s a trivial implementation of the save and load functions that simply stores responses in the current R session.</p> <div class="sourceCode" id="cb2"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">as.data.frame</span>(<span class="fu">t</span>(data))</span> <span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="fu">exists</span>(<span class="st">"responses"</span>)) {</span> <span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> responses <span class="ot">&lt;&lt;-</span> <span class="fu">rbind</span>(responses, data)</span> <span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> } <span class="cf">else</span> {</span> <span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> responses <span class="ot">&lt;&lt;-</span> data</span> <span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a> }</span> <span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a></span> <span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> (<span class="fu">exists</span>(<span class="st">"responses"</span>)) {</span> <span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> responses</span> <span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a> }</span> <span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>Before continuing further, make sure this basic app works for you and that you understand every line in it—it is not difficult, but take the two minutes to go through it. The code for this app is also available as a <a href="https://gist.github.com/daattali/c4db11d81f3c46a7c4a5">gist</a> and you can run it either by copying all the code to your RStudio IDE or by running <code>shiny::runGist("c4db11d81f3c46a7c4a5")</code>.</p> </section> <section id="local-vs-remote" class="level1"> <h1>Local vs remote storage</h1> <p>Before diving into the different storage methods, one important distinction to understand is <em>local storage</em> vs <em>remote storage</em>.</p> <p>Local storage means saving a file on the same machine that is running the Shiny application. Functions like <code>write.csv()</code>, <code>write.table()</code>, and <code>saveRDS()</code> implement local storage because they will save a file on the machine running the app. Local storage is generally faster than remote storage, but it should only be used if you always have access to the machine that saves the files.</p> <p>Remote storage means saving data on another server, usually a reliable hosted server such as Dropbox, Amazon, or a hosted database. One big advantage of using hosted remote storage solutions is that they are much more reliable and can generally be more trusted to keep your data alive and not corrupted.</p> <p>When going through the different storage type options below, keep in mind that if your Shiny app is hosted on shinyapps.io, you will have to use a remote storage method for the time being. In the meantime, using local storage is only an option if you’re hosting your own <a href="https://www.rstudio.com/products/shiny/shiny-server/">Shiny Server</a>. If you want to host your own server, <a href="http://deanattali.com/2015/05/09/setup-rstudio-shiny-server-digital-ocean">here is a guide</a> that describes in detail how to set up your own Shiny Server.</p> </section> <section id="persistent" class="level1"> <h1>Persistent data storage methods</h1> <p>Using the above Shiny app, we can store and retrieve responses in many different ways. Here we will go through seven ways to achieve data persistence that can be easily integrated into Shiny apps. For each method, we will explain the method and provide a version of <code>saveData()</code> and <code>loadData()</code> that implements the method. To use a method as the storage type in the example app, run the app with the appropriate version of <code>saveData()</code> and <code>loadData()</code>.</p> <p>As a reminder, you can see all the seven different storage types being used, along with the exact code used, <a href="https://daattali.com/shiny/persistent-data-storage/">in this live Shiny app</a>.</p> <p>Here is a summary of the different storage types we will learn to use.</p> <div class="table table-condensed"> <table class="caption-top table"> <colgroup> <col style="width: 22%"> <col style="width: 25%"> <col style="width: 17%"> <col style="width: 18%"> <col style="width: 16%"> </colgroup> <thead> <tr class="header"> <th>Method</th> <th>Data type</th> <th style="text-align: center;">Local storage</th> <th style="text-align: center;">Remote storage</th> <th>R package</th> </tr> </thead> <tbody> <tr class="odd"> <td>Local file system</td> <td>Arbitrary data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;"></td> <td>-</td> </tr> <tr class="even"> <td>Dropbox</td> <td>Arbitrary data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>rdrop2</td> </tr> <tr class="odd"> <td>Amazon S3</td> <td>Arbitrary data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>aws.s3</td> </tr> <tr class="even"> <td>SQLite</td> <td>Structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;"></td> <td>RSQLite</td> </tr> <tr class="odd"> <td>MySQL</td> <td>Structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;">YES</td> <td>RMySQL</td> </tr> <tr class="even"> <td>Google Sheets</td> <td>Structured data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>googlesheets4</td> </tr> <tr class="odd"> <td>MongoDB</td> <td>Semi-structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;">YES</td> <td>mongolite</td> </tr> </tbody> </table> </div> <section id="file" class="level2"> <h2 data-anchor-id="file">Store arbitrary data in a file</h2> <p>This is the most flexible option to store data since files allow you to store any type of data, whether it is a single value, a big <em>data.frame</em>, or any arbitrary data. There are two common cases for using files to store data:</p> <ol type="1"> <li>you have one file that gets repeatedly overwritten and used by all sessions (like the example in the <a href="../../../../r/articles/share/share-data">sharing data article</a>), or</li> <li>you save a new file every time there is new data</li> </ol> <p>In our case we’ll use the latter because we want to save each response as its own file. We can use the former option, but then we would introduce the potential for <a href="https://en.wikipedia.org/wiki/Race_condition#File_systems">race conditions</a> which will overcomplicate the app. A race condition happens when two users submit a response at the exact same time, but since the file cannot deal with multiple edits simultaneously, one user will overwrite the response of the other user.</p> <p>When saving multiple files, it is important to save each file with a different file name to avoid overwriting files. There are many ways to do this. For example, you can simply use the current timestamp and an <em>md5 hash</em> of the data being saved as the file name to ensure that no two form submissions have the same file name.</p> <p>Arbitrary data can be stored in a file either on the local file system or on remote services such as Dropbox or Amazon S3.</p> <section id="local" class="level3"> <h3 data-anchor-id="local">1. Local file system (<strong>local</strong>)</h3> <p>The most trivial way to save data from Shiny is to simply save each response as its own file on the current server. To load the data, we simply load all the files in the output directory. In our specific example, we also want to concatenate all of the data files together into one <em>data.frame</em>.</p> <p><strong>Setup:</strong> The only setup required is to create an output directory (responses in this case) and to ensure that the Shiny app has file permissions to read/write in that directory.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb3"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>outputDir <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">t</span>(data)</span> <span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a> <span class="co"># Create a unique file name</span></span> <span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> fileName <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(<span class="st">"%s_%s.csv"</span>, <span class="fu">as.integer</span>(<span class="fu">Sys.time</span>()), digest<span class="sc">::</span><span class="fu">digest</span>(data))</span> <span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> <span class="co"># Write the file to the local system</span></span> <span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> <span class="fu">write.csv</span>(</span> <span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> <span class="at">x =</span> data,</span> <span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a> <span class="at">file =</span> <span class="fu">file.path</span>(outputDir, fileName), </span> <span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="at">row.names =</span> <span class="cn">FALSE</span>, <span class="at">quote =</span> <span class="cn">TRUE</span></span> <span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> )</span> <span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a></span> <span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="co"># Read all the files into a list</span></span> <span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> files <span class="ot">&lt;-</span> <span class="fu">list.files</span>(outputDir, <span class="at">full.names =</span> <span class="cn">TRUE</span>)</span> <span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">lapply</span>(files, read.csv, <span class="at">stringsAsFactors =</span> <span class="cn">FALSE</span>) </span> <span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="co"># Concatenate all data together into one data.frame</span></span> <span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">do.call</span>(rbind, data)</span> <span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> <section id="dropbox" class="level3"> <h3 data-anchor-id="dropbox">2. Dropbox (<strong>remote</strong>)</h3> <p>If you want to store arbitrary files with a remote hosted solution instead of the local file system, you can store files on <a href="https://www.dropbox.com">Dropbox</a>. Dropbox is a file storing service which allows you to host any file, up to a certain maximum usage. The free account provides plenty of storage space and should be enough to store most data from Shiny apps.</p> <p>This approach is similar to the previous approach that used the local file system. The only difference is that now that files are being saved to and loaded from Dropbox. You can use the <a href="https://github.com/karthik/rdrop2"><code>rdrop2</code></a> package to interact with Dropbox from R. Note that <code>rdrop2</code> can only move existing files onto Dropbox, so we still need to create a local file before storing it on Dropbox.</p> <p><strong>Setup:</strong> You need to have a Dropbox account and create a folder to store the responses. You will also need to add authentication to <code>rdrop2</code> with any approach <a href="https://github.com/karthik/rdrop2#accessing-dropbox-on-shiny-and-remote-servers">suggested in the package README</a>. The authentication approach I chose was to authenticate manually once and to copy the resulting <code>.httr-oauth</code> file that gets created into the Shiny app’s folder.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb4"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(rdrop2)</span> <span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>outputDir <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">t</span>(data)</span> <span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> <span class="co"># Create a unique file name</span></span> <span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a> fileName <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(<span class="st">"%s_%s.csv"</span>, <span class="fu">as.integer</span>(<span class="fu">Sys.time</span>()), digest<span class="sc">::</span><span class="fu">digest</span>(data))</span> <span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a> <span class="co"># Write the data to a temporary file locally</span></span> <span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> filePath <span class="ot">&lt;-</span> <span class="fu">file.path</span>(<span class="fu">tempdir</span>(), fileName)</span> <span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="fu">write.csv</span>(data, filePath, <span class="at">row.names =</span> <span class="cn">FALSE</span>, <span class="at">quote =</span> <span class="cn">TRUE</span>)</span> <span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="co"># Upload the file to Dropbox</span></span> <span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">drop_upload</span>(filePath, <span class="at">path =</span> outputDir)</span> <span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a> <span class="co"># Read all the files into a list</span></span> <span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a> filesInfo <span class="ot">&lt;-</span> <span class="fu">drop_dir</span>(outputDir)</span> <span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a> filePaths <span class="ot">&lt;-</span> filesInfo<span class="sc">$</span>path_display</span> <span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">lapply</span>(filePaths, drop_read_csv, <span class="at">stringsAsFactors =</span> <span class="cn">FALSE</span>)</span> <span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a> <span class="co"># Concatenate all data together into one data.frame</span></span> <span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">do.call</span>(rbind, data)</span> <span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> <section id="s3" class="level3"> <h3 data-anchor-id="s3">3. Amazon S3 (<strong>remote</strong>)</h3> <p>Another popular alternative to Dropbox for hosting files online is <a href="https://aws.amazon.com/s3/">Amazon S3</a>, or <em>S3</em> in short. Just like with Dropbox, you can host any type of file on S3, but instead of placing files inside directories, in S3 you place files inside of <em>buckets</em>. You can use the <a href="https://github.com/cloudyr/aws.s3"><code>aws.s3</code></a> package to interact with S3 from R.</p> <p><strong>Setup:</strong> You need to have an <a href="https://aws.amazon.com/">Amazon Web Services</a> account and to create an S3 bucket to store the responses. As the <a href="https://github.com/cloudyr/aws.s3">package documentation explains</a>, you will need to set a few environment variables in order to call the API.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb5"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(aws.s3)</span> <span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>s3BucketName <span class="ot">&lt;-</span> <span class="st">"my-unique-s3-bucket-name"</span></span> <span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="fu">Sys.setenv</span>(<span class="st">"AWS_ACCESS_KEY_ID"</span> <span class="ot">=</span> <span class="st">"key"</span>,</span> <span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"AWS_SECRET_ACCESS_KEY"</span> <span class="ot">=</span> <span class="st">"secret"</span>,</span> <span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"AWS_DEFAULT_REGION"</span> <span class="ot">=</span> <span class="st">"region"</span>)</span> <span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span> <span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a> <span class="co"># Create a plain-text representation of the data</span></span> <span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">paste0</span>(</span> <span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(<span class="fu">names</span>(data), <span class="at">collapse =</span> <span class="st">","</span>), <span class="st">"</span><span class="sc">\n</span><span class="st">"</span>,</span> <span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(<span class="fu">unname</span>(data), <span class="at">collapse =</span> <span class="st">","</span>)</span> <span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> )</span> <span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a></span> <span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> file_name <span class="ot">&lt;-</span> <span class="fu">paste0</span>(</span> <span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(</span> <span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">get_time_human</span>(),</span> <span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> <span class="fu">digest</span>(data, <span class="at">algo =</span> <span class="st">"md5"</span>),</span> <span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> <span class="at">sep =</span> <span class="st">"_"</span></span> <span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> ),</span> <span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> <span class="st">".csv"</span></span> <span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> )</span> <span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a></span> <span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> <span class="co"># Upload the file to S3</span></span> <span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> <span class="fu">put_object</span>(<span class="at">file =</span> <span class="fu">charToRaw</span>(data), <span class="at">object =</span> file_name, <span class="at">bucket =</span> s3_bucket_name)</span> <span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a></span> <span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> <span class="co"># Get a list of all files</span></span> <span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> file_names <span class="ot">&lt;-</span> <span class="fu">get_bucket_df</span>(s3BucketName)[[<span class="st">"Key"</span>]]</span> <span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> <span class="co"># Read all files into a list</span></span> <span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">lapply</span>(file_names, <span class="cf">function</span>(x) {</span> <span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> object <span class="ot">&lt;-</span> <span class="fu">get_object</span>(x, s3BucketName)</span> <span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> object_data <span class="ot">&lt;-</span> <span class="fu">readBin</span>(object, <span class="st">"character"</span>)</span> <span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> <span class="fu">read.csv</span>(<span class="at">text =</span> object_data, <span class="at">stringsAsFactors =</span> <span class="cn">FALSE</span>)</span> <span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a> })</span> <span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> <span class="co"># Concatenate all data together into one data.frame</span></span> <span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">do.call</span>(rbind, data)</span> <span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a> data </span> <span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> </section> <section id="table" class="level2"> <h2 data-anchor-id="table">Store structured data in a table</h2> <p>If the data you want to save is structured and rectangular, storing it in a table would be a good option. Loosely defined, structured data means that each observation has the same fixed fields, and rectangular data means that all observations contain the same number of fields and fit into a nice 2D matrix. A <em>data.frame</em> is a great example of such data, and thus data.frames are ideal candidates to be stored in tables such as relational databases.</p> <p>Structured data must have some <em>schema</em> that defines what the data fields are. In a <em>data.frame</em>, the number and names of the columns can be thought of as the schema. In tables with a header row, the header row can be thought of as the schema.</p> <p>Structured data can be stored in a table either in a relational database (such as SQLite or MySQL) or in any other table-hosting service such as Google Sheets. If you have experience with database interfaces in other languages, you should note that R does not currently have support for prepared statements, so any SQL statements have to be constructed manually. One advantage of using a relational database is that with most databases it is safe to have multiple users using the database concurrently without running into race conditions thanks to <a href="https://en.wikipedia.org/wiki/Database_transaction">transaction support</a>.</p> <section id="sqlite" class="level3"> <h3 data-anchor-id="sqlite">4. SQLite (<strong>local</strong>)</h3> <p>SQLite is a very simple and light-weight relational database that is very easy to set up. SQLite is serverless, which means it stores the database locally on the same machine that is running the shiny app. You can use the <a href="https://github.com/rstats-db/RSQLite"><code>RSQLite</code></a> package to interact with SQLite from R. To connect to a SQLite database in R, the only information you need to provide is the location of the database file.</p> <p>To store data in a SQLite database, we loop over all the values we want to add and use a <a href="https://www.w3schools.com/sql/sql_insert.asp">SQL INSERT</a> statement to add the data to the database. It is essential that the schema of the database matches exactly the names of the columns in the Shiny data, otherwise the SQL statement will fail. To load all previous data, we use a plain <a href="https://www.w3schools.com/sql/sql_select.asp">SQL SELECT *</a> statement to get all the data from the database table.</p> <p><strong>Setup:</strong> First, you must have SQLite installed on your server. Installation is fairly easy; for example, on an Ubuntu machine you can install SQLite with <code>sudo apt-get install sqlite3 libsqlite3-dev</code>. If you use shinyapps.io, SQLite is already installed on the shinyapps.io server, which will be a handy feature in future versions of shinyapps.io, which will include persistent local storage.</p> <p>You also need to create a database and a table that will store all the responses. When creating the table, you need to set up the schema of the table to match the columns of your data. For example, if you want to save data with columns “name” and “email” then you can create the SQL table with <code>CREATE TABLE responses(name TEXT, email TEXT);</code>. Make sure the shiny app has write permissions on the database file and its parent directory.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb6"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(RSQLite)</span> <span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>sqlitePath <span class="ot">&lt;-</span> <span class="st">"/path/to/sqlite/database"</span></span> <span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>table <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a></span> <span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">dbConnect</span>(<span class="fu">SQLite</span>(), sqlitePath)</span> <span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a> <span class="co"># Construct the update query by looping over the data fields</span></span> <span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a> query <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(</span> <span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a> <span class="st">"INSERT INTO %s (%s) VALUES ('%s')"</span>,</span> <span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a> table, </span> <span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(<span class="fu">names</span>(data), <span class="at">collapse =</span> <span class="st">", "</span>),</span> <span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(data, <span class="at">collapse =</span> <span class="st">"', '"</span>)</span> <span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a> )</span> <span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a> <span class="co"># Submit the update query and disconnect</span></span> <span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbGetQuery</span>(db, query)</span> <span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbDisconnect</span>(db)</span> <span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a></span> <span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">dbConnect</span>(<span class="fu">SQLite</span>(), sqlitePath)</span> <span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a> <span class="co"># Construct the fetching query</span></span> <span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a> query <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(<span class="st">"SELECT * FROM %s"</span>, table)</span> <span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a> <span class="co"># Submit the fetch query and disconnect</span></span> <span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">dbGetQuery</span>(db, query)</span> <span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbDisconnect</span>(db)</span> <span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> <section id="mysql" class="level3"> <h3 data-anchor-id="mysql">5. MySQL (<strong>local or remote</strong>)</h3> <p>MySQL is a very popular relational database that is similar to SQLite but is more powerful. MySQL databases can either be hosted locally (on the same machine as the Shiny app) or online using a hosting service.</p> <p>This method is very similar to the previous SQLite method, with the main difference being where the database is hosted. You can use the <a href="https://github.com/rstats-db/RMySQL"><code>RMySQL</code></a> package to interact with MySQL from R. Since MySQL databases can be hosted on remote servers, the command to connect to the server involves more parameters, but the rest of the saving/loading code is identical to the SQLite approach. To connect to a MySQL database, you need to provide the following parameters: host, port, dbname, user, password.</p> <p><strong>Setup:</strong> You need to create a MySQL database (either locally or using a web service that hosts MySQL databases) and a table that will store the responses. As with the setup for SQLite, you need to make sure the table schema is properly set up for your intended data.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb7"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(RMySQL)</span> <span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="fu">options</span>(<span class="at">mysql =</span> <span class="fu">list</span>(</span> <span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="st">"host"</span> <span class="ot">=</span> <span class="st">"127.0.0.1"</span>,</span> <span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"port"</span> <span class="ot">=</span> <span class="dv">3306</span>,</span> <span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"user"</span> <span class="ot">=</span> <span class="st">"myuser"</span>,</span> <span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a> <span class="st">"password"</span> <span class="ot">=</span> <span class="st">"mypassword"</span></span> <span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>))</span> <span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>databaseName <span class="ot">&lt;-</span> <span class="st">"myshinydatabase"</span></span> <span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>table <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a></span> <span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">dbConnect</span>(<span class="fu">MySQL</span>(), <span class="at">dbname =</span> databaseName, <span class="at">host =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>host, </span> <span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a> <span class="at">port =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>port, <span class="at">user =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>user, </span> <span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a> <span class="at">password =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>password)</span> <span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a> <span class="co"># Construct the update query by looping over the data fields</span></span> <span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a> query <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(</span> <span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a> <span class="st">"INSERT INTO %s (%s) VALUES ('%s')"</span>,</span> <span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a> table, </span> <span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(<span class="fu">names</span>(data), <span class="at">collapse =</span> <span class="st">", "</span>),</span> <span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a> <span class="fu">paste</span>(data, <span class="at">collapse =</span> <span class="st">"', '"</span>)</span> <span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a> )</span> <span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a> <span class="co"># Submit the update query and disconnect</span></span> <span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbGetQuery</span>(db, query)</span> <span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbDisconnect</span>(db)</span> <span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a></span> <span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">dbConnect</span>(<span class="fu">MySQL</span>(), <span class="at">dbname =</span> databaseName, <span class="at">host =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>host, </span> <span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a> <span class="at">port =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>port, <span class="at">user =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>user, </span> <span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a> <span class="at">password =</span> <span class="fu">options</span>()<span class="sc">$</span>mysql<span class="sc">$</span>password)</span> <span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a> <span class="co"># Construct the fetching query</span></span> <span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a> query <span class="ot">&lt;-</span> <span class="fu">sprintf</span>(<span class="st">"SELECT * FROM %s"</span>, table)</span> <span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a> <span class="co"># Submit the fetch query and disconnect</span></span> <span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">dbGetQuery</span>(db, query)</span> <span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a> <span class="fu">dbDisconnect</span>(db)</span> <span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> <section id="gsheets" class="level3"> <h3 data-anchor-id="gsheets">6. Google Sheets (<strong>remote</strong>)</h3> <p>If you don’t want to deal with the formality and rigidity of a database, another option for storing tabular data is in a Google Sheet. One nice advantage of Google Sheets is that they are easy to access from anywhere; but unlike with databases, with Google Sheets data can be overwritten with multiple concurrent users.</p> <p>You can use the <a href="https://github.com/tidyverse/googlesheets4"><code>googlesheets4</code></a> package to interact with Google Sheets from R.</p> <p><strong>Setup:</strong> All you need to do is create a Google Sheet and set the top row with the names of the fields. You can do that either via a web browser or by using the <code>googlesheets4</code> package. You also need to have a Google account. Now if you try to get R to write to a Google Sheet, R will ask for your permission explicitly every time. In a Shiny app, you probably want to be able to write to Google Sheets automatically without having to authorize it every time manually, so you need to set up {googlesheets4} with authentication. To learn how to add this authentication, refer to the package documentation.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb8"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(googlesheets4)</span> <span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>table <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a></span> <span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a> <span class="co"># The data must be a dataframe rather than a named vector</span></span> <span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> data <span class="sc">%&gt;%</span> <span class="fu">as.list</span>() <span class="sc">%&gt;%</span> <span class="fu">data.frame</span>()</span> <span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a> <span class="co"># Add the data as a new row</span></span> <span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a> <span class="fu">sheet_append</span>(SHEET_ID, data)</span> <span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a></span> <span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a> <span class="co"># Read the data</span></span> <span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a> <span class="fu">read_sheet</span>(SHEET_ID)</span> <span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> </section> <section id="nosql" class="level2"> <h2 data-anchor-id="nosql">Store semi-structured data in a NoSQL database</h2> <p>If you have data that is not fully structured but is also not completely free-form, a good middle ground can be using a NoSQL database. NoSQL databases can also be referred to as schemaless databases because they do not use a formal schema. NoSQL databases still offer some of the benefits of a traditional relational database, but are more flexible because every entry can use different fields. If your Shiny app needs to store data that has several fields but there is no unifying schema for all of the data to use, then using a NoSQL database can be a good option.</p> <p>There are many NoSQL databases available, but here we will only show how to use mongoDB.</p> <section id="mongodb" class="level3"> <h3 data-anchor-id="mongodb">7. MongoDB (<strong>local or remote</strong>)</h3> <p>MongoDB is one of the most popular NoSQL databases, and just like MySQL it can be hosted either locally or remotely. There are many web services that offer mongoDB hosting, including <a href="https://www.mongodb.com/cloud/atlas">MongoDB Atlas</a> which gives you free mongoDB databases. In mongoDB, entries (in our case, responses) are stored in a <em>collection</em> (the equivalent of an S3 bucket or a SQL table).</p> <p>You can use the <a href="https://github.com/jeroen/mongolite"><code>mongolite</code></a> package to interact with mongoDB from R. As with the relational database methods, all we need to do in order to save/load data is connect to the database and submit the equivalent of an update or select query. To connect to the database you need to provide the following: db, host, username, password. When saving the data, <code>mongolite</code> requires the data to be in a data.frame format.</p> <p><strong>Setup:</strong> All you need to do is create a mongoDB database—either locally or using a web service such as MongoDB Atlas. Since there is no schema, it is not mandatory to create a collection before populating it.</p> <p><strong>Code:</strong></p> <div class="sourceCode" id="cb9"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">library</span>(mongolite)</span> <span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span> <span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="fu">options</span>(<span class="at">mongodb =</span> <span class="fu">list</span>(</span> <span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a> <span class="st">"host"</span> <span class="ot">=</span> <span class="st">"cluster0.1twdg.mongodb.net"</span>,</span> <span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="st">"username"</span> <span class="ot">=</span> <span class="st">"myuser"</span>,</span> <span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a> <span class="st">"password"</span> <span class="ot">=</span> <span class="st">"mypassword"</span></span> <span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>))</span> <span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>databaseName <span class="ot">&lt;-</span> <span class="st">"myshinydatabase"</span></span> <span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>collectionName <span class="ot">&lt;-</span> <span class="st">"responses"</span></span> <span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a></span> <span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a>saveData <span class="ot">&lt;-</span> <span class="cf">function</span>(data) {</span> <span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">mongo</span>(<span class="at">collection =</span> collectionName,</span> <span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a> <span class="at">url =</span> <span class="fu">sprintf</span>(</span> <span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a> <span class="st">"mongodb+srv://%s:%s@%s/%s"</span>,</span> <span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>username,</span> <span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>password,</span> <span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>host,</span> <span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a> databaseName</span> <span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a> ),</span> <span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a> <span class="at">options =</span> <span class="fu">ssl_options</span>(<span class="at">weak_cert_validation =</span> <span class="cn">TRUE</span>))</span> <span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a> <span class="co"># Insert the data into the mongo collection as a data.frame</span></span> <span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> <span class="fu">as.data.frame</span>(<span class="fu">t</span>(data))</span> <span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a> db<span class="sc">$</span><span class="fu">insert</span>(data)</span> <span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a>}</span> <span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a></span> <span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a>loadData <span class="ot">&lt;-</span> <span class="cf">function</span>() {</span> <span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a> <span class="co"># Connect to the database</span></span> <span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a> db <span class="ot">&lt;-</span> <span class="fu">mongo</span>(<span class="at">collection =</span> collectionName,</span> <span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a> <span class="at">url =</span> <span class="fu">sprintf</span>(</span> <span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a> <span class="st">"mongodb+srv://%s:%s@%s/%s"</span>,</span> <span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>username,</span> <span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>password,</span> <span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a> <span class="fu">options</span>()<span class="sc">$</span>mongodb<span class="sc">$</span>host,</span> <span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a> databaseName</span> <span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a> ),</span> <span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a> <span class="at">options =</span> <span class="fu">ssl_options</span>(<span class="at">weak_cert_validation =</span> <span class="cn">TRUE</span>))</span> <span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a> <span class="co"># Read all the entries</span></span> <span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a> data <span class="ot">&lt;-</span> db<span class="sc">$</span><span class="fu">find</span>()</span> <span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a> data</span> <span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a>}</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> </section> </section> </section> <section id="conclusion" class="level1"> <h1>Conclusion</h1> <p>Persistent storage lets you do more with your Shiny apps. You can even use persistent storage to access and write to remote data sets that would otherwise be too big to manipulate in R.</p> <p>The following table can serve as a reminder of the different storage types and when to use them. Remember that any method that uses local storage can only be used on Shiny Server, while any method that uses remote storage can be also used on shinyapps.io.</p> <div class="table table-condensed"> <table class="caption-top table"> <colgroup> <col style="width: 22%"> <col style="width: 25%"> <col style="width: 17%"> <col style="width: 18%"> <col style="width: 16%"> </colgroup> <thead> <tr class="header"> <th>Method</th> <th>Data type</th> <th style="text-align: center;">Local storage</th> <th style="text-align: center;">Remote storage</th> <th>R package</th> </tr> </thead> <tbody> <tr class="odd"> <td>Local file system</td> <td>Arbitrary data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;"></td> <td>-</td> </tr> <tr class="even"> <td>Dropbox</td> <td>Arbitrary data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>rdrop2</td> </tr> <tr class="odd"> <td>Amazon S3</td> <td>Arbitrary data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>aws.s3</td> </tr> <tr class="even"> <td>SQLite</td> <td>Structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;"></td> <td>RSQLite</td> </tr> <tr class="odd"> <td>MySQL</td> <td>Structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;">YES</td> <td>RMySQL</td> </tr> <tr class="even"> <td>Google Sheets</td> <td>Structured data</td> <td style="text-align: center;"></td> <td style="text-align: center;">YES</td> <td>googlesheets4</td> </tr> <tr class="odd"> <td>MongoDB</td> <td>Semi-structured data</td> <td style="text-align: center;">YES</td> <td style="text-align: center;">YES</td> <td>mongolite</td> </tr> </tbody> </table> </div> <p><br> <em>You can view the original post of this article, and leave further comments, at <a href="https://deanattali.com/blog/shiny-persistent-data-storage/">https://deanattali.com/blog/shiny-persistent-data-storage/</a>.</em></p> </section> </main> <!-- /main --> <script id="quarto-html-after-body" type="application/javascript"> window.document.addEventListener("DOMContentLoaded", function (event) { const toggleBodyColorMode = (bsSheetEl) => { const mode = bsSheetEl.getAttribute("data-mode"); const bodyEl = window.document.querySelector("body"); if (mode === "dark") { bodyEl.classList.add("quarto-dark"); bodyEl.classList.remove("quarto-light"); } else { bodyEl.classList.add("quarto-light"); bodyEl.classList.remove("quarto-dark"); } } const toggleBodyColorPrimary = () => { const bsSheetEl = window.document.querySelector("link#quarto-bootstrap"); if (bsSheetEl) { toggleBodyColorMode(bsSheetEl); } } toggleBodyColorPrimary(); const isCodeAnnotation = (el) => { for (const clz of el.classList) { if (clz.startsWith('code-annotation-')) { return true; } } return false; } const clipboard = new window.ClipboardJS('.code-copy-button', { text: function(trigger) { const codeEl = trigger.previousElementSibling.cloneNode(true); for (const childEl of codeEl.children) { if (isCodeAnnotation(childEl)) { childEl.remove(); } } return codeEl.innerText; } }); clipboard.on('success', function(e) { // button target const button = e.trigger; // don't keep focus button.blur(); // flash "checked" button.classList.add('code-copy-button-checked'); var currentTitle = button.getAttribute("title"); button.setAttribute("title", "Copied!"); let tooltip; if (window.bootstrap) { button.setAttribute("data-bs-toggle", "tooltip"); button.setAttribute("data-bs-placement", "left"); button.setAttribute("data-bs-title", "Copied!"); tooltip = new bootstrap.Tooltip(button, { trigger: "manual", customClass: "code-copy-button-tooltip", offset: [0, -8]}); tooltip.show(); } setTimeout(function() { if (tooltip) { tooltip.hide(); button.removeAttribute("data-bs-title"); button.removeAttribute("data-bs-toggle"); button.removeAttribute("data-bs-placement"); } button.setAttribute("title", currentTitle); button.classList.remove('code-copy-button-checked'); }, 1000); // clear code selection e.clearSelection(); }); var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//); var mailtoRegex = new RegExp(/^mailto:/); var filterRegex = new RegExp("^https?:\/\/((shiny\.(posit|rstudio)\.com?)|.+shiny-dev-center\.netlify)"); var isInternal = (href) => { return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href); } // Inspect non-navigation links and adorn them if external var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool)'); for (var i=0; i<links.length; i++) { const link = links[i]; if (!isInternal(link.href)) { // undo the damage that might have been done by quarto-nav.js in the case of // links that we want to consider external if (link.dataset.originalHref !== undefined) { link.href = link.dataset.originalHref; } // target, if specified link.setAttribute("target", "_blank"); if (link.getAttribute("rel") === null) { link.setAttribute("rel", "noopener"); } } } function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { const config = { allowHTML: true, maxWidth: 500, delay: 100, arrow: false, appendTo: function(el) { return el.parentElement; }, interactive: true, interactiveBorder: 10, theme: 'quarto', placement: 'bottom-start', }; if (contentFn) { config.content = contentFn; } if (onTriggerFn) { config.onTrigger = onTriggerFn; } if (onUntriggerFn) { config.onUntrigger = onUntriggerFn; } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); for (var i=0; i<noterefs.length; i++) { const ref = noterefs[i]; tippyHover(ref, function() { // use id or data attribute instead here let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href'); try { href = new URL(href).hash; } catch {} const id = href.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note) { return note.innerHTML; } else { return ""; } }); } const xrefs = window.document.querySelectorAll('a.quarto-xref'); const processXRef = (id, note) => { // Strip column container classes const stripColumnClz = (el) => { el.classList.remove("page-full", "page-columns"); if (el.children) { for (const child of el.children) { stripColumnClz(child); } } } stripColumnClz(note) if (id === null || id.startsWith('sec-')) { // Special case sections, only their first couple elements const container = document.createElement("div"); if (note.children && note.children.length > 2) { container.appendChild(note.children[0].cloneNode(true)); for (let i = 1; i < note.children.length; i++) { const child = note.children[i]; if (child.tagName === "P" && child.innerText === "") { continue; } else { container.appendChild(child.cloneNode(true)); break; } } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(container); } return container.innerHTML } else { if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } return note.innerHTML; } } else { // Remove any anchor links if they are present const anchorLink = note.querySelector('a.anchorjs-link'); if (anchorLink) { anchorLink.remove(); } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } // TODO in 1.5, we should make sure this works without a callout special case if (note.classList.contains("callout")) { return note.outerHTML; } else { return note.innerHTML; } } } for (var i=0; i<xrefs.length; i++) { const xref = xrefs[i]; tippyHover(xref, undefined, function(instance) { instance.disable(); let url = xref.getAttribute('href'); let hash = undefined; if (url.startsWith('#')) { hash = url; } else { try { hash = new URL(url).hash; } catch {} } if (hash) { const id = hash.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note !== null) { try { const html = processXRef(id, note.cloneNode(true)); instance.setContent(html); } finally { instance.enable(); instance.show(); } } else { // See if we can fetch this fetch(url.split('#')[0]) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.getElementById(id); if (note !== null) { const html = processXRef(id, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } } else { // See if we can fetch a full url (with no hash to target) // This is a special case and we should probably do some content thinning / targeting fetch(url) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.querySelector('main.content'); if (note !== null) { // This should only happen for chapter cross references // (since there is no id in the URL) // remove the first header if (note.children.length > 0 && note.children[0].tagName === "HEADER") { note.children[0].remove(); } const html = processXRef(null, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } }, function(instance) { }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { let cellAttr = 'data-code-cell="' + cell + '"'; let lineAttr = 'data-code-annotation="' + annotation + '"'; const selector = 'span[' + cellAttr + '][' + lineAttr + ']'; return selector; } const selectCodeLines = (annoteEl) => { const doc = window.document; const targetCell = annoteEl.getAttribute("data-target-cell"); const targetAnnotation = annoteEl.getAttribute("data-target-annotation"); const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation)); const lines = annoteSpan.getAttribute("data-code-lines").split(","); const lineIds = lines.map((line) => { return targetCell + "-" + line; }) let top = null; let height = null; let parent = null; if (lineIds.length > 0) { //compute the position of the single el (top and bottom and make a div) const el = window.document.getElementById(lineIds[0]); top = el.offsetTop; height = el.offsetHeight; parent = el.parentElement.parentElement; if (lineIds.length > 1) { const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]); const bottom = lastEl.offsetTop + lastEl.offsetHeight; height = bottom - top; } if (top !== null && height !== null && parent !== null) { // cook up a div (if necessary) and position it let div = window.document.getElementById("code-annotation-line-highlight"); if (div === null) { div = window.document.createElement("div"); div.setAttribute("id", "code-annotation-line-highlight"); div.style.position = 'absolute'; parent.appendChild(div); } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter"); gutterDiv.style.position = 'absolute'; const codeCell = window.document.getElementById(targetCell); const gutter = codeCell.querySelector('.code-annotation-gutter'); gutter.appendChild(gutterDiv); } gutterDiv.style.top = top - 2 + "px"; gutterDiv.style.height = height + 4 + "px"; } selectedAnnoteEl = annoteEl; } }; const unselectCodeLines = () => { const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"]; elementsIds.forEach((elId) => { const div = window.document.getElementById(elId); if (div) { div.remove(); } }); selectedAnnoteEl = undefined; }; // Handle positioning of the toggle window.addEventListener( "resize", throttle(() => { elRect = undefined; if (selectedAnnoteEl) { selectCodeLines(selectedAnnoteEl); } }, 10) ); function throttle(fn, ms) { let throttle = false; let timer; return (...args) => { if(!throttle) { // first call gets through fn.apply(this, args); throttle = true; } else { // all the others get throttled if(timer) clearTimeout(timer); // cancel #2 timer = setTimeout(() => { fn.apply(this, args); timer = throttle = false; }, ms); } }; } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { annoteDlNode.addEventListener('click', (event) => { const clickedEl = event.target; if (clickedEl !== selectedAnnoteEl) { unselectCodeLines(); const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active'); if (activeEl) { activeEl.classList.remove('code-annotation-active'); } selectCodeLines(clickedEl); clickedEl.classList.add('code-annotation-active'); } else { // Unselect the line unselectCodeLines(); clickedEl.classList.remove('code-annotation-active'); } }); } const findCites = (el) => { const parentEl = el.parentElement; if (parentEl) { const cites = parentEl.dataset.cites; if (cites) { return { el, cites: cites.split(' ') }; } else { return findCites(el.parentElement) } } else { return undefined; } }; var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]'); for (var i=0; i<bibliorefs.length; i++) { const ref = bibliorefs[i]; const citeInfo = findCites(ref); if (citeInfo) { tippyHover(citeInfo.el, function() { var popup = window.document.createElement('div'); citeInfo.cites.forEach(function(cite) { var citeDiv = window.document.createElement('div'); citeDiv.classList.add('hanging-indent'); citeDiv.classList.add('csl-entry'); var biblioDiv = window.document.getElementById('ref-' + cite); if (biblioDiv) { citeDiv.innerHTML = biblioDiv.innerHTML; } popup.appendChild(citeDiv); }); return popup.innerHTML; }); } } }); </script> <nav class="page-navigation"> <div class="nav-page nav-page-previous"> <a href="../../../../r/articles/build/pool-dplyr/index.html" class="pagination-link" aria-label="Using dplyr and pool to query a database"> <i class="bi bi-arrow-left-short"></i> <span class="nav-page-text">Using dplyr and pool to query a database</span> </a> </div> <div class="nav-page nav-page-next"> <a href="../../../../r/articles/build/layout-guide/index.html" class="pagination-link" aria-label="Application layout guide"> <span class="nav-page-text">Application layout guide</span> <i class="bi bi-arrow-right-short"></i> </a> </div> </nav> </div> <!-- /content --> <footer class="footer"> <div class="nav-footer"> <div class="nav-footer-left"> <p>Proudly supported by <a href="https://www.posit.co/" class="no-icon"><img src="../../../../images/posit-logo-black.svg" alt="Posit" width="80" style="padding-left: 3px;vertical-align:text-top;"></a></p> </div> <div class="nav-footer-center"> &nbsp; </div> <div class="nav-footer-right"> <ul class="footer-items list-unstyled"> <li class="nav-item compact"> <a class="nav-link" href="https://twitter.com/posit_pbc"> <i class="bi bi-twitter" role="img" aria-label="Posit Twitter"> </i> </a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://www.youtube.com/playlist?list=PL9HYL-VRX0oRbLoj3FyL5zeASU5FMDgVe"> <i class="bi bi-youtube" role="img" aria-label="Shiny YouTube Playlist"> </i> </a> </li> <li class="nav-item compact"> <a class="nav-link" href="../../../../blog/index.xml"> <i class="bi bi-rss-fill" role="img" aria-label="Shiny Blog"> </i> </a> </li> </ul> </div> </div> </footer> <script src="../../../../site_libs/quarto-html/zenscroll-min.js"></script> </body></html>

Pages: 1 2 3 4 5 6 7 8 9 10