Mastering Shiny - Book Review
Contents
shiny
is my goto package for building interactive dashboard. I have built more
than fifty shiny apps so far and have found the entire package infrastructure
around it to be extremely useful in showcasing data, algos, metrics - you name
it. This book by Hadley Wickham came out in 2021 but I never had a chance to go
over it, until now. It was wonderful to see so many code patterns/hacks that I
have learned over years appearing in the book. Needless to say, I have learned
so many fascinating aspects of shiny
from this book. This blogpost summarizes
some of my learnings/relearnings from the book:
Your first Shiny app
This chapter walks through the basic components of a Shiny app and helps the reader to create something a web app very quickly. Mainly I think it serves as a motivation to read the rest of the book.
Basic UI
This chapter gives a quick tour of all the input
and output
functions that
make up any shinyapp. It is meant to be a quick refresher to all the shiny
functions if you are a bit rusty with the package. If you are a beginner, of
course reading through all the functions at once is a massive cognitive overload
that can only be reduced by enough practice.
Basic Reactivity
ui
object in a shinyapp contains the HTML presented to every user of your app. Theui
is simple because every user gets the same HTML. Theui
is simple because every user gets the same HTML. Theserver
is more complicated because every user needs to get an independent version of the app. Shiny invokes yourserver()
function each time a new session starts. When a server function, it creates a new local environment that is independent of every other invocation of the function. This allows each session to have a unique state, as well as isolating the variables created inside the function. Hence all the reactive programming you’ll do in Shiny will be inside theserver
functionserver
function takes in three inputs,input
,output
andsession
input
objects are read-onlyinput
objects: it’s selective about who is allowed to read it .To read from aninput
, you must be in reactive context created by a function likerenderText()
orreactive()
- The render function does two things
- It sets up a special reactive context that automatically tracks what inputs the output uses
- It converts the output of your
R
code into HTML suitable for display for a web page
- The programming style of Shiny is declarative. You express higher-level goals or describe important constraints, and rely on someone else to decide how and/or when to translate that into action
- A Shiny app will only ever do the minimal amount of work needed to update the output controls that you can currently see
- The reactive graph contains one symbol for every input and output, and we connect an input to an output whenever the output accesses the input
- Order the code is run is solely determined by reactive graph. This is
different from most
R
code where the execution order is determined by the order of lines - producers - reactive inputs and expressions
- consumers - reactive expressions and outputs
- Why do we need reactive expressions ? Because in normal functions, you cannot access input variables
eventReactive
has two arguments: the first argument specifies what to take a dependency on, and the second argument specifies what to computeobserveEvent
has two important arguments,eventExpr
andhandlerExpr
. The first argument is the input or expression to take a dependency on, the second argument is the code that will be run- There are two important differences between
observeEvent
andeventReactive
- You don’t assign the result of
observeEvent
- You can’t refer to it from other reactive consumers
- You don’t assign the result of
Workflow
- Came across a link to a wonderful talk by Jennifer Bryan during RStudio conference(2022)
- In
R
, every error is accompanies by atraceback
or call stack, which literally traces back through the sequence of calls that lead to the error - Add a call to
browser()
in your source code to launch interactive debugger - To get the most useful help as quickly as possible, you need to create a reprex or reproducible example. The goal of reprex is to provide the smallest possible snippet of R code that illustrates the problem and can easily be run on another computer
- Most of the learnings from the chapter is from the link to Jennifer Bryan’s
talk. There are four strategies to debug your code
- Fresh start
- Restart your R-console(Cleans workspace, resets options and environment variables, clears search path)
- Reproducible
- Create a reprex so that you can recreate the bug with a simplified code so that others can help you
- Debug
- Use
traceback()
,options(error=recover)
,browser()
options - Comparison to Certificate of Death, Autopsy, War of Crafts Analogy
- Use
debug
,debugonce
functions
- Use
- Deter
- Use libraries such as
testthat
packages to write unit tests R CMD check
testthat::test_check()
- Write errors for humans
- Leave access panels
- Automate your checks
- Use libraries such as
- Fresh start
Layout, Themes, HTML
- Layout functions provide the high-level visual structure of an app. Layouts
are created by a hierarchy of function calls, where the hierarchy in
R
matches the hierarchy in the generated HTML - To make complex layouts, you’ll need all layout functions inside of
fluidPage()
. To make a two-column layout with inputs on the left and outputs on the right, you can usesidebarLayout()
- Break up page into pieces using
tabsetPanel()
andtabPanel()
.
I am familiar and have worked with most of the functions mentioned in the chapter and hence did not learn anything particularly interesting from this chapter
Graphics
- A plot can respond to four different mouse events :
click
,dblclick
,hover
andbrush
- One can modify the size of the plot, one can pass
width
andheight
arguments torenderPlot
- One can supply a string to the corresponding
plotOutput
arguments such asclick
,brush
,dbclick
, to create corresponding shiny input objects - There are functions built in shiny such as
nearPoints
,brushedPoints
- The basic data flow in interactive plots in order to understand their
limitations. The basic flow is something like this:
- JavaScript captures the mouse event
- Shiny sends the mouse event data back to
R
, telling the app that the input is now out of date - All the downstream reactive consumers are recomputed
plotOutput()
generates a new PNG and sends it to the browser
User Feedback
req
: This is a function that I should have used in my apps. I have always resorted to somewhat convoluted way of checking whether the user has provided an input or clicked a specific button before launching the reactive engine. This nice little function seems to be doing exactly that. This function is used to pause reactives so that nothing happens until some condition is true.req()
checks for required values before allowing a reactive producer to continue.- It works by signaling a special condition that causes all downstream reactives and outputs to stop executing.
shinyFeedback=
package can be used to display messages related to problems relating to a single inputvalidate()
can be used to stop execution of the rest of the code and instead displays the message in any downstream outputs. My hack has always been to use reactive values and then check the values for displaying messages or data. However usingvalidate()
is definitely a better way of handling feedback and invalidation at the same time- By default, the message shown by
showNotification
will disappear after 5 seconds withProgress()
andincProgress()
can be used to create progress bars- If you want more visual options, one can use
waiter
package. One can useuse_waitress()
function to show a visually appealing progress bar on a specific HTML element waiter
package provides a lot of functionality to show progress bars- I had never known about
waiter
package. The first version of the package came out in 2021. Here is what I learned from reading the manual- The function
autoWaiter
is there for convenience to easily add waiters to dynamically rendered Shiny content where ‘dynamic’ meansrender*
and*output
function pair. By defaultautoWaiter
will be applied to all dynamically-rendered elements - One can show a loading screen on app launch
- There are various functions in the package such as
Waiter
,Waitress
,Hostess
package
- The function
Uploads and downloads
This chapter is mainly about transferring files to and from a shinyapp. In the past, I have always provided pre-configured flat files for convenience. I don’t think I will deviate from the process for any future apps. So, for now, I have skimmed through the chapter and will revisit at a later date.
fileInput()
variable returns a data frame with four columns,name
,size
,type
anddatapath
- If the user is uploading a dataset, there are two details that one needs to be
aware of
input$upload
is initialized to NULL on page load- The
accept
argument allows you to limit the possible inputs
- Unlike other outputs,
downloadButton()
is not paired with a render function. Instead, you usedownloadHandler()
Dynamic UI
- There are three key techniques for creating dynamic user interfaces
- Using the
update
family of functions to modify parameters of input controls - Using
tabsetPanel
to conditionally show and hide parts of the user interface - Using
uiOutput
andrenderUI
to generate selected parts of the user interface with code
- Using the
ObserveEvent
andupdateSliderInput
usually go together to dynamically change UI elementsfreezeReactiveValue
ensures that any reactives or outputs that use the input won’t be updated until the next full round of invalidation- One can also use the
tabsetPanel
that has variestabPabelBody
but they are hidden. You can programatically control what gets shown by usingselected
parameter
Have used most of the techniques mentioned in this chapter in one of my shinyapps.
Bookmarking
This chapter talks about the way to store a specific state of shinyapp and revisit later. Skimmed this chapter and will revisit later. For all the 50 or more apps I have built, I have never found the use of bookmarking any of the demos. So, in all likelihood, I would never use this feature of Shiny
Tidy Evaluation
- There are two types of variables
env-variable
anddata-variable
. The former is a programming variable that you create with<-
. The latter is a statistical variable that lies inside a data frame - Data-masking functions allow you to use variables in the “current” data frame
without any extra syntax. It’s used in many
dplyr
functions. - In base
R
, one can switch between data-variable and environment variable by switching from$
to[[
- Inside data-masking functions, you can use
.data
and.env
, if you want to be explicit about whether you’re talking about a data variable or an environment variable
|
|
- Try to avoid
paste() + parse() + eval()
pattern as you can easily introduce bugs - Working with multiple variables is trivial when you are working with a function
that uses tidy-selection: you can just pass a character vector of variable
names in to
any_of()
orall_of()
. Wouldn’t it be nice if we could do that in data-masking functions too .That’s the idea of theacross()
function. it allows you to use tidy-selection inside data-masking functions
I found this chapter very interesting and useful as I had paid attention to the
fact that functions in tidyverse
use data-variables and environment-variables.
I had always hacked around and got this working. But reading this chapter makes
it very clear on the way to handle the two types of variables in data masking
and data selection functions. More over, this chapter also gives the motivation
behind introducing across()
function
Why reactivity?
- Reactivity is important for shinyapps because they’re interactive: users change input controls which causes logic to run on the server ultimately resulting in outputs updating.
- For shinyapps to be maximally useful, we need reactive expressions and outputs to update if and only if their inputs change. We want outputs to stay in sync with inputs while ensuring that we never do more work than necessary.
- Event-driven programming solves the problem of unnecessary computation, but it creates a new problem: you have to carefully track which inputs affect which computations
- A reactive expression has two important properties
- it’s lazy
- it’s cached
- While the ideas of reactivity have bbeen around for a long time, it wasn’t until the late 1990s; that they were seriously studied in academic computer science. Research in reactive programming was kicked off by FRAN, a novel system for incorporating changes over time and user input into a functional programming language. It wasn’t until the 2010s that reactive programming roared into the programming mainstream through the fast-paced world of JavaScript UI frameworks.
- Pioneering frameworks like Knockout, Ember, and Meteor demonstrated that reactive programming could make UI programming dramatically easier.
The reactive graph
- As soon as the app is started and the server function has been executed for the first time, all reactive consumers and producers are instantiated in the reactive graph with no connections. All reactive expressions and outputs are in their starting state(invalidated grey)
- The reactive inputs are ready(green) indicating that their values are available for computation
- In the execution phase, Shiny picks an invalidated output and starts executing it. It traverses the reactive graph establishing connections in such a way that output value is filled. This happens for all the output nodes and session goes to rest
- The most interesting part is what happens when an input changes. The system invalidates the relevant input, traverses through the dependencies and invalidates all the producers and consumers dependent on the changed input mode. It also removes all the relationships between those nodes. But why erase relationships ? That is the key part of Shiny’s reactive programming model: though these particular arrows were important, they are now out of date. The only way to ensure that our graph stays accurate is to erase arrows when they become stale, and let Shiny rediscover the relationships around these nodes as they re-execute
- Learned about
reactlog
package that shows how the reactive graph evolves over time
Reactive building blocks
- There are two types of reactive values. A single reactive value is created by
reactiveVal()
. A list of reactive values is created byreactiveValues
- Reactive expressions cache errors in exactly the same way that they cache values
- Errors are also treated the same way as values when it comes to the reactive
graph: errors propagate through the reactive graph exactly the same way as
regular values. The only difference is what happens when an error hits an
output or observer:
- An error in an output will be displayed in the app.
- An error in an observer will cause the current session to terminate. If you
don’t want this to happen, you’ll need to wrap the code in
try()
ortryCatch()
- Observers and outputs are terminal nodes in a reactive graph. They differ from
reactive expressions in two important was
- They are eager and forgetful
- The value returned by an observer is ignored because they are designed to work with functions called for their side-effects
- Observers are often coupled with reactive values in order to track state changes over time
isolate()
allows you to access the current value of a reactive or expression without taking a dependency on it
- Difference between
observeEvent()
andeventReactive()
- It’s like the difference between observe and reactive. One is intended to be run when some reactive variable is “triggered” and is meant to have side effects (observeEvent), and the other returns a reactive value and is meant to be used as a variable (eventReactive). Even in the documentation for those functions, the former is shown without being assigned to a variable (because it is intended to just produce a side effect), and the latter is shown to be assigned into a variable and used later on.
- An eventReactive creates an object that you define like reactive does, but with out usual chain-reaction behavior you get from reactive. However it is lazily evaluated and cached like the other reactives.
- An observeEvent can not create an object that you define (it creates something else). It is immediately evaluated and not cached. It is for causing side-effects.
Escaping the graph
- Reactive dependency is not created between the reactive value and the observer
in the following cases
- You call an
update
function setting thevalue
argument. This sends a message to the browser to change the value of an input, which then notifiesR
that the input value has been changed - You modify the value of a reactive value
- You call an
- All the examples mentioned in the chapter are centered around one message:
observeEvent
in an app creates connections that are captured by the reactive graph and one can usinvalidateLater()
to manually control the invalidation of an observer
General guidelines
Skimmed this chapter as it was more about software development skills and not specific to Shiny as such
Functions
This chapter resonated with my work a lot as I have mostly followed the material
mentioned in this book by trial and error. After building more than a dozen
apps, I realized that there must be a way to cut down repetitive tasks in
building ui and server components. That’s when I learned about namespaces
in
R
that has completely changed the way I build a shiny app.
- Why write separate functions ?
- In the UI, you have components that are repeated in multiple places with minor variation. Pulling out repeated code into a function reduces duplication
- In the server, complex reactives are hard to debug. Pulling out a reactive into a separate function, makes it substantially easier to debug
- Put large functions in their own
R/{function-name}.R
- When extracting out helpers, avoid taking reactives as input or returning outputs. Instead, pass values into arguments and assume the caller will turn the result in to a reactive if needed.
- Let’s say you have a complicated reactive function, it is better to put in
req
and then outsource the heavy duty to a non-reactive function
It was fantastic to see all the hacks that I have used in my work, formalized and expressed as concepts and rules that can be followed in a shinyapp development. Frankly I had never thought about the structure behind decoupling reactives and non-reactives. Feels so nice that all the patterns that I have been unconsciously following does have a structure to it.
Shiny Modules
Despite building over 50 shiny apps, I had never delved in to Shiny Modules and I must say this has been the biggest learning experience from the book. Before going through this chapter, I went through some of the online webinars on the topic and got familiarized with the topic. Here are some of the webinars/online videos that I went through:
- Structure your app using Shiny Modules
- Shinyapp in 100 seconds
- R Shiny Modules
- Why Modules Matter for New Shiny developers
After going through the above videos, I was in a much better shape to understand this chapter. The following are some of the points mentioned in this chapter:
- A module is very similar to an app. Like an app, it’s composed of two pieces
- module UI function that generates
ui
specification - module server function that runs code inside the
server
specification
- module UI function that generates
- Namespacing is different in module UI and server
- It is explicit in module UI
- It is implicit in moduleServer
Packages
- The core idea of a package is that it’s a set of conventions for organising your code and related artefacts: if you follow those conventions, you get a bunch of tools for free.
- Converting an app to a package
- Create an
R
directory and moveapp.R
into it - Transform your app into a standalone function
- Call
usethis::use_description()
to create a description file - Remove any calls to
source()
, sinceload_all()
automatically sources all.R
files inR/
- If you are loading datasets using
read.csv
you can instead useusethis::use_data(mydataset)
to save the data in thedata/
directory.load_all
automatically loads the data for you
- Create an
- There are two common extra steps you might take beyond the basics: making it
easy to deploy your app-package, and turning it into a “real” package
- You will need an
app.R
that tells the deployment server how to run your app - With a
DESCRIPTION
file, you need to explicitly specify all the packages in it. One can do so usingusethis::use_package("shiny")
commands
- You will need an
- A minimal package contains
R/
directory, aDESCRIPTION
file, and a function to run your app. This is very useful because it unlocks some useful workflows to speed up app development
Rest of the book
The last three chapters talk about testing, security and performance. By the time I had managed to come to these sections, I was a bit exhausted and reserved these chapters for a read at a later date.
Takeaway
This is probably one of the first books that I am reading, AFTER putting in a considerable amount of effort in building aroundy shiny apps. The advantage of reading AFTER putting in the work is that I was able spot patterns that I had figured out from trial and error. Seeing the patterns codified as general rules makes it easier to remember than trusting your subconscious mind to fill in, whenever it is needed. Shiny has a ton of features and it is only by building apps that one can learn and internalize. This book definitely helps you get a great comprehensive view of reactive framework as well as practical guidelines to developing shinyapps. My guess is that it will take at least a few years of developing apps to fully internalize the guidelines mentioned in this book and become really good at Shinyapp development.