Building a multi-session {shiny} application with {brochure}
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
About {brochure}
{shiny}
, and the closely linked packages like {bslib}
, {thematic}
, {shinytest}
, etc,
are a fantastic resource for R
programmers that enable building powerful interactive applications.
Building on top of these, are some new (and not so new) R
packages that that streamline and standardize
the development of {shiny}
applications. Among these are {golem}
(my personal favorite), {packer}
, {rhino}
,
and last, but certainly not least, {brochure}
.
{brochure}
is unique among the [shiny]
-related packages because it enables navitely multi-session applications. The
development is oriented around pages
that have with their own ui
, server
, and page
functions served on
independent endpoints. Thus, whenever we go from my_app.me/
to my_app.me/contact
, {brochure}
ensures that the
two pages run in independent {shiny}
sessions. This is fundamentally different from typical {shiny}
applications that are
by design single-session. And yes, with {brochure}
we can now have separate URL
s for our pages!
There is a lot more going on under the hood in {brochure}
that can be included here, and most of it I don’t fully understand.
For this I recommend to read the documentation and browse the source code. However,
I just built my first serious draft application with {brochure}
and wanted to comment on the experience.
Building my first {brochure}
app
I typically use {golem}
for developing {shiny}
applications, and was excited to see that {brochure}
is also designed to work with {golem}
. This meant that I can have a familiar directory structure
and I can use my usual workflow and shortcuts to develop the application. I also was thrilled to see that
there is a brochure::new_page
template function that can be used with golem::add_module
to create
{shiny}
module + {brochure}
page skeleton.
From here, setting up a basic application with several pages, is then as simple as:
- calling
golem::add_module(name = "page_X", module_template = brochure::new_page)
for each page, and
- calling the automatically generated
page_X()
page function within the project’srun_app()
function.
And that is pretty much it, {brochure}
handles the redirects,
so once you run_app()
you can visit the various endpoints we defined by pointing the
browser to .../page_1
, .../page_2
, etc.
So far, my pages do not share data, so I haven’t needed to use cookies or a database to
pass objects between sessions, but {brochure}
’s documentation covers this and I am
looking forward to adapting as my application increases in complexity.
Deploying my first {brochure}
app
To deploy my {brochure}
application to shinyapps.io
, I used golem::add_shinyappsio_file()
to
generate an app.R
file and then deploy with rsconnect::deployApp()
. However, once deployed, I could
only access the home page, at the url: my_account.shinyapps.io/my_app/
. All other pages could not be
accessed, returning a 404
code because the apparently the redirects were not set up correctly. But
everything worked fine locally. So what happened?
By default, {brochure}
assumes that the application’s URL is of the form my_app.me
, such that the /
endpoint (my_app.me/
)
would be home
and the /page
endpoint would lead to some page (my_app.me/page
). However, on shinyapps.io
,
and possibly other hosting options (e.g., ShinyProxy
), the app URL is of the form my_account.shinyapps.io/my_app/
,
so when {brochure}
redirects to /page
the generated URL (my_account.shinyapps.io/page
) is wrong,
it should be my_account.shinyapps.io/my_app/page
.
So, the default redirect href
s worked fine in my ‘development’ setting but not in ‘production’. I
needed a way to generate a different href
based on the current app URL, i.e., whether or not it contained
the my_app
base path.
In the {golem}
+{brochure}
framework we can achieve this by setting R
options before running the
application with run_app()
. So when developing locally, I can set options(baseurl = "")
and keep
working with default settings. In turn, when deploying to the server, we can set options(baseurl = "my_app")
.
Then, the environment in which we call run_app()
will have an option baseurl
that correspond to the application’s URL.
The next step was to write a function that would change the page
’s href
on the fly by looking up the baseurl
option,
and prefixing the endpoint. Along the lines of:
#' make_href #' #' @description Add appropriate prefix to redirect link depending on context (option baseurl) #' @param endpoint endpoint without leading `/` #' @noRd make_href <- function(endpoint) { baseurl <- getOption("baseurl") if (baseurl != "") { paste0("/", baseurl, "/", endpoint, sep = "") } else { paste0("/", endpoint, sep = "") } }
Then, in /dev/run_dev.R
we can set
options(baseurl = "") > make_href("") [1] "/" > make_href("page2") [1] "/page2" # then run app should work with unprefixed hrefs run_app()
In ‘production’ mode on shinyapps.io, we can add the baseurl option in app.R
before calling run_app()
:
options(baseurl = "my_app") > make_href("") [1] "/my_app/" > make_href("page2") [1] "/my_app/page2" # then run app should work with prefixed hrefs run_app()
To put this to work, we have to wrap our target endpoint
with the make_ref
function. For example,
by default, a link in a {brochure}
app might be:
tags$a( href = "page", "page" )
But to allow dynamic href
we’d need:
tags$a(href = make_href("page"), "page")
Finally, we also want to set the brochure::brochureApp
argument basepath
with our context-dependent baseurl
option,
as this will allow {brochure}
to correctly format the href
of our page. In the end, the run_app
function
would look something like this:
run_app <- function( onStart = NULL, options = list(), enableBookmarking = NULL, ... ) { with_golem_options( app = brochureApp( # Putting the resources here golem_add_external_resources(), # main pages home(), page1(), #... onStart = onStart, options = options, enableBookmarking = enableBookmarking, content_404 = "Not found", # change the base path depending on context: basepath = getOption("baseurl"), req_handlers = list(), res_handlers = list(), wrapped = shiny::fluidPage ), golem_opts = list(...) ) }
Final thoughts
Please note that {brochure}
is still a work in progress, and perhaps
not yet fully ready for all projects. Consider it carefully before
embarking on a large new project or transitioning single-session {shiny}
, as
{brochure}
might not yet have all features that might be required. After all,
the repository has a big bold warning THIS IS A WORK IN PROGRESS, DO NOT USE.
Overall, I was surprised how quickly one could get started with {brochure}
, especially
from {golem}
as a stepping stone. It is remarkable that we have this resource,
and I am thankful to Colin Fay for leading the way on novelties like these!
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.