Coder Social home page Coder Social logo

appsilon / shiny.react Goto Github PK

View Code? Open in Web Editor NEW
92.0 25.0 11.0 2.87 MB

Use React in Shiny applications.

Home Page: https://appsilon.github.io/shiny.react

License: GNU Lesser General Public License v3.0

JavaScript 98.67% R 1.26% CSS 0.05% TypeScript 0.01%
r react shiny rhinoverse

shiny.react's Introduction

shiny.react shiny.react logo

Use React in Shiny applications.

CRAN status cranlogs total CI

This R package enables using React in Shiny apps and is used e.g. by the shiny.fluent package. It contains R and JS code which is independent from the React library (e.g. Fluent UI) that is being wrapped.

Installation

Stable version:

install.packages("shiny.react")

Development version:

remotes::install_github("Appsilon/shiny.react")

Development

To build and install the package, run:

(cd js && yarn && yarn webpack)
Rscript -e 'devtools::document(); devtools::install()'

Testing

  • cd js && yarn lint lints the JS code
  • cd js && yarn test runs the unit tests for the JS code
  • Rscript -e "lintr::lint_package()" runs linter for the R code
  • Rscript -e "devtools::test()" runs unit tests for the R code

How to contribute?

If you want to contribute to this project please submit a regular PR, once you're done with a new feature or bug fix.

Reporting a bug is also helpful - please use GitHub issues and describe your problem as detailed as possible.

Appsilon

Appsilon is a Posit (formerly RStudio) Full Service Certified Partner.
Learn more at appsilon.com.

Get in touch [email protected]

Explore the Rhinoverse - a family of R packages built around Rhino!

We are hiring!

shiny.react's People

Contributors

andyquinterom avatar averissimo avatar dokato avatar filipstachura avatar jakubnowicki avatar jakubsob avatar kamilzyla avatar marekrogala avatar pawelchabros avatar sankhadeepdutta avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shiny.react's Issues

Use Ubuntu for pkgdown workflow

  1. Currently the pkgdown workflow unnecessarily uses macOS which costs 10 times more than Ubuntu.
  2. It would be also good to set a lower time limit - it is currently 360 minutes, while a typical run takes less than 5. If the workflow hangs for some reason, we pay a lot.

[Bug]: React inputs are automatically bound by Shiny

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Project Version

No response

Platform and OS Version

No response

Existing Issues

No response

What happened?

When React components are created they're automatically bound by Shiny.

Steps to reproduce

  1. RemoveInputAdapter effect that sets Shiny input value
  2. The value on the server is still available

Expected behavior

When setting of input values isn't implemented for a React input the value is not available in Shiny

Attachments

No response

Screenshots or Videos

No response

Additional Information

No response

Implement debounce/throttle

Problem

Debounce is critical for some types of components and vanilla Shiny inputs use it by default. Without debounce, some React components are quite difficult to use - for example, playing with a slider can trigger a massive number of recalculations.

We need to add a debounce/throttle option with sensible defaults to InputAdapter and setInput.

Requirements

The implementation must be sufficient to resolve Appsilon/shiny.fluent#130. The behavior of debounced/throttled inputs should follow that of vanilla Shiny as closely as possible. For example, the input value should be "flushed" immediately when the component is unmounted from the DOM tree (e.g. when it is rendered via renderUI()). See comment for a test app which might be helpful.

`tabsetPanel` doesn't work in React context

Reproducible example

shinyApp(
  ui = bootstrapPage(
    shiny.react:::ReactContext(
      tabsetPanel(
        id = "tabset",
        selected = "tab2",
        tabPanel("tab1", "Tab 1"),
        tabPanel("tab2", "Tab 2")
      )
    )
  ),
  server = function(input, output) {
    observe(dput(input$tabset))
  }
)

Problems

  1. After initially loading the app, no tab is selected. Removing selected = "tab2" doesn't change this.
  2. It is possible to click on the tab to select it, but it won't be sent to the server (input$tabset is NULL).

Expected behavior

The app should behave as it would if we replaced shiny.react:::ReactContext() with div():

  1. The tab passed as selected argument should be actually selected when the application is initially loaded.
  2. The currently selected tab should be available on the server as input$tabset.

Notes

Replacing shiny.react:::ReactContext() with shiny.react:::ShinyBindingWrapper() seems to resolve problem (2).

Handle singletons and head tags in React context

In Shiny you can use e.g. singleton(tags$head(tags$script(...))) to add some code to document head. This is sometimes used by libraries (e.g. shinyAce, shiny.router) to add dependencies instead of htmlDependency(). However singletons and head tags currently don't work properly in React context.

Note: It is often possible to work around this issue by including the dependencies / mock component manually outside of React context. For example, you can put div(style = "display: none", shinyAce::aceEditor(NULL)) anywhere outside of React to load the dependencies and make aceEditor() work also within React.

Support accessor in `setInput()`

Background

React components typically use handlers to notify parent about value changes, e.g. (Checkbox from Blueprint.js):

<Checkbox
  defaultChecked={false}
  onChange={(event) => handle(event.target.checked)}
/>

With shiny.react it is possible to use such a component in the following way:

Checkbox(
  defaultChecked = FALSE,
  onChange = JS('(event) => Shiny.setInputValue("checked", event.target.checked)')
)

However, it is inconvenient and error-prone to define the onChange handler using JS code embedded in a string, so we have

shiny.react::setInput(inputId, argIdx = 1)

which can be seen as equivalent to

(...args) => Shiny.setInputValue(inputId, args[argIdx - 1]) // JS uses 0-based indexing

Problem

The setInput() function currently only supports sending one of the arguments to Shiny server. It is insufficient when we need to do something with the argument first, like getting the .target.checked property in the Checkbox example above. It would be great if we could do something like this:

Checkbox(
  defaultChecked = FALSE,
  onChange = setInput("checked", "[0].target.value")
)

Approach

Consider replacing the argIdx argument of setInput with accessor which would behave as in the example above. Suggest other possible API ideas for setInput (e.g. have both argIdx and accessor arguments) and consult with reviewer before implementation.

Relevant files

  1. R/react.R:174: definition of setInput()
  2. js/src/react/mapReactData.js:88: JS-side handler

Support nested reactOutput

I found that nested reactOutput() doesn't work as expected as compare to uiOutput().

Below is a minimal reproducible example:

library(shiny)
library(shiny.fluent)
shinyApp(
  ui = div(
    Checkbox.shinyInput("disabled", label = "Disable"),
    reactOutput("ui")
  ),
  server = function(input, output) {
    output$ui <- renderReact({
      tagList(
        TextField.shinyInput("text", label = "TextField", disabled = input$disabled),
        reactOutput("ui2")
      )
    })
    output$ui2 <- renderReact({
      Label(deparse(input$text), required = TRUE)
    })
  }
)

image

Expected output or workaround using uiOutput():

library(shiny)
library(shiny.fluent)
shinyApp(
  ui = div(
    Checkbox.shinyInput("disabled", label = "Disable"),
    reactOutput("ui")
  ),
  server = function(input, output) {
    output$ui <- renderReact({
      tagList(
        TextField.shinyInput("text", label = "TextField", disabled = input$disabled),
        uiOutput("ui2")
      )
    })
    output$ui2 <- renderUI({
      Label(deparse(input$text), required = TRUE)
    })
  }
)

image

Ensure shiny.react is recognized as an R repository

Our GitHub page shows shiny.react as a 98.5% JavaScript project (bottom of the right panel on the main page), which is not quite right. Due to this rdrr.io doesn't find shiny.react when searching for GitHub packages (hopefully we can be found by searching on CRAN).

We have this problem as our repository contains a lot of non-minified library code, e.g. inst/www/shiny.react/shiny-react.js. It could be addressed by using .gitattributes to mark certain files as generated: Customizing how changed files appear on GitHub.

[Feature]: Support for react native

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Description

It would be great to have access to react native components so that apps on mobile could have access to things like the geolocation or the camera.

Problem

Being able to create useful shiny progressive web apps.

Proposed Solution

Support for react native.

Alternatives Considered

using the shinysense package.

Update to 0.2.0 make_output, mark_as_react_tag going away...

Excited to take advantage of the new version! Looking at the news.md some critical functions i was using went away, in particular make_output() and mark_as_react_tag(). When i update to 0.2.0 this breaks my app do the missing functions. Sounds like can solve doing something like this "Components can now be defined by combining reactElement() and asProps().", but i'm having trouble doing so.

# old code

Provider <- shiny.react::make_output(NULL, "FabricRedux", "Provider")

make_component <- function(component_name) {
  function(...) {
    component <- shiny::tag(component_name, rlang::dots_list(...))
    component <- shiny.react:::mark_as_react_tag("FabricRedux", component)
    return(component)
  }
}
# new code something like this
Provider <- function(...) reactElement( # nolint
  module = "@/shiny.react", name = "Provider", props = asProps(...)
)

Any insights/tips to help combine reactElement and asProps to achieve would be appreciated!

Support updates for components created with `ButtonAdapter`

Currently, only components created with InputAdapter() can be updated with updateReactInput(). However, ability to update buttons would be quite useful, e.g. for changing the disabled prop. Also, it would make sense in terms of feature parity with base Shiny, which has updateActionButton().

Fix nested usage of `renderUI` with react components

Nesting uiOutput and renderUI with shiny.fluent components breaks in some scenarios and doesn’t re-render correctly. The issue was broken down to Stack being rendered using renderUI and containing uiOutput inside of it.

The recording describes the behaviour:

  • Change the reactive which will be shown in a card
  • Change reactive which forces the card to render
  • Change the reactive which is shown in the card - works properly
  • Change reactive which forces the card to render - react rendering mechanism works and shiny doesn’t. (NOTE: rendering using uiOutput + renderUI works if content of the card is static or doesn’t contain any shiny.fluent components inside renderUI e.g. plain text instead of Text

Video of the issue

Screen.Recording.2021-11-10.at.12.26.24.mov

Reproducible example

library(shiny)
library(shiny.fluent)

ui <- fluidPage(
  PrimaryButton.shinyInput("renderCard", text = "Render card"),
  PrimaryButton.shinyInput("resetCard", text = "Reset card content"),
  textOutput("resetCardCount"),
  textOutput("renderCardCount"),
  Toggle.shinyInput("toggle", label = "Toggle dynamic content in Stack", value = TRUE),
  fluidRow(
    column(
      width = 6,
      Text("reactOutput + renderReact"),
      reactOutput("containerReact")
    ),
    column(
      width = 6,
      Text("uiOutput + renderUI"),
      uiOutput("container")
    )
  )
)

server <- function(input, output, session) { 
  
  resetCard <- eventReactive(input$resetCard, {
    input$resetCard
  })
  
  renderCard <- eventReactive(input$renderCard, {
    input$renderCard
  })
  
  output$renderCardCount <- renderText({
    glue::glue("Render card button clicks: {renderCard()}")
  })
  
  output$resetCardCount <- renderText({
    glue::glue("Reset card content button clicks: {resetCard()}")
  })
  
  output$containerReact <- renderReact({
    renderCard()
    Stack(
      style = list("background-color" = "grey"),
      Text(variant = "large", "Card"),
      if (isolate(input$toggle)) uiOutput("uiReact") else (Text("Static content"))
    )
  })
  
  output$uiReact <- renderUI({
    div(
      Text("Card content"),
      Text(variant = "large", resetCard())
    )
  })

  output$container <- renderUI({
    renderCard()
    Stack(
      style = list("background-color" = "grey"),
      Text(variant = "large", "Card"),
      if (isolate(input$toggle)) uiOutput("ui") else (Text("Static content"))
    )
  })
  
  output$ui <- renderUI({
    div(
      Text("Card content"),
      Text(variant = "large", resetCard())
    )
  })
}

shinyApp(ui, server)

Use `attributsToProps` from html-react-parser

To render Shiny in React context, we are essentially translating htmltools tags to React elements. The bulk of the work is done by prepareProps() JS function, but it has limitations (see comments in the code). It would be beneficial to use attributeToProps() from html-react-parser library, as it might handle more cases than our home-brewed solution. In particular, we could get rid of "unsupported kebab-case CSS property" warnings.

Handle `shiny.tag.function` dependencies

Goal

The behavior of this app should be equivalent whether the selectInput() is wrapped in ReactContext() or not:

shinyApp(
  ui = shiny.react:::ReactContext(
    selectInput("select", "Select", c("Apples", "Bananas"))
  ),
  server = function(input, output) {}
)

Right now it gives the following error:

Error in FUN(X[[i]], ...) : 
  Expected a recursive structure built of NULLs, lists and dependencies

Root cause

Let's take a look at deparse(selectInput("select", "Select", c("Apples", "Bananas"))):

structure(
  list(
    # Omitted for brevity.
  ),
  class = "shiny.tag",
  html_dependencies = list(
    structure(
      function() {
        if (is_shiny_app()) {
          shiny::registerThemeDependency(func)
          return(mfunc(get_current_theme()))
        }
        mfunc(bs_global_get())
      },
      class = "shiny.tag.function" # The problem
    )
  )
)

The problem boils down to this: the flattenDeps() function doesn't handle arguments of class shiny.tag.function.

Support `bindEvent()` for `renderReact()`

Background

Since Shiny 1.6 you can use bindEvent() on render functions, e.g.:

shinyApp(
  ui = tagList(
    actionButton("render", "Render"),
    textOutput("message")
  ),
  server = function(input, output) {
    output$message <- renderText("Hello!") |> bindEvent(input$render)
  }
)

Goal

We'd like to support using bindEvent() on renderReact() as well, e.g. this should work:

library(shiny.react)

shinyApp(
  ui = tagList(
    actionButton("render", "Render"),
    reactOutput("message")
  ),
  server = function(input, output) {
    output$message <- renderReact("Hello!") |> bindEvent(input$render)
  }
)

Apply namespace automatically in the `updateReactInput`

When using updateReactInput() in a Shiny module, you need to wrap the id in session$ns call, but it is not necessary when using e.g. shiny::updateTextInput(). For consistency, updateReactInput() should add a namespace automatically.

[Bug]: A named list of inputs causes card to not show.

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Project Version

No response

Platform and OS Version

No response

Existing Issues

No response

What happened?

A card will disappear without warning or error if the div's are named: e.g.

$`Bargaining Unit(s)`
<div class="react-container">
  <script class="react-data" type="application/json">{"type":"element","module":"@/shiny.fluent","name":"Dropdown","props":{"type":"raw","value":{"inputId":"barg","label":"Bargaining Unit(s)","options":[{"key":"CUPE","text":"CUPE"},{"key":"PSA","text":"PSA"}],"multiSelect":true,"value":"CUPE"}}}</script>
  <script>jsmodule['@/shiny.react'].findAndRenderReactData()</script>
</div>

This is unfortunate as we can't do something like

c("barg", "dept", "crew", "tr_code") |>
    set_names(
      c("Bargaining Unit", "Department", "Crew", "Transaction Code")
    ) |>
    imap(
      \(x, idx) Dropdown.shinyInput(...)
    )

Steps to reproduce

  1. May add later a reprex...

...

Expected behavior

Expected card to remain.

Attachments

No response

Screenshots or Videos

No response

Additional Information

No response

React context can affect layout

Problem

To ensure that Shiny inputs/outputs work when rendered by React, we use ShinyBindingWrapper. Unfortunately it adds an additional wrapper div into the rendered HTML, which makes it harder to read and can affect layout/styling.

The following snippet will render "Hi" and "there!" on separate lines. If we replace shiny.react:::ReactContext with tagList, both words will appear on one line.

shinyApp(
  ui = shiny.react:::ReactContext(
    actionLink("a", "Hi"),
    actionLink("b", "there!")
  ),
  server = function(input, output) {}
)

A similar issue can be observed for textOutput():

shinyApp(
  ui = shiny.react:::ReactContext(
    textOutput("a", inline = TRUE),
    textOutput("b", inline = TRUE)
  ),
  server = function(input, output) {
    output$a <- renderText("Hi")
    output$b <- renderText("there!")
  }
)

Solution

It is possible to avoid the wrapper div; a preliminary solution is available on with-shiny-bindings branch.

Add a `startDownload` helper

Problem

Currently, there is no straightforward way to trigger downloads from React. In effect, e.g. a PrimaryButton from shiny.fluent cannot be easily used as a download button (but see Appsilon/shiny.fluent#39 for a workaround).

Solution

It should be possible to create a startDownload() helper, which could be used the following way:

shinyApp(
  ui = PrimaryButton(onClick = startDownload("file"), text = "Download"),
  server = function(input, output) {
    output$file <- downloadHandler(
      filename = function() "iris.csv",
      content = function(file) write.csv(iris, file)
    )
  }
)

[Bug]: Can't reproduce tutorial - webpack issue

Guidelines

  • I agree to follow this project's Contributing Guidelines.

Project Version

0.2.3

Platform and OS Version

No response

Existing Issues

No response

What happened?

While following tutorial "Porting Liquid Oxygen to Shiny", after yarn webpack command I get the following error:

ERROR in ./node_modules/@emdgroup-liquid/liquid/dist/react.js 4:0-61
Module not found: Error: Can't resolve './react-component-lib' in '.../js/node_modules/@emdgroup-liquid/liquid/dist'
Did you mean 'index.js'?
BREAKING CHANGE: The request './react-component-lib' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.

Steps to reproduce

  1. Follow "Porting Liquid Oxygen to Shiny" tutorial.
  2. Error should appear after yarn webpack command.

Expected behavior

Webpack compiles without error.

Attachments

No response

Screenshots or Videos

No response

Additional Information

No response

Add support for `HTML()` within React

In React context, htmltools::HTML() is treated as plain text. It should be however possible to make React render the HTML. Two options come to mind:

  1. Insert a div and use its dangerouslySetInnerHTML. This solution has a drawback of creating an additional wrapper div - might be undesirable, if the user wants strict control over the HTML structure.
  2. Parse the HTML with html-react-parser to get a React element and render it.

Note: A workaround for now is to add a uiOutput() in place of HTML() and render the desired HTML dynamically.

Support R Markdown

Background

It is possible to use shiny.react in R Markdown with runtime: shiny, e.g.:

---
title: "shiny-rmarkdown"
output: html_document
runtime: shiny
---

```{r, include=FALSE}
library(shiny.fluent)
```

```{r, echo=FALSE}
TextField.shinyInput("text")
renderUI(Text(input$text, variant = "large"))
```

Goal

It would be nice if this worked as well:

---
title: "shiny-rmarkdown"
output: html_document
---

```{r, include=FALSE}
library(shiny.fluent)
```

```{r, echo=FALSE}
PrimaryButton("Nice!")
```

Thoughts

2022-07-26

For full support two things probably need to happen:

  1. The reactOutput() should use createRenderFunction() (or markRenderFunctino(), or the underlying mechanics).
  2. The JS code must not assume Shiny is available (see the markdown-hack branch for an idea how this can be achieved).

2023-07-03

  1. It seems that shiny.react components already almost work in Rmd. The core mechanism (plain htmltools::div() with a trigger script tag + htmltools::htmlDependency()) appears to work fine - just like plain shiny::tags do.
  2. The problem is that the shiny.react JS code loaded by the htmlDependency() assumes that Shiny JS is available. Now, I think conceptually it wouldn't be very hard to work without this assumption, but on the other hand it’s already baked into the code quite deeply.
  3. It is not obvious if we want or need to use htmlwidgets here. The mechanism it provides might solve problems which we haven’t foreseen, but it might also not fit our needs very well (seems strongly focused on Shiny outputs) and/or require a redesign of some core mechanics in shiny.react.

Support progress indicators

With Shiny it is possible to report progress of a long running computation (using either the functional or object-oriented API). Many React libraries provide components which could be used instead (e.g. Spinner and ProgressIndicator from Fluent UI), however it is difficult to use them as Shiny is super vigilant about delaying any rendering until all computations are finished. It is possible to hack around this problem to some extent, e.g.:

library(shiny)
library(shiny.fluent)

shinyApp(
  ui = Stack(tokens = list(childrenGap = 10), style = list(width = 200),
    PrimaryButton.shinyInput("start", text = "Start Computation"),
    reactOutput("spinner")
  ),
  server = function(input, output) {
    computing <- reactiveVal(FALSE)
    trigger <- debounce(computing, 5) # Enough delay for Shiny to render the Spinner first
    observeEvent(input$start, computing(TRUE))
    observeEvent(trigger(), {
      if (trigger()) {
        Sys.sleep(3) # Long computation
        computing(FALSE)
      }
    })
    output$spinner <- renderReact({
      if (computing()) Spinner(size = 3)
    })
  }
)

Still, this is far from being a drop-in replacement for the progress reporting functionality in base Shiny.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.