28 R + Shiny + React: welcome {reactR}

React is an open source library designed to quickly develop user interfaces or UI components, on the front end. It has been developed by Facebook and the community (more than 1500 contributors) and made public in May 29 2013. It is currently used worldwide and has around 163k stars on the Github ranking and widely impacts the mobile market, through the React Native library. React is also really convenient to develop attractive documentations with docusaurus. If you ever have built user interfaces with pure JS, you might like React!

Below, we give a short introduction to the React ecosystem and see how we can benefit from it from the R Shiny side.

28.1 Quick introduction to React

To understand React there are few prerequisites notably basic HTML/CSS/JS knowledge, especially JS modules (see section 10.4.6.1). However, if you managed to reach this chapter, you should not worry too much!

28.1.1 Setup

Node and npm are required. If you are not sure, run:

node -v
npm -v

At that stage it is also good to have yarn as we’ll need it for reactR.

If nothing is returned, please refer to section 10.3.1. To initiate a React project, we leverage the npx command:

npx create-react-app <PROJECT_NAME>

Replace <PROJECT_NAME> by the real name of your project. If this seems intimidating, keep in mind this is the same concept has using the golem package to initiate the creation of robust shiny projects, except that we work from the terminal.

Once done (the package initialization takes some time), move to the project folder and launch the demo app:

cd <PROJECT_NAME> && npm start

If you have yarn, yarn start also works.

You should see something similar to Figure 28.1.

npm start opens the react app

FIGURE 28.1: npm start opens the react app

Congrats! You are running your first React app!

28.1.2 Basics

We are now all set up to start learning the basics of React. Among all created files, notice the /src/app.js file. Inside the App function, we remove all the content inside the return statement to put a simple <h1>Hello, world!</h1> HTML title. We also clean the imports as we don’t need any CSS and logo anymore. We obtain:

function App() {
  return (
    <h1>Hello, world!</h1>
  );
}

// don't remove, this is needed by index.js
export default App; 

Once done, we run npm build (or yarn build), to rebuild the JS code and npm start to restart the server and preview the app. In practice, once the server is launched, there is no need to restart it whenever the code is updated. A simple save will refresh the interface!

The code you see above is a React component. The definition is rather simple: it is a function that returns a moreless complex piece of UI. How are components rendered by React?

So far, we didn’t have a look at the /src/index.js script. Inside we find:

ReactDOM.render(
  App,
  document.getElementById('root')
);

Recent version of npx create-react-app have <React.StrictMode><App /></React.StrictMode> instead of App, which does the same thing. You may also find <App />. In practice, we rarely modify this part.

In short, this inserts the App component inside the element having root as id in the main HTML page. This HTML skeleton may be found in the public/index.html folder. You may imagine that at the end of the day, our app will be composed of multiple bricks and call ReactDOM.render on the top level component.

28.1.2.1 About JSX

We just wrote our first React component. Didn’t you notice something weird in that code? JS and HTML are mixed, in what we called JSX, that is a syntax extension to JS. JSX makes the code less verbose, for instance:

React.createElement(
  'h1',
  'Hello, world!'
);

does exactly the same thing as above but when the code becomes more complex, it is nearly impossible to read.

Let’s see how to pass variables into JSX. We want to show Hello, <Your Name>, we store the name in a variable and modify the app.js code accordingly:

function App() {
  const name = "David";
  return (
    <h1>Hello, {name}</h1>
  );
}

Expressions are passed within curly brackets {expression} and you may even call functions inside. Tag attributes also require curly brackets. Let’s modify the title tag to give it a color and a size.

function App() {
  const name = "David";
  return (
    <h1 style={color: "red", fontSize: 40}>Hello, {name}</h1>
  );
}

Try to save. Why does this fail? We can’t pass multiple object properties inside a single {}. We need either double brackets like {{object properties: values, ...}} or to store the object in a variable before:

function App() {
  const name = "David";
  return (
    <h1 style={{color: "red", fontSize: 40}}>Hello, {name}</h1>
  );
}

// OR

function App() {
  const name = "David";
  const myStyle = {
    color: "red", 
    fontSize: 40
  }
  return (
    <h1 style={myStyle}>Hello, {name}</h1>
  );
}

Notice that we write CSS properties following the camelCase syntax, font-size being equivalent to fontSize.

28.1.2.2 Combining components

The whole interest is to combine multiple components to create reusable pieces. We edit the above code to create a SayHello component. Notice the props parameter. It is a way to pass configuration from the parent component. In that case, we want to display the person name, that is props.name. In the meantime, we edit the App parent component and call SayHello three times, passing a different name like <SayHello name="David" /> (this is the reason why we recover props.name in the lower level component):

function SayHello(props) {
  return (
    <h1>Hello, {props.name}</h1>
  );
}


function App() {
  return(
  <>
    <SayHello name="David" />
    <SayHello name="Lisa" />
    <SayHello name="Simon" />
  </>
  );
}

Notice the enclosing <>...</>. This is called a React fragment and useful if we don’t want to insert any extra <div> in the DOM.

We could be even more efficient by leveraging the lists capabilities. We create an array of names and apply the map method to return the corresponding <SayHello /> sub-component:

const names = ["David", "Lisa", "Simon"];
function App() {
  const sayHelloToAll = names.map(
    (name) => <SayHello key={name} name={name} />
  ); 
  return(sayHelloToAll);
}

By convention, all elements inside a map require keys.

Props are read-only and must not be modified withing their own component. How do we update components then?

28.1.2.3 Component state

A component state is private and controlled by this same component. Since React 16.8 and the introduction of hooks, this is not necessary to convert the component function to a class. The easiest example to illustrate hooks capabilities is the button. Each time, we click on a button, we want to keep the current number of clicks in a state, like the actionButton(). We start by importing the useState function from react and create a new ActionButton component. Inside, we set the state with useState that create the state variable count as well as the function to update it, namely setCount. This way to create two variables at once is called array destructuring. We set the initial number of counts to 0:

import {useState} from 'react';

function ActionButton() {
  const [count, setCount] = useState(0);
}

Next step is to create the button skeleton. We provide an onClick property that updates the button state. Inside, we write an anonymous function which calls setCount and increments the count value by 1. At this point, you may replace the app.js content by:

import {useState} from 'react';

function ActionButton() {
  const [count, setCount] = useState(0);
  return(
    <button onClick={() => setCount(count + 1)}>
      Number of clicks: {count}
    </button >
  );
}

function App() {
  return (
    <ActionButton />
  );
}

export default App;

We may improve the previous app and add a reset button within the ActionButton component that sets the count back to 0:

function ActionButton() {
  const [count, setCount] = useState(0);
  return(
    <>
    <button onClick={() => setCount(count + 1)}>
      Number of clicks: {count}
    </button >
    <button onClick={() => setCount(0)}>
      Reset
    </button >
    </>
  );
}

It would make more sense to only show the reset button once the button has been clicked, at least once. We define the isResetVisible variable which is true whenever the count is higher than 0 and false if the count is 0, leveraging the JS ternary operator. We store the reset button in a variable and only return something if isResetVisible is true:

function ActionButton() {
  const [count, setCount] = useState(0);
  const isResetVisible = count > 0 ? true : false;
  let resetButton;
  if (isResetVisible) {
    resetButton = <button onClick={() => setCount(0)}>
      Reset
    </button >
  }
  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        Number of clicks: {count}
      </button >
      {resetButton}
    </>
  );
}

That’s all folks! Now that you get a better understanding of how React works, let’s see how to use it with Shiny.

28.2 Introduction to {reactR}

The React ecosystem is rich and provides many plug and play boilerplate. For instance, {reactable} is an HTML widget developed on top of the react-table library. What about the modern React Argon design system, shown Figure 28.2?

Argon design template with React

FIGURE 28.2: Argon design template with React

In this part, we see how to leverage reactR to bring the Argon design React template to Shiny.

To install reactR:

install.packages("reactR")
remotes::install_github("react-R/reactR")

28.2.1 Scaffold inputs

28.2.1.1 Setup

reactR exposes the scaffoldReactShinyInput() that sets the necessary pieces of code to get started with the input development (see Figure 28.3):

  • An <input_name>.R file containing the input skeleton as well as its related update function. It also creates a custom HTML dependency pointing to the input JS logic.
  • A jsx file with a boilerplate to start developing the input JS logic.
  • A package.json file containing dependencies.

Besides, providing an optional dependency with a name and version taken from npm installs all necessary dependencies in the package.json file.

In our case, we want to build on top of Argon from here:

path <- file.path(tempdir(), "mypkg")
usethis::create_package(path, rstudio = TRUE)
reactR::scaffoldReactShinyInput(
  "argon_action_button", 
  list(
    name = "argon-design-system-react", 
    version = "^1.1.0"
  )
)
Package initialization with {reactR}

FIGURE 28.3: Package initialization with {reactR}

Note that reactR provides webpack as JS manager. To build the JS code we go to the RStudio terminal tab (or any terminal) and run at the package root:

yarn install
yarn run webpack --mode=development

This installs all dependencies listed in the package.json file and create a package-lock.json file. If you ever used renv, this is very similar and guarantees isolation of your package. All dependencies are stored in the node_modules folder.

We encountered few cases where the yarn install failed. In that case, you may try npm i argon-design-system-react and also check your VPN settings. To check whether argon is properly installed, have a look inside the package node_modules folder.

The run webpack command compiles the JS in production ready code. Should you need to customize the build pipeline, the webpack configuration is located in webpack.config.js. On the R side, the generated JS file is referenced in an HTML dependencies, located in the <input_name>.R file. If you specify the --mode=development tag, it falls back to development code.

28.2.1.2 Implement the input JS logic

It’s time to develop the logic. The Argon documentation provides a boilerplate, which we slightly simplified, as show below:

import { reactShinyInput } from 'reactR';
// reactstrap components
import { Button } from "reactstrap";

function ArgonButton() {
  return (
    <Button color="primary" type="button">
      Button
    </Button>
  );
}

reactShinyInput(
  '.argon_action_button', 
  'mypkg.argon_action_button', 
  ArgonButton
);

As you can see, we also need reactstrap which is a Bootstrap React template.

Notice the first two lines where we import the reactShinyInput function from the reactR JS core and reactstrap, a React powered Bootstrap 4 library. reactShinyInput provides a wrapper to automatically bind the input element. This is extremely convenient as it allows to solely focus on the component logic rather than binding it to the shiny system. It takes three parameters:

  • The element class which is obtained from the R side by createReactShinyInput().
  • The input binding name which is useful to store a reference in the Shiny.InputBindings registry.
  • The React component function.
  • There is an optional fourth parameter allowing to pass custom options like rate policies.

Overall, reactShinyInput extends the Shiny.InputBinding class by providing extra methods like:

getInputConfiguration(el) {
  return $(el).data('configuration');
}
setInputConfiguration(el, configuration) {
  $(el).data('configuration', configuration);
}

getInputConfiguration and setInputConfiguration, respectively get and set the user provided configuration, passed in the createReactShinyInput() R side function. Under the hood, in addition to ship the reactR, React, ReactTools HTML dependencies (and any user defined custom dependency), createReactShinyInput() does generate three tag elements:

  • The first tag is the element placeholder containing the unique id. React will insert the component inside this target with ReactDOM.render.
  • The second tag is a script containing the value passed as JSON.
  • The second tag is a script containing the configuration, also passed as JSON.
reactR::createReactShinyInput(
  inputId = "plop", 
  class = "myinput", 
  dependencies = htmltools::findDependencies(icon("bicycle")), 
  default = 0, 
  configuration = list(a = 1, b = "test"), 
  container = div
)

The configuration and values are processed in the initialize method, which is a great place since it is called before the input is bound:

$(el).data('value', JSON.parse($(el).next().text()));
$(el).data('configuration', JSON.parse($(el).next().next().text()));

They are stored in the corresponding data attributes. The most important part is the render method that creates the React element , based upon its configuration, value and renders it in the DOM:

render(el) {
  const element = React.createElement(component, {
    configuration: this.getInputConfiguration(el),
    value: this.getValue(el),
    setValue: this.setValue.bind(this, el),
    el: el
  });
  ReactDOM.render(element, el);
}

The render method is called once inside subscribe and also each time the element is updated with receiveMessage.

Interestingly, setValue is made available to the React component. For the action button case, it is called each time the onClick event is triggered, that is each time the user clicks on the button. This is the only way to update its value since it is not accessible to the user through update_argon_action_button(). Other inputs like textInput() have the onChange event, fired each time the text field is updated. The value may also be updated by the user with updateTextInput(). Consequently, there is no general rule and the situation may vary from an input to another!

For now, let’s just erase the {creatR} boilerplate (argon_action_button.jsx) with the above code an recompile with yarn run webpack. We also have to tell Shiny we want to use Bootstrap 4 instead of Bootstrap 3, through the bslib::bs_theme function:

devtools::document()
devtools::load_all()
library(shiny)
library(mypkg)

ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  argon_action_button("plop")
)

server <- function(input, output, session) {
  
}

shinyApp(ui, server)

The button is looking good but not really like in the Argon design system. Indeed, we forgot to import the argon CSS assets. Add this code to the argon_action_button.jsx:

import "argon-design-system-react/src/assets/css/argon-design-system-react.min.css";

Under the hood, webpack knows how to load CSS in the webpack.config.js:

rules: [
  {
      test: /\.jsx?$/,
      loader: 'babel-loader',
      options: {
          presets: ['@babel/preset-env', '@babel/preset-react']
      }
  },
  // For CSS so that import "path/style.css"; works
  {
      test: /\.css$/,
      use: ['style-loader', 'css-loader']
  }
]

The next part of this tutorial consists in making the button interactive. We edit the argon_action_button.jsx code to add three input parameters in the ArgonButton component:

  • value is the button count. The initial value is provided by the default slot on the R side (see below).
  • configuration contains various user provided properties like the color status, outline style, …
  • setValue is a way to increment the button value. Letting setValue(value + 1), ensures to increment the button value by 1 unit each click.
import { reactShinyInput } from 'reactR';
// reactstrap components
import { Button } from "reactstrap";
// Import argon CSS
import "argon-design-system-react/src/assets/css/argon-design-system-react.min.css";

function ArgonButton({configuration, value, setValue}) {
  return (
    <Button 
      color={configuration.status} 
      type="button" 
      onClick={() => setValue(value + 1)}>
      {configuration.label}
    </Button>
  );
}

reactShinyInput(
  '.argon_action_button', 
  'mypkg.argon_action_button', 
  ArgonButton
);

On the R side, we remove the default parameter from the external API but keep it internally to set the initial value to 0, like for shiny::actionButton. Status and label are stored in the configuration named list. Be careful! Names matter since they are recovered on the JS side with configuration.prop_name:

argon_action_button <- function(
  inputId, 
  label, 
  status = "primary"
) {
  reactR::createReactShinyInput(
    inputId,
    "argon_action_button",
    htmltools::htmlDependency(
      name = "argon_action_button-input",
      version = "1.0.0",
      src = "www/mypkg/argon_action_button",
      package = "mypkg",
      script = "argon_action_button.js"
    ),
    default = 0,
    configuration = list(
      label = label,
      status = status
    ),
    htmltools::tags$div
  )
}

Once everything is up to date, we rebuild the JS, reload/document the package and run the app demo:

ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  argon_action_button("plop", "Click me!")
)

server <- function(input, output, session) {
  observe(print(input$plop))
}

shinyApp(ui, server)

We may also modify the update input boilerplate since we don’t change the button value:

update_argon_action_button <- function(
  session, 
  inputId, 
  configuration = NULL
) {
  message <- list()
  if (!is.null(configuration)) message$configuration <- configuration
  session$sendInputMessage(inputId, message);
}

devtools::document()
devtools::load_all()
ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  fluidRow(
    argon_action_button("plop", "Click me!"),
    argon_action_button("update", "Update button 1")
  )
)

server <- function(input, output, session) {
  observe(print(input$plop))
  observeEvent(input$update, {
    update_argon_action_button(
      session, "plop", 
      configuration = list(
        label = "New text", 
        status = "success"
      )
    )
  }, ignoreInit = TRUE)
}

shinyApp(ui, server)

28.2.1.3 Exercise

  1. Add a size, outline and icon (from fontawesome) parameters to the ArgonButton component in the corresponding R script. Hint: the icon parameter is the trickiest one. You might find helpful to capture its HTML dependency with htmltools::htmlDependencies(iconTag) so as to properly render it. In HTML icon("bicycle") produces <i class="fa fa-bicycle" role="presentation" aria-label="bicycle icon"></i>. However, in React we want <i className="fa fa-bicycle />. Therefore, you will have to extract the icon class and send it to JS in the configuration list.
  2. Implement the logic on the JS side. Hint: you may use the below code:
import { reactShinyInput } from 'reactR';
// reactstrap components
import { Button } from "reactstrap";
// import argon CSS
import "argon-design-system-react/src/assets/css/argon-design-system-react.min.css";

function ArgonButton({configuration, value, setValue}) {
  let iconTag, btnCl, innerTag;
  if (...) {
    btnCl = "btn-icon";
    innerTag = <>
      <span className="btn-inner--icon">
        <i className=... />
      </span>
      <span className="btn-inner--text">...</span>
    </>;
  } else {
    innerTag = configuration.label;
  }

  let outlined;
  if (...) {
    outlined = true;
  }

  return (
    <Button
      color={configuration.status}
      type="button"
      className=...
      outline=...
      size=...
      onClick={() => setValue(value + 1)}>
      {innerTag}
    </Button>
  );
}

reactShinyInput(
  '.argon_action_button', 
  'mypkg.argon_action_button', 
  ArgonButton
);
  1. Try your code with:
library(shiny)
library(mypkg)

ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  fluidRow(
    argon_action_button(
      "plop", 
      "Click me!", 
      size = "lg", 
      outline = TRUE
    ),
    argon_action_button(
      "update", 
      "Update button 1", 
      icon = icon("bicycle")
    )
  )
)

server <- function(input, output, session) {
  observe(print(input$plop))
  observeEvent(input$update, {
    update_argon_action_button(
      session, "plop", 
      configuration = list(
        label = "New text", 
        status = "success"
      )
    )
  }, ignoreInit = TRUE)
}

shinyApp(ui, server)

You should get the result shown Figure 28.4.

Argon Action button with React

FIGURE 28.4: Argon Action button with React

28.2.2 A slider input

Below we propose a preliminary implementation of the Argon slider input. The documentation provides a JSX skeleton:

{/* Simple slider */}
<div className="input-slider-container">
  <div className="slider" ref="slider1" />
  <Row className="mt-3 d-none">
    <Col xs="6">
      <span className="range-slider-value">
        {this.state.simpleValue}
      </span>
    </Col>
  </Row>
</div>

The slider JS API is actually taken from the well known noUiSlider library and is already available in the package node modules. Still in the same package, we can call:

reactR::scaffoldReactShinyInput("argon_slider", edit = FALSE)

It creates a new srcjs/argon_slider.jsx script. As Argon uses the pure JS API for noUIslider, this would be too much work for us mainly because the provided reactShinyInput JS helper does not have a proper initialization method to create the slider instance. We would have to change it as per below:

initialize(el) {
  // Unchanged compared to reactR
  $(el).data('value', JSON.parse($(el).next().text()));
  $(el).data(
    'configuration', 
    JSON.parse($(el).next().next().text())
  );
  
  // Create the slider instance 
  self = this;
  Slider.create($(el).attr(id), {
    start: self.getInputValue(el),
    connect: [true, false],
    step: self.getInputConfiguration(el).step,
    range: { 
      min: self.getInputConfiguration(el).min, 
      max: self.getInputConfiguration(el).max 
    }
  });
}

as well as updating methods like setInputValue, setInputConfiguration. Fortunately, there is already a nouislider-react API, which will makes our job much easier than shown above. To add the new dependency we run:

yarn add nouislider-react
// or
npm i nouislider-react

and replace the import statement inside our JSX file to rely on nouislider-react. The Nouislider component markup is fairly intuitive. We set the min and max as well as the current value:

import { reactShinyInput } from 'reactR';
import Nouislider from "nouislider-react";

function ArgonSlider({configuration, value, setValue}) {
  const rangeOpts = {
    min: configuration.min,
    max: configuration.max
  };
  return(
    <Nouislider
      range={rangeOpts}
      start={value}
      connect={[true, false]}
    />
  );
}

reactShinyInput(
  '.argon_slider', 
  'mypkg.argon_slider', 
  ArgonSlider
);

The connect property makes the slider bar background filled with the theme color. Figure 28.5 shows what happens when this parameter is disabled.

As we start to accumulate components, it is good practice to start modularizing our code. We create a main.jsx file:

nano srcjs/main.jsx

which contains the necessary code to load the common CSS assets, import both button and slider components and initialize them:

// Import argon CSS
import "argon-design-system-react/src/assets/css/argon-design-system-react.min.css";

import initArgonButton from './argon_action_button.jsx';
import initArgonSlider from './argon_slider.jsx';

initArgonButton();
initArgonSlider();

The argon_slider.jsx looks like, where we export a default function, namely initArgonSlider:

import { reactShinyInput } from 'reactR';
import Nouislider from "nouislider-react";

function ArgonSlider({configuration, value, setValue}) {
  const rangeOpts = {
    min: configuration.min,
    max: configuration.max
  };
  return(
    <Nouislider
      range={rangeOpts}
      start={value}
      connect={[true, false]}
    />
  );
}

export default function initArgonSlider(){
  return reactShinyInput(
    '.argon_slider',
    'mypkg.argon_slider',
    ArgonSlider
  );
}

We have to modify the webpack.config.js to change the entry point to main.jsx, whose output will be argon.js:

entry: [
  path.join(__dirname, 'srcjs', 'main.jsx')
],
output: {
  path: path.join(__dirname, 'inst/www/mypkg/argon'),
  filename: 'argon.js'
}

This means we have to update the HTML dependency on the R side, for instance for the argon_slider_input.R:

argon_slider_input <- function(inputId, value, default = value, min, max) {
  reactR::createReactShinyInput(
    inputId,
    "argon_slider",
    htmltools::htmlDependency(
      name = "argon",
      version = "1.0.0",
      src = "www/mypkg/argon",
      package = "mypkg",
      script = "argon.js"
    ),
    default,
    list(
      min = min,
      max = max
    ),
    htmltools::tags$div
  )
}

After rebuilding the JS and R package, we can run the app below:

ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  argon_slider_input("plop", 10, min = 0, max = 100)
)

server <- function(input, output, session) {
  observe(print(input$plop))
}

shinyApp(ui, server)

As shown on Figure 28.5, the slider is properly displayed and the value is recovered from the server. However, nothing happens when the range is dragged. The reason is quite simple: we did not set the setValue inside our JSX code yet.

Slider input with Argon React

FIGURE 28.5: Slider input with Argon React

The slider API provides many events but not all are suitable for us. Ideally, we would like an update:

  • Each time the range is released after dragging.
  • Each time the range is moved by arrow key (keyboard).

This seems like a perfect shot for the onChange prop. Let’s add it to the ArgonSlider component:

function ArgonSlider({configuration, value, setValue, el}) {
  const rangeOpts = {
    min: configuration.min,
    max: configuration.max
  };

  return(
    <Nouislider
      range={rangeOpts}
      start={[value]}
      connect={[true, false]}
      onChange={() => setValue()}
    />
  );
}

What value should we pass to setValue? This is quite easy to recover the slider value directly within the React component. We pass a parameter to the function, which will recover the latest value:

value => setValue(parseFloat(value))

The trick is to convert the value to a number since we recover a string by default. You may use parseInt or parseFloat, depending whether you want a integer or the exact value. Right now, the slider cannot be moved with the keyboard. Adding the keyboardSupport prop make it possible. This may correspond to a parameter provided in the argon_slider_input() configuration. The tooltips parameter enhances the user experience by showing the current value, on top of the slider, while pips add a simple grid. Not mentioned in the documentation, we add some CSS properties (top and bottom margins) to improve the display. Importantly, as the update slider function passes a named list to JS, any missing property is dropped, which can lead to undefined values on the JS side and unexpected behavior. Therefore, it is good to put default values whenever necessary to avoid this kind of issue. For instance, assume you passed orientation = "horizontal" upon slider creation and forgot to put that parameter in the update list, you can do the following in the JSX code:

if (configuration.orientation === undefined) {
  configuration.orientation = "horizontal";
}

The whole component code may be found below:

function ArgonSlider({configuration, value, setValue, el}) {
  const rangeOpts = {
    min: configuration.min,
    max: configuration.max
  };
  
  // Better margins
  const sliderStyle = {marginTop: "50px", marginBottom: "50px"};
  
  // Grid
  const pipOpts = {
    mode: 'range',
    density: 3
  };
  
  return(
    <Nouislider
      style={sliderStyle}
      range={rangeOpts}
      start={[value]}
      connect={[true, false]}
      onChange={value => setValue(parseFloat(value))}
      keyboardSupport={configuration.keyboard}
      tooltips={configuration.tooltips}
      pips={pipOpts}
    />
  );
}

The argon_slider_input() is given by:

argon_slider_input <- function(inputId, value, default = value, min, max, keyboard = TRUE,
                               tooltips = TRUE) {
  reactR::createReactShinyInput(
    inputId,
    "argon_slider",
    htmltools::htmlDependency(
      name = "argon",
      version = "1.0.0",
      src = "www/mypkg/argon",
      package = "mypkg",
      script = "argon.js"
    ),
    default,
    list(
      min = min,
      max = max,
      keyboardSupport = keyboard,
      tooltips = tooltips
    ),
    htmltools::tags$div
  )
}

There are many more parameters that can be added to the following API. We leave it as an exercise for the reader.

Finally, let’s see how update_argon_slider_input() works:

ui <- fluidPage(
  theme = bslib::bs_theme(version = "4"),
  argon_slider_input("plop", 10, min = 0, max = 100),
  br(),
  argon_action_button("update", "Update button 1")
)

server <- function(input, output, session) {
  observe(print(input$plop))
  observeEvent(input$update, {
    update_argon_slider_input(
      session, "plop",
      value = 100,
      configuration = list(
        min = 0,
        max = 200,
        tooltips = FALSE,
        keyboardSupport = FALSE
      )
    )
  }, ignoreInit = TRUE)
}

shinyApp(ui, server)

The keyboardSupport option does not seem changed, neither is the tooltips option. Actually, only that list may be modified. Let’s address this below. The nouiSlider React API provides internal access to the slider instance from within the component. To make it work, we have to leverage the React Hook feature:

  • Create a Hook hosting the component reference and a method to update it with React.useState.
  • Update the instance option each time the component is re-rendered by Shiny.
const [ref, setRef] = React.useState(null);

const setConfiguration = () => {
  if (ref && ref.noUiSlider) {
    ref.noUiSlider.updateOptions(configuration);
  }
};

setConfiguration();

We must specify the instanceRef property which will capture the slider reference when available and update the local ref:

<Nouislider
  style={sliderStyle}
  keyboardSupport={configuration.keyboard}
  connect={[true, false]}
  range={rangeOpts}
  start={[value]}
  onChange={value => setValue(parseFloat(value))}
  tooltips={configuration.tooltips}
  pips={pipOpts}
  instanceRef={
    instance => {
      if (instance && !ref) {
        setRef(instance);
      }
    }
  }
/>

We recompile the code and run the previous example. While the tooltip is gone, the keyboard interaction is still there. This is not surprising as updateOptions does not handle all settings. The final result is displayed Figure 28.6.

Slider input with Argon React with more options

FIGURE 28.6: Slider input with Argon React with more options