Site icon R-bloggers

Developing React Applications in RStudio Workbench

[This article was first published on The Jumping Rivers Blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Introduction

RStudio Workbench provides a development environment for R, Python, and many other languages. When developing a performant web application you may progress from Shiny towards tools like Plumber. This allows you to continue development of the true application code, modelling, data processing in the language you already know, R, while providing an interface to those processes suitable for embedding in larger web-based applications.

As a Shiny developer, one popular front-end library you might already be familiar with is React. React is used in many popular {htmlwidgets} such as {reactable} and anything built with {reactR}.

React is a “A JavaScript library for building user interfaces”. It provides the coupling between your data, application state, and the HTML-based user interface. An optional extension of JavaScript called JSX allows efficient definition of React based user interface components. Another optional extension of JavaScript called TypeScript enables strict typing of your code, potentially improving the quality.

This short article covers some technical hurdles to make use of the standard Create React App workflow, this is compatible with Typescript, Redux, and other extensions.


Do you use RStudio Pro? If so, checkout out our managed RStudio services


Initial Setup

We are using RStudio Workbench for our development, and VS Code Sessions within Workbench as our IDE. Some extensions will be installed by default, a few of our enabled extensions include:

The last extension is the most important and is installed when following the RStudio Workbench installation guide.

We will assume a recent version of Node.js is installed, check with nodejs --version. We are using version 16.13.2. Open a VS Code Session in RStudio Workbench and create a new project:

npx create-react-app my-app --template typescript

The issues

Following the getting started instructions, we will enter the new project and start the development server:

cd my-app
npm start

If port 3000 is already in use, you will be prompted to use another port, allow this. Now we will use the RStudio Workbench extension to find our development server, in our case, 0.0.0.0:3001:

When you open one of these links you will find a blank page instead of the usual spinning React logo. Inspecting the Console will identify several issues. These all stem from the URL provided by the RStudio Workbench extension. We can easily resolve all of these problems.

Issue 1 – Incorrect application root

The example template (public/index.html) uses a variable PUBLIC_URL to enable development within different environments. The favicon has an href of %PUBLIC_URL%/favicon.ico.

Our application is now privately available at https://rstudio.jumpingrivers.cloud/workbench/s/ecb2d3c9ab5a71bf18071/p/fc2c1fd4/ for us to use and test while developing it. Click on a server in the RStudio Workbench extension to open your own link.

Create a file called .env.development in the root of your project with the following contents:

PUBLIC_URL=/workbench/s/ecb2d3c9ab5a71bf18071/p/fc2c1fd4

If npm start is still running, stop it now and restart it. Refresh your application now as well. The session and process IDs will usually remain the same so you will have another blank page, but fewer console errors.

Issue 2 – Incorrect routes in Express dev server

The files you are expecting are now missing because the development server uses the new PUBLIC_URL to serve content, but RStudio Workbench is removing the subdirectories when it maps back to 0.0.0.0:3000.

We can set up a proxy to server content on both “/workbench/s/ecb2d3c9ab5a71bf18071/p/fc2c1fd4” and “/”. Create a file “./src/setupProxy.js” with the following content:

module.exports = function (app) {
  app.use((req, _, next) => {
    if (!req.url.startsWith(process.env.PUBLIC_URL))
      req.url = process.env.PUBLIC_URL + req.url;
    next();
  });
};

When you now restart the dev server npm start and refresh the browser you will finally see a spinning React logo.

Two errors in the console remain.

Issue 3 – Invalid manifest

By definition, a web application manifest will not be requested with any authentication cookies. This is a very easy fix.

In “public/index.html” add crossorigin="use-credentials", e.g.

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

becomes:

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials" />

Refresh your application to see the changes immediately. In some cases you may require authentication in produciton, but if not then you should only enable it in development mode. We can use the already included HtmlWebpackPlugin to conditionally include our changes:

<% if (process.env.NODE_ENV === 'development') { %>
    <!-- enable authentication in dev mode only -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" crossorigin="use-credentials" />
<% } else { %>
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<% } %>

Note that if you are running your production application behind a protected endpoint, such as when using RStudio Connect, you may remove the conditional statement and include credentials in all cases.

Issue 4 – WebSocket connections

Finally, you may have noticed that auto-reload is not enabled and there are network errors every ~5 seconds.

We can fix this by intercepting all WebSocket connections made from our web page. Add a script tag to the head of “public/index.html”. As with the authenticated manifest, we can embed this script only when in development mode.

<% if (process.env.NODE_ENV === 'development') { %>
<script>
  const WebSocketProxy = new Proxy(window.WebSocket, {
    construct(target, args) {
      console.log("Proxying WebSocket connection", ...args);
      let newUrl = "wss://" + window.location.host + "%PUBLIC_URL%/ws";
      const ws = new target(newUrl);
      
      // Configurable hooks
      ws.hooks = {
        beforeSend: () => null,
        beforeReceive: () => null
      };
  
      // Intercept send
      const sendProxy = new Proxy(ws.send, {
        apply(target, thisArg, args) {
          if (ws.hooks.beforeSend(args) === false) {
            return;
          }
          return target.apply(thisArg, args);
        }
      });
      ws.send = sendProxy;
  
      // Intercept events
      const addEventListenerProxy = new Proxy(ws.addEventListener, {
        apply(target, thisArg, args) {
          if (args[0] === "message" && ws.hooks.beforeReceive(args) === false) {
            return;
          }
          return target.apply(thisArg, args);
        }
      });
      ws.addEventListener = addEventListenerProxy;
  
      Object.defineProperty(ws, "onmessage", {
        set(func) {
          const onmessage = function onMessageProxy(event) {
            if (ws.hooks.beforeReceive(event) === false) {
              return;
            }
            func.call(this, event);
          };
          return addEventListenerProxy.apply(this, [
            "message",
            onmessage,
            false
          ]);
        }
      });
  
      // Save reference
      window._websockets = window._websockets || [];
      window._websockets.push(ws);
      
      return ws;
    }
  });
  
  window.WebSocket = WebSocketProxy;
</script>
<% } %>

Refresh your applications and wait to confirm that there are no WebSocket connection errors any more.

References


For updates and revisions to this article, see the original post

To leave a comment for the author, please follow the link and comment on their blog: The Jumping Rivers Blog.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.