/react-shiny-template

React - Shiny Template

Primary LanguageJavaScriptMIT LicenseMIT

Maintcainer Ask Me Anything !

[React JS 🤝 Shiny] template

See the minimalistic DEMO

React JS developers - welcome on (dash) board!

This setup allows the user to build frontend in pure React.js whereas keep the backend/logic in Shiny.

But one may ask - why?

  1. 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 with React.
  2. Shiny wouldn't have to be involved in generating UI.
  3. UI part is no longer dependent on R wrappers of JS libraries.
  4. You are able now to take advantage of:
  5. The React app is ultimately built as a static page therefore it can be placed as a static resource in our Shiny project (e.g. in www folder). It implies that nothing changes in terms of the deployment e.g. to RStudio Connect).

For whom?

  • You are a Shiny developer passionate about React / 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 your Shiny 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 with shiny.react

Setup

The setup allows for:

  1. Using Node.js server for the development (to see changes made live)
  2. Building the React app as a static page and then using it with Shiny

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

Launching the app

  1. Make sure you have all the R dependencies installed:
npm run install_shiny

Or from the Shiny subfolder

renv::restore()
  1. Then you launch the Shiny app
npm run prod

Or from the Shiny subfolder in an usual way:

shiny::runApp()

Development

Starting the development server

Required Node.js 16.x

  1. Make sure you have all the R dependencies installed:
npm run install_shiny
  1. If you are starting the development for the first time you need to install all the JS dependencies:
npm run install_react
  1. 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()

Communication between Shiny and React

There are basically three ways how a React app can communicate a Shiny backend:

  1. ShinyReact (WebSocket)
  2. ReactShiny (WebSocket)

You can also learn more about communication between JS and R through websocket HERE

  1. ReactShiny (HTTP API)

NOTE 1: no ui function is being presented assuming that all the UI is being handled by React 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).

1. ShinyReact (WebSocket)

Example

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>
}

2. ReactShiny (WebSocket)

Example

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} />
}

3. ReactShiny (HTTP API)

Description

This is probably the least popular way of communicating with Shiny server. However, there are many benefits from using it:

  1. Thanks to the stateless nature of HTTP API you can manage the app state solely in React (with a help of e.g. mobx, redux)
  2. You don't need to configure two-way WebSocket communication whenever React needs anything from Shiny (i.e. approach 1 combined with approach 2)
  3. It would be potentially easier to replace Shiny with any other HTTP 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)

registerDataObj(name, data, filterFunc)
Publishes any R 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 into registerDataObj) and req (an environment that implements the Rook specification for HTTP requests). filterFunc will be called with these values whenever an HTTP request is made to the URL endpoint. The return value of filterFunc should be a Rook-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

shiny::httpResponse(status, content_type, content, headers)

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


Graph

graph


Example

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>
}

FAQ

Why Shiny HTTP API approach? What about Plumber?

  1. 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.

  2. 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 standard Shiny app. It could be particularly useful for the Shiny developers that want to keep their workflow.

  3. 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 all Shiny dependencies (through {{ headContent() }} used in htmlTemplate() function), so when the session is over you can notice the characteristic grey page and notification about reloading the session.

  4. 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

Conclusions

  1. There is a possibility to break the monolithic structure of the app into the Shiny backend and React frontend part.

  2. Despite the breakout the React app can be still a part of the Shiny project implying no need for a separate frontend server.

  3. The React app can be almost totally independent from Shiny (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 in Shiny.
  4. Apart from a WebSocket Shiny offers session-scoped HTTP API out of the box.

  5. 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.

  6. React ecosystem offers huge amount of cool features you can use directly in your Shiny app!