Everything in brochure follows from one sentence: a page is a Shiny session, and two sessions never meet.
Opening /contact does not hide / and show
something else. It closes the first session and opens a second one,
which loads its own UI and runs its own server. Nothing computed on the
first page is there when the second starts.
That is not a limitation to work around. It is the trade: you get urls that mean something, and you give up the shared reactive graph that a single page app leans on. This article is about what that costs and how the usual patterns translate.
| Single page Shiny | brochure |
|---|---|
tabsetPanel() / navbarPage() |
one page() per tab |
conditionalPanel() on a “screen” input |
one page() per screen |
updateTabsetPanel() to move around |
a link, or server_redirect() |
renderUI() picking the screen |
the url picks the page |
| a navbar written once in the ui | wrapped, or a tag list in ... |
reactiveValues() shared by the screens |
a cookie plus a store |
moduleServer() per screen |
the same modules, one per page |
Modules survive the move unchanged, which is most of the work saved. What does not survive is whatever passed values between them.
There are two places, and the difference between them is who chooses the value.
The url, when the visitor does. A parameterised href makes the state part of the address, which means it can be linked to, bookmarked and shared:
page(
href = "/product/:sku",
ui = function(request) {
mod_product_ui("product")
},
server = function(input, output, session) {
mod_product_server("product", sku = get_keys()$sku)
}
)A cookie plus a store, when it is yours to decide.
The cookie names a session; the data lives server side, keyed by that
name. vignette("cookies") covers it, including the two
mistakes that are silent when made.
What does not work, and is worth naming because it is the reflex you
are bringing with you: a reactiveValues() at app scope. It
is a single object, shared by every visitor and every page, and it is
not reactive across sessions. The same goes for assigning into the
global environment.
A session is one visitor on one page: two people reading
/dashboard are two sessions, and one person with
/dashboard and /admin open in two tabs is two
sessions as well. That is Shiny’s usual arrangement, and brochure does
not change it.
What it does not change either is that all of them live in one R
process. Pages are not processes: a page that blocks holds up every
other page and every other visitor. On a two page app where
/slow takes five seconds to render, /fast
answers in 0.2s on its own and in 4.8s when it is opened while
/slow is rendering.
So the reasons to reach for {promises} and
{future} in a Shiny app are the same ones here, and
splitting an app into pages is not one of them. One thing to keep in
mind when you do: a navigation ends the session, so a future still
running when the visitor leaves has nowhere left to deliver.
An app whose screens genuinely interact — a filter in a sidebar
feeding six outputs, a selection in one tab driving a plot in another —
is one page. Making it several means inventing a way to carry the shared
state across, and the result is a worse version of what
tabsetPanel() gave you for free.
Brochure earns its place when the parts of your app are separate: a landing page, a login, a dashboard, an admin panel, a public report at a url you can send to someone. Different urls because they are different things — not because the app happens to have several screens.
vignette("brochure") — pages, routing, and what reaches
every pagevignette("cookies") — the cookie and store pattern, in
fullvignette("testing") — testing the routing without a
browservignette("golem") — the same app, as a package