Recreating the Shiny App tutorial with a Plumber API + React: Part 2
This is part two of our three part series
- Part 1: Recreating the Shiny App tutorial with a Plumber API + React: Part 1
- Part 2: Recreating the Shiny App tutorial with a Plumber API + React: Part 2 (this post)
- Part 3: Recreating the Shiny App tutorial with a Plumber API + React: Part 3
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.
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.
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.
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.
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: 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!
That’s it for part 2! In part 3 of our series, we will show you how to host on RStudio Connect!