This setup allows the user to build frontend in pure React.js
whereas keep the backend/logic in Shiny
.
But one may ask - why?
- By breaking the monolithic structure of the
Shiny
app into frontend & backend we are able to apply modern standards and patterns for building beautiful web applications withReact
. Shiny
wouldn't have to be involved in generating UI.- UI part is no longer dependent on R wrappers of JS libraries.
- You are able now to take advantage of:
- Material UI, PrimeReact, Fluent UI, React-Bootstrap, Blueprint, Ant Design and many other great UI libraries.
- static typing with TypeScript ❤️
- using JSX ❤️
- mobx, redux for state management
- modern tools for creating your design system/styling
React
components - e.g. Storybook, styled-components - solutions addressing performance issues - e.g. react-virtualized/react-window for rendering huge lists
- support from large
React
community - the best standards of building web applications and patterns
- (many many others)
- The
React
app is ultimately built as a static page therefore it can be placed as a static resource in ourShiny
project (e.g. inwww
folder). It implies that nothing changes in terms of the deployment e.g. to RStudio Connect).
- You are a
Shiny
developer passionate aboutReact
/ willing to apply the latest standards of developing frontend or - You would like to reuse existing components from your other
React
applications or - You would like to collaborate with a
React
developer on yourShiny
dashboard to make it even more awesome
And
- You want to deploy your app the same way as other, regular
Shiny
apps
then this setup is for you!
Otherwise you might be interested in using
shiny.react
and packages based on top of that (e.g.shiny.fluent
). Here is a nice example of how to wrap Liquid Oxygen library withshiny.react
The setup allows for:
- Using
Node.js
server for the development (to see changes made live) - Building the
React
app as a static page and then using it withShiny
The React
app itself has been initialized with create-react-app
, so in case you need to perform some more sophisticated operations please take a look at the documentation
- Make sure you have all the
R
dependencies installed:
npm run install_shiny
Or from the
Shiny
subfolderrenv::restore()
- Then you launch the
Shiny
app
npm run prod
Or from the
Shiny
subfolder in an usual way:shiny::runApp()
Required
Node.js
16.x
- Make sure you have all the
R
dependencies installed:
npm run install_shiny
- If you are starting the development for the first time you need to install all the
JS
dependencies:
npm run install_react
-
Then you need to start both:
a. the
Node.js
development server:npm run start_react
b. the
Shiny
app:npm run start_shiny
If using Linux or MAC OS you can run simply:
npm run dev
And you are ready to go!
Sometimes your development app may behave strange (like restarting the session). Please try to clear the cache (in Chrome by
Shift
+F5
)
Once you decide your React
app is ready you need to build it and place it inside your Shiny
project. You can do it by running the command:
npm run build
Now, you can run your Shiny
app:
npm run prod
Or from the
Shiny
subfolder in an usual way:shiny::runApp()
There are basically three ways how a React
app can communicate a Shiny
backend:
You can also learn more about communication between JS and R through websocket HERE
NOTE 1: no
ui
function is being presented assuming that all theUI
is being handled byReact
app
NOTE 2: The examples given below aim to present just the rough idea of how the connection could be established (putting aside applicable design patterns or great tools you could use - like
react-query
).
On the Shiny
server side:
library(shiny)
server <- function(input, output, session) {
#...
session$sendCustomMessage("message_from_shiny", "HELLO FROM SHINY SERVER!")
}
On the React
side:
const App = () => {
const [shinyMessage, setShinyMessage] = useState(null);
window.Shiny.addCustomMessageHandler("message_from_shiny", (msg) => {
setShinyMessage(msg);
});
return <p>{shinyMessage}</p>
}
On the Shiny
server side:
library(shiny)
server <- function(input, output, session) {
#...
observeEvent(input$message_from_react, {
print(input$message_from_react)
})
}
On the React
side:
const App = () => {
const sendMessage = (e) => {
window.Shiny.setInputValue("message_from_react", e.target.value);
};
return <input type="text" onChange={sendMessage} />
}
This is probably the least popular way of communicating with Shiny
server. However, there are many benefits from using it:
- Thanks to the stateless nature of
HTTP API
you can manage the app state solely inReact
(with a help of e.g.mobx
,redux
) - You don't need to configure two-way WebSocket communication whenever
React
needs anything fromShiny
(i.e. approach 1 combined with approach 2) - It would be potentially easier to replace
Shiny
with any otherHTTP API
backend
The existence of HTTP API
in the Shiny
package given out of the box is a great and promising feature. However, out of the box does not actually mean transparent in a sense that the developer must combine certain - not intuitively named or well documented - functions in order to achieve it:
session$registerDataObj(name, data, filterFunc)
shiny::httpResponse(status, content_type, content, headers)
registerDataObj(name, data, filterFunc)
Publishes anyR
object as a URL endpoint that is unique to this session.name
must be a single element character vector; it will be used to form part of the URL.filterFunc
must be a function that takes two arguments:data
(the value that was passed intoregisterDataObj
) and req (an environment that implements theRook
specification for HTTP requests).filterFunc
will be called with these values whenever an HTTP request is made to the URL endpoint. The return value offilterFunc
should be aRook
-style response.
So instead of publishing any R object directly (in our case data = list()
) we are focusing on the filterFunc(data, req)
function, which in this case will work as the request handler.
The function returns an URL which looks similarily to this:
session/13b6edsessiontoken3764158e8a3af1/dataobj/example-api-example-get-api?w=&nonce=14367c50429fc201
The response will be handled by function shiny::httpResponse(...)
- there is no detailed description unfortunately (yet), but the idea is pretty straightforward - see the documentation. When determining content_type
you can use this source
On the Shiny
server side:
library(shiny)
library(jsonlite)
library(dplyr)
library(ggplot2)
server <- function(input, output, session) {
#...
return_data <- ggplot2::midwest
#' Endpoint for getting the data
example_get_data_url <- session$registerDataObj(
name = "example-get-api",
data = list(), # Empty list, we are not sharing any object
# That's the place where the request is being handled
filterFunc = function(data, req) {
if (req$REQUEST_METHOD == "GET") {
response <- return_data
response %>%
toJSON(auto_unbox = TRUE) %>%
httpResponse(200, "application/json", .)
}
}
)
session$sendCustomMessage(
"shiny_api_urls",
list(
example_get_data_url = example_get_data_url
)
)
}
On the React
side:
const App = () => {
const [urls, setUrls] = useState(null);
const [data, setData] = useState([]);
Shiny.addCustomMessageHandler('shiny_api_urls', function(urls) => {
setUrls(urls);
fetchData(urls);
})
const fetchData = async (urls) => {
const fetchedData = await fetch(urls.example_get_data_url).then(data => data.json());
setData(fetchedData);
}
const item_list = data.map((item) => (
<li key={item.PID}>{`${item.county} (${item.state})`}</li>
));
return <ul>{item_list}</ul>
}
Why Shiny
HTTP API approach? What about Plumber
?
-
Plumber doesn’t offer WebSocket connection out of the box yet as
Shiny
does. In other words, with Plumber only the client is initiating a communication - by making a request - whereas Shiny allows for bidirectional initialization. Having that the developer can trigger things to happen from the server-side, e.g. send a notification/message to the browser. -
As the UI is a static web page it can be part of the
Shiny
project. Therefore the developer does not have to bother with separate servers/deployments for backend and frontend. Deployment process to RStudio Connect will then be the same as for the standardShiny
app. It could be particularly useful for theShiny
developers that want to keep their workflow. -
The session is still managed by
Shiny
(all the HTTP URLs contain a session token, so assuming that session token is secret the HTTP URLs might be considered as session-scoped).React
app contains allShiny
dependencies (through{{ headContent() }}
used inhtmlTemplate()
function), so when the session is over you can notice the characteristic grey page and notification about reloading the session. -
With this approach you can still benefit from
Shiny
reactivity when developing your backend.
At the end of a day - everything depends on the case
-
There is a possibility to break the monolithic structure of the app into the
Shiny
backend andReact
frontend part. -
Despite the breakout the
React
app can be still a part of theShiny
project implying no need for a separate frontend server. -
The
React
app can be almost totally independent fromShiny
(except initial WebSocket-based URL exchange) which:- makes the potential backend replacement much easier.
- allows for concurrent development of the UI (e.g. by
React
developer) and server logic inShiny
.
-
Apart from a WebSocket
Shiny
offers session-scoped HTTP API out of the box. -
Such setup seems to be pretty hard to implement on the existing/grown up projects, so one could consider it when starting a new one.
-
React
ecosystem offers huge amount of cool features you can use directly in yourShiny
app!