That's not real web
A former boss told me that what I built with Shiny was not real web. It took me years, and a package, to work out which half of that he got right.
Before I joined ThinkR, back when I was learning R, a boss of mine said something that annoyed me for years:
What you do with Shiny is not real web.
I was building applications. They ran in a browser. People used them every day to do their job. What exactly was his problem?
I did not have an answer then. I have one now, and it is not the one I expected: he was wrong, and he was also right, and the two halves are worth separating.
Where he was wrong
Shiny taught me to think in interactions.
That sounds small. It is not. Before Shiny I wrote scripts: data in at the top, a plot out at the bottom, run it again when something changes. Shiny made the program answer to someone. A slider moves and the histogram follows. That shift, from a pipeline you run to a thing that responds, is the whole job of building software people use, and I learned it in R rather than by first learning JavaScript, a build toolchain and a framework.
Shiny put that within reach of anyone who could already write R. Dismissing it because of how it talks to the browser misses what it actually did.
So when someone tells you Shiny is not serious, they are wrong about the part that matters most: it is the reason a lot of us ship anything at all.
Where he was right
He was talking about the plumbing, and about the plumbing he had a point.
A Shiny app keeps a WebSocket open and an R session alive for every connected user. The server holds your inputs, your outputs and everything in between, and decides what to re-render when something changes. It works, and it is comfortable, but it is not how the web was designed to work. It is closer to a desktop application that happens to draw itself inside a browser, with R as its engine.
The web was designed around a much duller idea: a browser asks for something over HTTP, a server answers, the connection closes, everyone forgets. Nothing persists between two requests unless you decide it should. It sounds primitive until you notice everything you get for free from it, because the whole infrastructure of the internet was built around that shape.
That was his point, and it took me a few years to hear it.
What changed
Knowing all this did not help much, because I had no way to do anything else from R. I was not going to rewrite the interactive parts in JavaScript and the data parts in R and glue them together with an API I would also have to invent.
Two things showed up close together.
The first was htmx. It is a small JavaScript library with an idea in it: any HTML element can send an HTTP request, and what comes back is HTML that the browser swaps into the page. Not JSON you then have to render. HTML, over the wire, ready to display. The interactivity lives in attributes, so you never write the JavaScript yourself.
The second was plumber2 landing on CRAN. An HTTP server for R that I actually wanted to build on.
htmx handles the browser side, plumber2 handles the R side, and htmxr is the piece I wrote to connect them: R functions that generate the right HTML attributes, so the whole round trip stays in R.
The sentence I kept
If I had to keep one line from all of it:
In Shiny, your server reacts. In htmxr, your browser asks.
Everything else follows from that inversion. Who holds the state. What happens to a user who leaves a tab open and goes to lunch. Whether you can put a cache in front, or run two servers behind a load balancer without worrying which one a visitor lands on. Whether anything other than R can talk to your endpoints.
It also decides what you pay for. A stateless server bills you for work actually being done; a session-based one bills you for people who are merely connected. I measured the difference on my own apps, and it was large enough to change which machine I rent.
So, was he right?
He was right about the plumbing and wrong about the conclusion he drew from it.
Shiny is not a worse way of doing the same thing. It is a different model, and it is the better one for plenty of cases: a rich analytical dashboard with interdependent outputs is exactly what its reactive graph is for, and reimplementing that over HTTP would be a lot of work for a worse result.
But it is not the only model available from R any more, and that is the part that took me years and a package to work out. If you have been told your work is not real web, the interesting question is not whether the person was rude. It is which half they were right about.
If you want to see what the other model looks like in practice, Get started walks through a first app, and Coming from Shiny lays out the trade-offs side by side.