GitHub
LinkedIn
Twitter
YouTube
RSS

Recreating the Shiny App tutorial with a Plumber API + React: Part 2

Author: Liam Kalita

Published: July 14, 2022

This is part two of our three part series

In the first part of this series, we introduced the technologies and packages required to create an application using ReactJS and an R {plumber} API instead of {shiny}. In this post, we will take you through the tutorial itself.

Dependencies

Before we start we need to ensure we have the tools that this exercise depends on:

  • Node >= 10.16 and npm >= 5.6 - Node is a JavaScript runtime environment that allows us to run JavaScript outside of a Web browser, npm is software registry that runs on Node.js that we can use to download open source packages.
  • R - If you’re on this blog you’re probably familiar with R
  • R Plumber - The web API R Package
  • RStudio Connect - The publishing platform we want to host our content on
  • rsconnect R package - An R package used for deploying applications to RStudio Connect.

For the IDE I am using Visual Studio Code (VSCode), although which IDE you use is up to you. VScode is a popular code editor that offers many features to help developers be more productive. VScode has a wide range of extensions that can add even more functionality, such as support for various languages and tools to help with version control. I think it is a great choice of IDE for this tutorial as we’re making use of multiple languages.


Do you require help building a Shiny app? Would you like someone to take over the maintenance burden? If so, check out our Shiny and Dash services.


Let’s make the App

We’ll assume you have a basic understanding of HTML and JavaScript, but you should be able to follow along with a basic programming background. Having a little knowledge of Linux shell commands would be beneficial for some of the terminal commands for generating directories, but you can also do most of it in VSCode using the user interface instead.

Let’s attempt an exercise in creating a small React+Plumber app; this will be very similar to a previous blog post recreating this tutorial {shiny} application using Python Flask.

Screenshot of histogram of waiting times with bins slider.

This will consist of two independent parts:

  • The R Plumber API that will serve the data
  • The React UI that will consume the data from the API

Folder Structure

Let’s start by creating a directory containing the project and then also a directory to house the API we will create. The below is a bash script but use whichever way is easiest for you to create directories.

mkdir -p app_example/api

After this command the folder structure should look like this:

.
└── app_example
    └── api

We would like our folder structure to eventually look like this:

.
└── app_example
    ├── api
    └── example-app

But we will create the app directory later using a React command line tool.

Plumber.R (API)

.
└── app_example
    └── api
        └── plumber.R

Here is the code for the plumber.R file for our React app. It will contain a single endpoint who’s sole purpose is to return some histogram data that can be consumed by the React application.

We can create a new file under the API directory and add the following.

# plumber.R
#*@apiTitle Example Plumber API

#* Get Histogram raw data
#* @get /hist-raw
function(bins) {
   x = faithful$waiting
   bins = as.numeric(bins)
   breaks = seq(min(x), max(x), length.out = bins + 1)
   hist_out = hist(x, breaks = breaks, main = "Raw Histogram")
   as.data.frame(hist_out[2:6])
}

Note that at the top we have changed the title of our API with the “#*@apiTitle” prefix and we have named it “Example Plumber API”

This uses the “Old Faithful Geyser Data” dataset which is natively available within R. We then pass this data into a histogram function along with a user specified number of bins parameter, and transform this into a dataframe of raw information that describes a histogram that looks something like the following:

Histogram Function Output

  counts    density mids xname equidist
1     44 0.01526082 48.3     x     TRUE
2     50 0.01734184 58.9     x     TRUE
3     32 0.01109878 69.5     x     TRUE
4    117 0.04057991 80.1     x     TRUE
5     29 0.01005827 90.7     x     TRUE

runPlumber.R (API)

.
└── app_example
    └── api
        ├── runPlumber.R
        └── plumber.R

Our runPlumber.R file is dependent on the Plumber.R file above

library("plumber")
pr("plumber.R") %>%
  pr_run(port = 8000)

Here is our second R file. It passes our previous plumber.R file in a {plumber} router function and runs it on a port of our choice. In this example we have gone with port 8000.

We can check if the {plumber} API works by running our runPlumber.R with RScript from a terminal

RScript runPlumber.R 

If you’re attempting this on Windows you may need to specify the full path to the RScript executable in the R directory in Program Files which might look something like the following

"C:\Program Files\R\(R Version)\bin\x64\RScript.exe" runPlumber.R 

This should get the following output:

Running plumber API at http://127.0.0.1:8000
Running swagger Docs at http://127.0.0.1:8000/__docs__/

We can then view the documentation of our {plumber} app which has been created through swagger at the address given when viewed from a browser

http://127.0.0.1:8000/__docs__/

We can select the endpoint we want to check, click “Try it out”, then enter a number of bins and execute.

Screenshot of input screen requesting number of bins

If we enter 5 as the number of bins we should get a response similar to the Output below.

[
  {
    "counts": 44,
    "density": 0.0153,
    "mids": 48.3,
    "xname": "x",
    "equidist": true
  },
  {
    "counts": 50,
    "density": 0.0173,
    "mids": 58.9,
    "xname": "x",
    "equidist": true
  },
  {
    "counts": 32,
    "density": 0.0111,
    "mids": 69.5,
    "xname": "x",
    "equidist": true
  },
  {
    "counts": 117,
    "density": 0.0406,
    "mids": 80.1,
    "xname": "x",
    "equidist": true
  },
  {
    "counts": 29,
    "density": 0.0101,
    "mids": 90.7,
    "xname": "x",
    "equidist": true
  }
]

If this works for you then the {plumber} API example is completed for now. We just need to make the React Application that consumes this API data.

React Application

We want to change directory to the parent app_example directory and create a React application here. Using create-react-app is the best to way to start creating a single page react application

npx create-react-app example-app
cd example-app
npm start

npx is an npm command allowing us to run a package without downloading it, so we can run the create-react-app package without storing the node module.

This generates a react application directory within the directory we are in, with the name that we give to the create-react-app command; in this case it is example-app, but you may call it whatever you wish. We can then change directory into the created example-app directory and use npm start to start a development server hosting the example app.

A development server updates while runnning when it detects changes in the source code detected in the src subdirectory. We can now view the example app if we navigate to localhost:3000 in a web browser.

Screenshot of react logo with text: Edit src/App.js and save to reload. Learn React.

We can stop the development server for now using Ctrl+C in the terminal hosting the app.

npm dependencies

We now need to install some npm dependencies using npm from within our example-app directory

npm install react-bootstrap bootstrap react-plotly.js plotly.js rc-slider axios lodash

This will install packages containing some open source React components that we will use in our application

  • react-bootstrap - a React package for Bootstrap
  • bootstrap - a styling library for quickly designing UIs
  • react-plotly.js - a React wrapper for plotly.js
  • plotly.js - a dependency for react-plotly.js a graphing library which we will use to consume our histogram data
  • rc-slider - a React slider component which we will use to select the number of bins
  • axios - a Promise based HTTP client which we will use to make requests to our API
  • lodash - a performance and utility library which we will use to debounce requests from the rc slider

JSX

The JavaScript files in the following sections will contain JSX which is a syntax extension for JavaScript which you may not be familiar with if you have only had experience with base JavaScript. JSX converts into base JavaScript when compiled, the two below snippets are identical in functionality.

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

React doesn’t require using JSX, but most people find it helpful as a visual aid when working with user interfaces as it is structurally very similar to HTML.

Further JSX information can be found here

index.js

The first file that we start with in a react project is index.js. It typically handles app startup and calls the Application component. It is the first file the web server seeks. For the purpose of this tutorial we can leave the index.js file mostly as it comes. In the render function of index.js we see HTML like tags - the “<App />” tag calls our App component which is exported from App.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

App.js Component Full Code

For this tutorial we only really need to edit the App.js file to change the App component that Index.js uses. In the src folder, we want to remove all the current code in App.js and replace it with the following, the code will be explained section by section after.

import React from 'react';

import 'bootstrap/dist/css/bootstrap.min.css';
import { Container, Col, Row, Card } from 'react-bootstrap';
import axios from 'axios';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
import Plot from 'react-plotly.js';
import { debounce } from 'lodash';

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
    };
    this.onSliderChange = this.onSliderChange.bind(this);
  }

  async onSliderChange(input) {
    await axios.get(`http://localhost:8000/hist-raw`, {
    {
        params: {
        bins: input,
      }
    }).then((data) => {
      this.setState({
        rawdata: [
          {
            y: data.data.map(x => x["counts"]),
            x: data.data.map(x => x["mids"]),
            type: 'bar'
          }
        ]
      })
    });
  }

  render() {
    return (
      <div className="App">
        <Container fluid>
          <Row>
            <Col md={3}>
              <Card>
                <Card.Body>
                  <Card.Title>Hello React!</Card.Title>
                  <Card.Text>
                    <label for="bins" class="col-form-label">
                      Number of bins:
                    </label>
                    <Slider 
                    id={"bins"} 
                    onChange={debounce(this.onSliderChange, 60)} 
                    min={1} 
                    max={50} 
                    marks={{
                      1: '1',
                      13: '13',
                      26: '26',
                      38: '38',
                      50: '50'
                    }} toolTipVisibleAlways={true} />
                  </Card.Text>
                </Card.Body>
              </Card>
            </Col>
            <Col md={8}>
              <Plot
                data={this.state.rawdata}
                layout={{
                  title: 'Histogram of waiting times',
                  bargap: 0.01,
                  autosize: true,
                  xaxis: {
                    title: 'Waiting time to next eruption (in mins)'
                  },
                  yaxis: {
                    title: 'Frequency'
                  },
                  useResizeHandler: true,
                  responsive: true
                }}
              />
            </Col>
          </Row>
        </Container>
      </div>
    );
  }
}

export default App;

App.js Code Breakdown

We will breakdown the above code the explain the individual elements

import React from 'react';

import 'bootstrap/dist/css/bootstrap.min.css';
import { Container, Col, Row, Card } from 'react-bootstrap';
import axios from 'axios';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
import Plot from 'react-plotly.js';
import { debounce } from 'lodash';

The above imports React components and css files from our node packages that we have installed through npm and allows us to use them in our App.js

Constructor

Here we create and open a React component class with a constructor and initialize its state with an empty state object. A constructor is a function that runs when the component is created. We bind our onSliderChange function to the component instance, this binding is necessary to make the keyword "this" work in the callback and allow us to pass through our OnClickEvent to a child component (in this case the Slider).

class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
    };
    this.onSliderChange = this.onSliderChange.bind(this);
  }

What is binding for? In JavaScript the following two snippets are not equivalent:

obj.method();
var method = obj.method;
method();

Binding ensures that the second snippet has the same behaviour as the first one. With React we need to bind the methods that we pass into other components.

onSliderChange()

The onSliderChange function makes a get request to the our {plumber} API (http://localhost:8000/hist-raw) using axios, we send it a params object containing a number of bins and it sends us back some histogram data.

async onSliderChange(input) {
    await axios.get(http://localhost:8000/hist-raw, 
      {
        params: {
        bins: input,
      }
    }).then((data) => {
      this.setState({
        rawdata: [
          {
            y: data.data.map(x => x["counts"]),
            x: data.data.map(x => x["mids"]),
            type: 'bar'
          }
        ]
      })
    });
  }

Once we recieve this data we manipulate it with map() functions and store the required data within a rawdata object we have created. This is formatted using the data format required in the Plotly package. This object is stored using this.SetState within the state of the App component.

react-bootstrap Layout

We have some react-bootstrap components Container Row Col Card to describe the layout and design. Components can appear inside other components similar to how DOM elements can appear inside other DOM elements in HTML.

  render() {
    return (
      <div className="App">
        <Container fluid>
          <Row>
            <Col md={3}>
              <Card>
                <Card.Body>
                  <Card.Title>Hello React!</Card.Title>
                  <Card.Text>
                    <label for="bins" class="col-form-label">
                      Number of bins:
                    </label>
                    <Slider 
                    id={"bins"} 
                    onChange={debounce(this.onSliderChange, 60)} 
                    min={1} 
                    max={50} 
                    marks={{
                      1: '1',
                      13: '13',
                      26: '26',
                      38: '38',
                      50: '50'
                    }}/>
                  </Card.Text>
                </Card.Body>
              </Card>
            </Col>
            <Col md={8}>
              <Plot
                data={this.state.rawdata}
                layout={{
                  title: 'Histogram of waiting times',
                  bargap: 0.01,
                  autosize: true,
                  xaxis: {
                    title: 'Waiting time to next eruption (in mins)'
                  },
                  yaxis: {
                    title: 'Frequency'
                  },
                  useResizeHandler: true,
                  responsive: true
                }}
              />
            </Col>
          </Row>
        </Container>
      </div>
    );
  }

Some of these components have properties that we can change within their opening tag. The value of the md property of columns can be changed to determine the width of them. More information on the layout properties can be found here

Slider

Within the Card.Text section we have added the slider component with properties to describe it. The part I’d like to draw attention to is the onChange property.

<Slider 
  id={"bins"} 
  onChange={debounce(this.onSliderChange, 60)} 
  min={1} 
  max={50} 
  marks={{
    1: '1',
    13: '13',
    26: '26',
    38: '38',
    50: '50'
}}/>

The onChange property is the function that is executed when the value of parent changes. In this case it is the Slider component. We set the onChange property to call the bound OnSliderChange function we created previously. We also wrap the function in a lodash debounce function making use of one of our npm dependencies.

The purpose of this is to reduce the amount of requests made to the API by only sending a request once the user has finished changing the value of the slider for a set amount of time. If we didn’t add this in, every tick of the slider change would trigger an HTTP request to our API. We only want to trigger one request once the slider has stopped changing for 60ms.

Plot

Here we have our plot again with some properties.

<Plot
data={this.state.rawdata}
layout={{
  title: 'Histogram of waiting times',
  bargap: 0.01,
  autosize: true,
  xaxis: {
    title: 'Waiting time to next eruption (in mins)'
  },
  yaxis: {
    title: 'Frequency'
  },
  useResizeHandler: true,
  responsive: true
}}
/>

Note that the data property calls upon this.state.rawdata. This means when the state changes of the App via the OnSliderChange function this Plot component will update with the new rawdata state. A Plotly plot also takes a layout parameters object to describe the axes and the styling of the graph.

Boilerplate Cleanup

We can tidy up some boilerplate files that have been generated. Since we are using bootstrap for our css we don’t need the generated css files and can remove them.

App.css
index.css

and we can remove the import line from our index.js file

import './index.css';

Trying Out the Application

If we run both the App and the {plumber} API and visit the url for the app we will most likely see this: Screenshot of empty histogram axes and bins slider set to minimum. Unfortunately if you try the slider nothing happens, and you may have also opened the developer console to discover our requests being blocked by the CORS policy. This is a security feature to help reduce possible CORS related attack vectors.

Cross-Origin Resource Sharing (CORS)

Cross-origin resource sharing (CORS) is a browser mechanism which enables controlled access to resources located outside of a given domain. More information on CORS can be found here. JavaScript treats both our application and our API as different origins because they are running on different ports. In order to test out our app and API locally we need to append this to our Plumber.R file in our API to include an Access-Control-Allow-Origin header with a response

#temporary testing purposes
#* @filter cors
cors = function(res) {
    res$setHeader("Access-Control-Allow-Origin", "http://localhost:3000")
    plumber::forward()
}

We should be able to restart the API and the slider should now update the graph. Excellent! Screenshot of histogram of waiting times with bins slider.

That’s it for part 2! In part 3 of our series, we will show you how to host on RStudio Connect!


Jumping Rivers Logo