John Coene
I’m a software engineer, founder of Opifex
Opifex
How shiny works?
The (very) brief version.
The client makes an initial GET
request to the server()
which responds with the initial ui
.
It then establishes a webSocket connection which is used for (almost) all subsequent communication between the client and the server()
.
Shiny = Single Page Application = 1 request
What do we mean by “endpoints”?
Just like plumber (with caveats) where we open endpoints
#* Echo back the input
#* @param msg The message to echo
#* @get /endpoint
function(msg="") {
list(msg = paste0("The message is: '", msg, "'"))
}
It’s an /endpoint
we can make a request to.
We can do the same within Shiny!
You will find real-world examples of the use of shiny endpoints on Github.
How?
We create an endpoint with a method on the myterious session
object.
It’s not a memorable method name and the arguments may be confusing.
Let’s unpack this.
What do they do?
filterFunc
A function that accepts a request and the data
and returns a response.data
- An object, passed to filterFunc
.name
the path, e.g.: name = "dataset"
=> /datasets~ishIt returns the full path.
This is because it is dynamically created for that very session.
session$registerDataObj("data", ...)
does not create a /data
path but rather /1638f...872/data
where the hash is referring to the session.
This can feel awkward but makes sense within Shiny.
The request
Accepts data and request.
req
is an environment
.The response
filter_fn <- \(data, req) {
shiny::httpResponse(
status = 200L,
content_type = "text/html; charset=UTF-8",
content = "",
headers = list()
)
}
status
- Status code of request (e.g.: 404)content_type
- Type of contentcontent
- Response bodyheaders
- Additional headersHTML
filter_fn <- \(data, req) {
shiny::httpResponse(
status = 200L,
content_type = "text/html",
content = "<h1>Hello, endpoints!</h1>"
)
}
JSON
The following more substantial example uses the data and the request.
filter_fn <- \(data, req) {
# GET "/1638f...872/data?col=mpg"
query <- shiny::parseQueryString(req$QUERY_STRING)
json <- jsonlite::toJSON(data[[query$col]])
shiny::httpResponse(
status = 200L,
content_type = "application/json",
content = json
)
}
GET on /1638f...872/data?col=mpg
.
Easy to use
From the client we can now call the API, it’s as easy as.
fetch('/1638f...872/data?col=mpg')
.then(response => response.json())
.then(data => {
// do something with the data
});
Could be translated to R with:
Easier to handle errors.
Using the WebSocket
So something with the data and send a response.
Handle the response client-side
Using endpoints
Client-side
Clearer code, easier to debug, and maintain.
Microscopic performance differences between request-response and WebSocket.
The request-response model forces the developer to think differently about a problem.
WebSocket ~ show/hide
vs.
Request-response ~ render when needed
Used (in Shiny) to retrieving data from the server, not to update the state of the server.
We’re making GET
requests.
We don’t have access to the session
.
Endpoints with Shiny