reactR icon indicating copy to clipboard operation
reactR copied to clipboard

Question: how to process nested tags with dependencies, would hydrate work?

Open DivadNojnarg opened this issue 7 months ago • 3 comments

@timelyportfolio LOL (2 years later almost related to your comment on shinyNextUI) I am facing the case where I have nested tags. Do you have an example of how to use hydrate from reactR. I am not very sure to what to pass to a component parameter (hydrate(component, tag)):

My use case is the following, using phosphoricons:

str(phosphoricons::ph_i("house"))
List of 3
 $ name    : chr "i"
 $ attribs :List of 1
  ..$ class: chr "ph-light ph-house ph-lg"
 $ children:List of 1
  ..$ :List of 10
  .. ..$ name      : chr "phosphor-icons"
  .. ..$ version   : chr "1.4.1"
  .. ..$ src       :List of 1
  .. .. ..$ file: chr "assets"
  .. ..$ meta      : NULL
  .. ..$ script    : NULL
  .. ..$ stylesheet: chr "css/icons.min.css"
  .. ..$ head      : NULL
  .. ..$ attachment: NULL
  .. ..$ package   : chr "phosphoricons"
  .. ..$ all_files : logi TRUE
  .. ..- attr(*, "class")= chr "html_dependency"
 - attr(*, "class")= chr "shiny.tag"
 - attr(*, "browsable_html")= logi TRUE

Contrary to shiny::icon, the later also adds the dependency to the icon in the children, which causes some issues. I can solve it by removing the dependency from the shiny tag and add it separately but if we could use hydrate that would be better.

I am not even sure that would be a solution, since the dependency would also have to be injected in the page head.

Thanks :)

DivadNojnarg avatar May 28 '25 12:05 DivadNojnarg

After thinking about it, the dependencies should be managed differently. That's what I get JS side:

{
    "name": "i",
    "attribs": {
        "class": "ph-light ph-house ph-lg"
    },
   // Thing that should not be here
    "children": [
        {
            "name": "phosphor-icons",
            "version": "1.4.1",
            "src": {
                "file": "assets"
            },
            "meta": null,
            "script": null,
            "stylesheet": "css/icons.min.css",
            "head": null,
            "attachment": null,
            "package": "phosphoricons",
            "all_files": true
        }
    ]
}

AFAIK, hydrate isn't aware on how to handle that from the source code and tries to create a react element, whereas It should not. Not sure how to more forward.

  • Is it ok to remove the dependency on the R side, and ask people to import it via a dependency function in their UI (not super nice).
  • Or if, we process this thing from JS, hydrate should be modified so it does not try to create a React element. From the object, it's hard to infer that the children is an htmldependency. But in any case, if the user does not manually provide the dependency, it get lost.
  • We could perhaps use Shiny.renderDependencies but this is expected to be used in conjunction with htmltools::renderTags on the R side. I did few testing and this does not seem to work: dependency path is incorrect and I don't manage to render the html.

DivadNojnarg avatar May 28 '25 14:05 DivadNojnarg

@DivadNojnarg Hi, sorry for delay while I was on vacation. I plan to play with this over the weekend and will report back. Thanks!!

timelyportfolio avatar Jun 12 '25 13:06 timelyportfolio

@DivadNojnarg Hi, finally got some time to play with this. If I am understanding correctly, I think in hydrate we could just ignore anything that resembles a htmlDependency tag. Let me know if you can think of any harmful side effects of ignoring. Assuming we process/include the dependencies on the R/user side, it seems ok. Here is an example,:

library(htmltools)
# remotes::install_github("react-R/reactR@develop")
library(reactR)
library(phosphoricons)

browsable(
  tagList(
    html_dependency_react(),
    html_dependency_reacttools(),
    # manual inclusion necessary if we do not include the `phosphoricons::ph_i("house")` tag
    # phosphoricons::html_dependency_phosphor(),
    tags$div(id = "app", "reactR rendered"),
    phosphoricons::ph_i("house"), # if we do not include then uncomment phosphoricons::html_dependency_phosphor()
    tags$script(type="module", HTML(sprintf(
"
  const tag = %s
  ReactDOM.render(reactR.hydrate({},tag), document.getElementById('app'))
",
      jsonlite::toJSON(
        tags$div(tags$span("reactR rendered"),phosphoricons::ph_i("house")),
        force = TRUE,
        auto_unbox = TRUE
      )
    )))
  )
)

Note, in this experimental isDependency function, I only test for properties "name", "version", "src", "script". We could attempt to make more robust, but I cannot think of any tag/component that would possess these properties.

timelyportfolio avatar Jun 23 '25 22:06 timelyportfolio

@timelyportfolio Your JS function does the job. A side effect would be someone who forget to manually include the dependency.

Would the second option of processing the dependency and rendering it seem realistic to you?

DivadNojnarg avatar Jul 09 '25 15:07 DivadNojnarg

@DivadNojnarg I have started some experiments, and I wanted to make sure that I understand correctly. In a shiny context, we would like any dependencies, such as phosphoricons, not picked up by the standard mechanisms to be handled and processed. Is this correct?

If true, then I think we will need logic on R side to add the resource path, and then on the JavaScript side to add the dependency (hopefully using already built Shiny functions) if not already added. I think the closest parallel would be with insertUI.

timelyportfolio avatar Jul 16 '25 13:07 timelyportfolio

Yes I think we want something like done in shiny::insertUI where the content is content = processDeps(ui, session). Then on the JS side they inject the deps in the head of the HTML document: maybe Shiny.renderDependencies.

DivadNojnarg avatar Jul 16 '25 13:07 DivadNojnarg

@DivadNojnarg ok here is a very, very ugly working experiment, which I will try to refine to remove all the cumbersome bits to a user.

library(shiny)
library(htmltools)
# remotes::install_github("react-R/reactR@develop")
library(reactR)
library(phosphoricons)

ui_with_uioutput <- uiOutput("app")
server_with_uioutput <- function(input, output, session) {
  output$app <- renderUI(phosphoricons::ph_i("house"))
}
shinyApp(ui = ui_with_uioutput, server = server_with_uioutput)


tl <- tagList(
  html_dependency_react(),
  html_dependency_reacttools(),
  # manual inclusion necessary if we do not include the `phosphoricons::ph_i("house")` tag
  # phosphoricons::html_dependency_phosphor(),
  tags$div(id = "app", "reactR rendered"),
  # phosphoricons::ph_i("house"), # if we do not include then uncomment phosphoricons::html_dependency_phosphor()
  tags$script(HTML(sprintf(
    "
(async () => {
  const tag = %s
  await Shiny.renderDependenciesAsync(%s);
  ReactDOM.render(reactR.hydrate({},tag), document.getElementById('app'));
})()
",
    jsonlite::toJSON(
      tags$div(tags$span("reactR rendered"),phosphoricons::ph_i("house")),
      force = TRUE,
      auto_unbox = TRUE
    ),
    # get dependency in proper format from R side, which we should be able to recreate in JavaScript
    jsonlite::toJSON(
      shiny:::processDeps(tags$div(tags$span("reactR rendered"),phosphoricons::ph_i("house")), session = list())$deps,
      force = TRUE,
      auto_unbox = TRUE,
      null = "null"
    )
  )))
)
ui <- tl
server <- function(input, output, session) {
  # add resource path so that the dependency is served
  shiny:::processDeps(tl, session)
}
shinyApp(ui = ui, server = server)

timelyportfolio avatar Jul 16 '25 21:07 timelyportfolio

Forgot about #47 which might be helpful even though motive is different. One other question: is this an input or output?

timelyportfolio avatar Jul 19 '25 00:07 timelyportfolio

@DivadNojnarg if input I fiddled with the #47 demo package to demonstrate how we might handle dependencies with an update input https://github.com/timelyportfolio/tester/commit/2773528fc94f425fc81cb8f3fec2fd9e3f8b8478.

timelyportfolio avatar Jul 19 '25 02:07 timelyportfolio

The code looks clean on the branch. In my case, this is for {scoutbaR}: https://github.com/cynkra/scoutbaR/commit/2dce5d9b0a967f3556b39a016d7b8c6682a3c920. scoutbaR is a "widget" even if I actually used the reactR input toolkit as I found it more convenient.

DivadNojnarg avatar Jul 19 '25 04:07 DivadNojnarg

@DivadNojnarg here is one approach to handle dependencies on creation https://github.com/timelyportfolio/scoutbaR/commit/1c563eb71f22378f0ce33ca6dd8887dac8657497. Let me know what you think. If I get some time, I'll try to use something similar to the tester example to deal with dependencies in the update piece.

As I worked through this, I am starting to think this might be hard to deal with universally/generically in reactR and potentially should live within the particular package, since actions for scoutbaR is more of an idiosyncratic prop. However, hydrate does now allow tags in props, so maybe reactR should try to handle.

timelyportfolio avatar Jul 19 '25 21:07 timelyportfolio

Sorry for taking so long to answer @timelyportfolio . This looks great :) Could you open a PR to scoutbaR? I can finish it if you don't have time (handle conflicts and so).

DivadNojnarg avatar Oct 20 '25 15:10 DivadNojnarg

I'll close this one. I believe this is resolved now. Thanks for the support!

DivadNojnarg avatar Oct 22 '25 21:10 DivadNojnarg