From 59ea70d29a07e727b5717a6ffd671db1d7058687 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 14 Jan 2026 09:43:50 -0500 Subject: [PATCH 1/3] chore: Describe new shiny-react component patterns --- shiny/shiny-react/SKILL.md | 188 +++++++++++++++++++++++-- shiny/shiny-react/assets/shinyreact.R | 7 +- shiny/shiny-react/assets/shinyreact.py | 11 +- 3 files changed, 194 insertions(+), 12 deletions(-) diff --git a/shiny/shiny-react/SKILL.md b/shiny/shiny-react/SKILL.md index 457e73b..a0f7c90 100644 --- a/shiny/shiny-react/SKILL.md +++ b/shiny/shiny-react/SKILL.md @@ -3,10 +3,12 @@ name: shiny-react description: > Build Shiny applications with React frontends using the @posit/shiny-react library. Use when: (1) Creating new Shiny apps with React UI, (2) Adding React components to - existing Shiny apps, (3) Using shadcn/ui or other React component libraries with Shiny, - (4) Understanding useShinyInput/useShinyOutput hooks, (5) Setting up bidirectional - communication between React and R/Python Shiny backends, (6) Building modern data - dashboards with React and Shiny. Supports both R and Python Shiny backends. + existing Shiny apps, (3) Creating reusable React widgets using custom web elements, + (4) Using shadcn/ui or other React component libraries with Shiny, (5) Understanding + useShinyInput/useShinyOutput hooks, (6) Setting up bidirectional communication between + React and R/Python Shiny backends, (7) Building modern data dashboards with React and + Shiny, (8) Implementing dynamic widget rendering with insertUI/removeUI. Supports both + R and Python Shiny backends. --- # shiny-react @@ -118,14 +120,181 @@ When writing React components that communicate with Shiny: if (isLoading) return ; ``` +7. **Use namespaces for multiple widget instances** - When embedding multiple instances of the same React widget, wrap them in `ShinyModuleProvider` to prevent ID conflicts: + ```typescript + import { ShinyModuleProvider } from "@posit/shiny-react"; + + + + + ``` + +8. **Create reusable widgets with custom web elements** - For self-contained React widgets that can be embedded in Shiny apps, use custom web elements. See the "Custom Web Element Pattern" section below for the recommended approach. + +## Shiny Module Namespaces + +When to use namespaces: + +- **Multiple widget instances** - Same React component used multiple times on one page +- **Shiny module integration** - React widgets inside Shiny modules (`moduleServer` in R, `@module.server` in Python) +- **Reusable components** - Creating widget libraries that work like standard Shiny UI components + +### Client-Side Pattern + +```typescript +import { ShinyModuleProvider } from "@posit/shiny-react"; + +// Wrap the widget in ShinyModuleProvider + + + + +// All hooks inside automatically namespace their IDs +function CounterWidget() { + const [count, setCount] = useShinyInput("count", 0); + // If namespace="counter1", this becomes "counter1-count" +} +``` + +### Server-Side Pattern + +Use standard Shiny module patterns. The `post_message()` function automatically namespaces messages: + +**R:** +```r +counter_ui <- function(id, title = "Counter") { + card( + card_header(title), + tags$tag("counter-widget", list(`data-namespace` = id)) + ) +} + +counter_server <- function(id) { + moduleServer(id, function(input, output, session) { + # input$count is automatically namespaced by Shiny + output$serverCount <- render_json({ input$count * 2 }) + + # post_message automatically applies session$ns() + post_message(session, "notification", list(text = "Updated!")) + + # Return reactive for use elsewhere + reactive({ input$count }) + }) +} +``` + +**Python:** +```python +def counter_ui(id: str, title: str = "Counter"): + return ui.card( + ui.card_header(title), + ui.HTML(f'') + ) + +@module.server +def counter_server(input, output, session): + @render_json + def serverCount(): + return input.count() * 2 + + # post_message automatically applies resolve_id() + await post_message(session, "notification", {"text": "Updated!"}) + + @reactive.calc + def count(): + return input.count() if input.count() is not None else 0 + + return count # Return reactive for use elsewhere +``` + +### Custom Web Element Pattern (Recommended) + +For self-contained React widgets embedded in standard Shiny apps, **use custom web elements** that handle their own lifecycle: + +```typescript +// main.tsx - define a custom web element +class CounterWidgetElement extends HTMLElement { + private root: Root | null = null; + + connectedCallback() { + // Read attributes using dataset and pass them as props to React component + const namespace = this.id; + const title = this.dataset.title || "Counter"; + const initialValue = parseInt(this.dataset.initialValue || "0"); + + this.root = createRoot(this); + this.root.render( + + + + + + ); + } + + disconnectedCallback() { + // Clean up React root when element is removed + if (this.root) { + this.root.unmount(); + this.root = null; + } + } +} + +// Register the custom element +customElements.define("counter-widget", CounterWidgetElement); +``` + +**Benefits of custom web elements:** +- **Pass configuration via HTML attributes**: Read attributes in `connectedCallback()` and pass them as props to your React component +- **Automatic initialization**: React initializes when element is added to DOM +- **Automatic cleanup**: React unmounts when element is removed from DOM +- **Dynamic rendering support**: Works seamlessly with `insertUI()`/`removeUI()` (R) or `ui.insert_ui()`/`ui.remove_ui()` (Python) +- **Semantic HTML**: `` is more readable than `
` +- **Self-contained**: All initialization logic lives in one place +- **No event listener dependencies**: No need to wait for `DOMContentLoaded` + +**Using the widget in Shiny:** + +R: +```r +counter_ui <- function(id, title = "Counter", initial_value = 0) { + card( + card_header(title), + tag("counter-widget", list( + id = id, + `data-title` = title, + `data-initial-value` = initial_value + )) + ) +} +``` + +Python: +```python +def counter_ui(id: str, title: str = "Counter", initial_value: int = 0): + return ui.card( + ui.card_header(title), + ui.HTML(f'') + ) +``` + +**Key Pattern:** Use `data-*` attributes to pass configuration from Shiny to React. Write the custom element to read these via `this.dataset` in `connectedCallback()` and pass them as props to your React component. + +This pattern allows React widgets to be used like native Shiny components, with clean APIs that follow Shiny conventions. The custom element automatically handles React lifecycle, making widgets work correctly even when dynamically added or removed. + +See `examples/8-modules/app-standard.R` and `app-standard.py` in the shiny-react repository for a complete working example with dynamic widget rendering. + ## Decision Tree 1. **New app from scratch?** → Use `npx create-shiny-react-app` -2. **Need TypeScript API details?** → Read `references/typescript-api.md` -3. **Setting up R backend?** → Read `references/r-backend.md` -4. **Setting up Python backend?** → Read `references/python-backend.md` -5. **Using shadcn/ui or Tailwind?** → Read `references/shadcn-setup.md` -6. **Understanding internals?** → Read `references/internals.md` +2. **Creating reusable React widgets for Shiny?** → Use custom web elements (see "Custom Web Element Pattern" above) +3. **Need multiple instances of same widget?** → Use `ShinyModuleProvider` with namespacing (see "Shiny Module Namespaces" above) +4. **Need TypeScript API details?** → Read `references/typescript-api.md` +5. **Setting up R backend?** → Read `references/r-backend.md` +6. **Setting up Python backend?** → Read `references/python-backend.md` +7. **Using shadcn/ui or Tailwind?** → Read `references/shadcn-setup.md` +8. **Understanding internals?** → Read `references/internals.md` ## Project Structure @@ -253,6 +422,7 @@ The [shiny-react repository](https://github.com/wch/shiny-react) includes exampl | `5-shadcn` | Modern UI with shadcn/ui and Tailwind CSS | | `6-dashboard` | Full analytics dashboard with charts and tables | | `7-chat` | AI chat app with streaming responses | +| `8-modules` | Shiny module namespaces with multiple widget instances (see two variants: full React app and standard Shiny app) | Each example includes complete R and Python backends. diff --git a/shiny/shiny-react/assets/shinyreact.R b/shiny/shiny-react/assets/shinyreact.R index 2c91f8e..ce9aa86 100644 --- a/shiny/shiny-react/assets/shinyreact.R +++ b/shiny/shiny-react/assets/shinyreact.R @@ -77,14 +77,19 @@ render_json <- function( #' React components using useShinyMessageHandler() hook. This wraps messages in a #' standard format and sends them via the "shinyReactMessage" channel. #' +#' When used within a Shiny module (moduleServer), the type is automatically +#' namespaced using session$ns(). Outside of modules, the type is passed through +#' unchanged. +#' #' @param session The Shiny session object #' @param type The message type (should match messageType in useShinyMessageHandler) #' @param data The data to send to the client post_message <- function(session, type, data) { + namespaced_type <- session$ns(type) session$sendCustomMessage( "shinyReactMessage", list( - type = type, + type = namespaced_type, data = data ) ) diff --git a/shiny/shiny-react/assets/shinyreact.py b/shiny/shiny-react/assets/shinyreact.py index ee52b96..82b7105 100644 --- a/shiny/shiny-react/assets/shinyreact.py +++ b/shiny/shiny-react/assets/shinyreact.py @@ -15,6 +15,7 @@ from shiny.html_dependencies import shiny_deps from shiny.types import Jsonifiable from shiny.render.renderer import Renderer, ValueFn +from shiny.module import resolve_id from typing import Any, Mapping, Optional, Sequence, Union @@ -33,7 +34,6 @@ def page_react( css_file: str | None = "main.css", lang: str = "en", ) -> ui.Tag: - head_items: list[ui.TagChild] = [] if js_file: @@ -100,6 +100,10 @@ async def post_message(session: Session, type: str, data: JsonifiableIn): React components using useShinyMessageHandler() hook. This wraps messages in a standard format and sends them via the "shinyReactMessage" channel. + When used within a Shiny module (@module.server), the type is automatically + namespaced using resolve_id(). Outside of modules, the type is passed through + unchanged. + Parameters ---------- session @@ -110,4 +114,7 @@ async def post_message(session: Session, type: str, data: JsonifiableIn): data The data to send to the client """ - await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) + namespaced_type = resolve_id(type) + await session.send_custom_message( + "shinyReactMessage", {"type": namespaced_type, "data": data} + ) From 064b9801cc94183ad8e54d2a66be1ba2d75b2263 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 14 Jan 2026 16:56:30 -0500 Subject: [PATCH 2/3] chore: Update with `ShinyReactComponentElement` class --- shiny/shiny-react/SKILL.md | 127 ++++++++-------- .../shiny-react/references/typescript-api.md | 135 ++++++++++++++++++ 2 files changed, 202 insertions(+), 60 deletions(-) diff --git a/shiny/shiny-react/SKILL.md b/shiny/shiny-react/SKILL.md index a0f7c90..a692de8 100644 --- a/shiny/shiny-react/SKILL.md +++ b/shiny/shiny-react/SKILL.md @@ -3,12 +3,12 @@ name: shiny-react description: > Build Shiny applications with React frontends using the @posit/shiny-react library. Use when: (1) Creating new Shiny apps with React UI, (2) Adding React components to - existing Shiny apps, (3) Creating reusable React widgets using custom web elements, - (4) Using shadcn/ui or other React component libraries with Shiny, (5) Understanding - useShinyInput/useShinyOutput hooks, (6) Setting up bidirectional communication between - React and R/Python Shiny backends, (7) Building modern data dashboards with React and - Shiny, (8) Implementing dynamic widget rendering with insertUI/removeUI. Supports both - R and Python Shiny backends. + existing Shiny apps, (3) Creating reusable React widgets using ShinyReactComponentElement + or custom web elements, (4) Using shadcn/ui or other React component libraries with Shiny, + (5) Understanding useShinyInput/useShinyOutput hooks, (6) Setting up bidirectional + communication between React and R/Python Shiny backends, (7) Building modern data + dashboards with React and Shiny, (8) Implementing dynamic widget rendering with + insertUI/removeUI. Supports both R and Python Shiny backends. --- # shiny-react @@ -129,7 +129,7 @@ When writing React components that communicate with Shiny: ``` -8. **Create reusable widgets with custom web elements** - For self-contained React widgets that can be embedded in Shiny apps, use custom web elements. See the "Custom Web Element Pattern" section below for the recommended approach. +8. **Create reusable widgets with ShinyReactComponentElement** - For self-contained React widgets that can be embedded in Shiny apps, extend `ShinyReactComponentElement` for automatic lifecycle management and Shiny integration. See the "ShinyReactComponentElement Base Class" section below for the recommended approach. ## Shiny Module Namespaces @@ -207,60 +207,63 @@ def counter_server(input, output, session): return count # Return reactive for use elsewhere ``` -### Custom Web Element Pattern (Recommended) +### ShinyReactComponentElement Base Class (Recommended) -For self-contained React widgets embedded in standard Shiny apps, **use custom web elements** that handle their own lifecycle: +For self-contained React widgets, extend `ShinyReactComponentElement` - a base class that handles React lifecycle, Shiny bindings, and namespace support automatically. +**Simple widget:** ```typescript -// main.tsx - define a custom web element -class CounterWidgetElement extends HTMLElement { - private root: Root | null = null; - - connectedCallback() { - // Read attributes using dataset and pass them as props to React component - const namespace = this.id; - const title = this.dataset.title || "Counter"; - const initialValue = parseInt(this.dataset.initialValue || "0"); - - this.root = createRoot(this); - this.root.render( - - - - - - ); - } +import { ShinyReactComponentElement } from "@posit/shiny-react"; +import { CounterWidget } from "./CounterWidget"; - disconnectedCallback() { - // Clean up React root when element is removed - if (this.root) { - this.root.unmount(); - this.root = null; - } - } +class CounterWidgetElement extends ShinyReactComponentElement { + static component = CounterWidget; } -// Register the custom element -customElements.define("counter-widget", CounterWidgetElement); +if (!customElements.get("counter-widget")) { + customElements.define("counter-widget", CounterWidgetElement); +} ``` -**Benefits of custom web elements:** -- **Pass configuration via HTML attributes**: Read attributes in `connectedCallback()` and pass them as props to your React component -- **Automatic initialization**: React initializes when element is added to DOM -- **Automatic cleanup**: React unmounts when element is removed from DOM -- **Dynamic rendering support**: Works seamlessly with `insertUI()`/`removeUI()` (R) or `ui.insert_ui()`/`ui.remove_ui()` (Python) -- **Semantic HTML**: `` is more readable than `
` -- **Self-contained**: All initialization logic lives in one place -- **No event listener dependencies**: No need to wait for `DOMContentLoaded` +The base class automatically: +- Creates React root and renders your component +- Wraps in `ShinyModuleProvider` if element has an `id` attribute +- Parses `data-*` attributes into props via `getConfig()` (JSON auto-parsing) +- Handles Shiny `bindAll`/`unbindAll` lifecycle +- Cleans up on disconnect + +**Blended component (React layout + Shiny content):** +```typescript +class SidebarLayoutElement extends ShinyReactComponentElement { + protected render() { + const config = this.getConfig(); + return ( + + ); + } +} +``` + +Use `data-slot="name"` in R/Python to create named slots. If no slots exist, all children are captured as `__children__`. + +**Key methods:** +- `getConfig()` - Returns parsed `data-*` attributes as object +- `onSlotMount` - Callback to pass to React for mounting Shiny content +- `mountSlot(name, el)` - Moves captured slot content and calls `Shiny.bindAll()` +- `captureSlots()` - Called automatically; captures `[data-slot]` children +- `clearContent()` - Override with no-op to preserve innerHTML +- `render()` - Override to customize React rendering **Using the widget in Shiny:** R: ```r counter_ui <- function(id, title = "Counter", initial_value = 0) { - card( - card_header(title), + tagList( + htmlDependency(...), tag("counter-widget", list( id = id, `data-title` = title, @@ -273,28 +276,32 @@ counter_ui <- function(id, title = "Counter", initial_value = 0) { Python: ```python def counter_ui(id: str, title: str = "Counter", initial_value: int = 0): - return ui.card( - ui.card_header(title), + return ui.TagList( + ui.include_js(...), ui.HTML(f'') ) ``` -**Key Pattern:** Use `data-*` attributes to pass configuration from Shiny to React. Write the custom element to read these via `this.dataset` in `connectedCallback()` and pass them as props to your React component. - -This pattern allows React widgets to be used like native Shiny components, with clean APIs that follow Shiny conventions. The custom element automatically handles React lifecycle, making widgets work correctly even when dynamically added or removed. +**Benefits:** +- Automatic initialization/cleanup with DOM lifecycle +- Works with `insertUI()`/`removeUI()` and `ui.insert_ui()`/`ui.remove_ui()` +- Semantic HTML: `` instead of `
` +- Configuration via attributes flows to React props +- Namespace support via element `id` -See `examples/8-modules/app-standard.R` and `app-standard.py` in the shiny-react repository for a complete working example with dynamic widget rendering. +See `examples/8-modules/` and `examples/9-blended/` in the shiny-react repository for complete examples. ## Decision Tree 1. **New app from scratch?** → Use `npx create-shiny-react-app` -2. **Creating reusable React widgets for Shiny?** → Use custom web elements (see "Custom Web Element Pattern" above) -3. **Need multiple instances of same widget?** → Use `ShinyModuleProvider` with namespacing (see "Shiny Module Namespaces" above) -4. **Need TypeScript API details?** → Read `references/typescript-api.md` -5. **Setting up R backend?** → Read `references/r-backend.md` -6. **Setting up Python backend?** → Read `references/python-backend.md` -7. **Using shadcn/ui or Tailwind?** → Read `references/shadcn-setup.md` -8. **Understanding internals?** → Read `references/internals.md` +2. **Creating reusable React widgets for Shiny?** → Extend `ShinyReactComponentElement` (see "ShinyReactComponentElement Base Class" above) +3. **Extending ShinyReactComponentElement?** → See examples in `examples/8-modules/` and `examples/9-blended/` +4. **Need multiple instances of same widget?** → Use `ShinyModuleProvider` with namespacing (see "Shiny Module Namespaces" above) +5. **Need TypeScript API details?** → Read `references/typescript-api.md` +6. **Setting up R backend?** → Read `references/r-backend.md` +7. **Setting up Python backend?** → Read `references/python-backend.md` +8. **Using shadcn/ui or Tailwind?** → Read `references/shadcn-setup.md` +9. **Understanding internals?** → Read `references/internals.md` ## Project Structure diff --git a/shiny/shiny-react/references/typescript-api.md b/shiny/shiny-react/references/typescript-api.md index 6d31739..adfcec7 100644 --- a/shiny/shiny-react/references/typescript-api.md +++ b/shiny/shiny-react/references/typescript-api.md @@ -9,6 +9,7 @@ Complete API reference for `@posit/shiny-react` hooks and components. - [useShinyMessageHandler](#useshinymessagehandler) - [useShinyInitialized](#useshinyinitialized) - [ImageOutput Component](#imageoutput-component) +- [ShinyReactComponentElement](#shinyreactcomponentelement) ## useShinyInput @@ -261,3 +262,137 @@ def myplot(): ax.scatter(data['x'], data['y']) return fig ``` + +## ShinyReactComponentElement + +Base class for creating custom web elements that render React components with automatic Shiny integration. + +```typescript +class ShinyReactComponentElement extends HTMLElement { + // Set on subclass to define the React component to render + static component: React.ComponentType> | null; + + // Protected properties + protected root: Root | null; + protected slotContents: Map; + + // Protected methods + protected getConfig(): Record; + protected captureSlots(selector?: string): Map; + protected mountSlot(slotName: string, container: HTMLElement | null): Promise; + protected get onSlotMount(): (slotName: string, el: HTMLElement | null) => Promise; + protected get namespace(): string | undefined; + protected render(): React.ReactNode; + protected clearContent(): void; + + // Lifecycle + connectedCallback(): void; + disconnectedCallback(): void; +} +``` + +### Features + +- **Automatic namespace support**: Wraps in `ShinyModuleProvider` if element has an `id` +- **Config parsing**: `getConfig()` parses `data-*` attributes with JSON auto-parsing +- **Slot preservation**: Captures `[data-slot]` children for blended React+Shiny content +- **Default slot**: If no `data-slot` elements, all children go to `__children__` slot +- **Shiny lifecycle**: Automatic `bindAll`/`unbindAll` management + +### Simple Widget Example + +```typescript +import { ShinyReactComponentElement } from "@posit/shiny-react"; +import { MyWidget } from "./MyWidget"; + +class MyWidgetElement extends ShinyReactComponentElement { + static component = MyWidget; +} + +if (!customElements.get("my-widget")) { + customElements.define("my-widget", MyWidgetElement); +} +``` + +The component receives parsed `data-*` attributes as props automatically. + +### Blended Component Example + +For React layouts containing Shiny content: + +```typescript +class MySidebarElement extends ShinyReactComponentElement { + protected render() { + const config = this.getConfig(); + return ( + + ); + } +} +``` + +In your React component, call `onSlotMount(slotName, containerEl)` after the container renders to move Shiny content into place. + +### Key Methods + +#### getConfig() + +Parses `data-*` attributes into a props object with JSON auto-parsing: + +```html + +``` + +Returns: `{ count: 5, items: [1,2,3], title: "Hello" }` + +- Numbers/booleans parsed from JSON +- Arrays/objects parsed from JSON +- Invalid JSON stays as string + +#### captureSlots(selector?) + +Called automatically in `connectedCallback()`. Captures children matching selector (default `[data-slot]`). + +If no matching elements found and element has children, all children are stored under the `__children__` slot. + +#### mountSlot(slotName, container) + +Moves captured slot content into the container element and calls `Shiny.bindAll()`. + +#### onSlotMount + +Getter that returns `mountSlot.bind(this)` - pass this to React components as a callback. + +#### clearContent() + +Clears `innerHTML` before React renders. Override with no-op to preserve existing content: + +```typescript +protected clearContent() {} // Keep existing content +``` + +### Override Points + +- **`render()`**: Customize what React renders (default renders `static component` with `getConfig()` props) +- **`getConfig()`**: Customize attribute parsing +- **`clearContent()`**: Override to preserve innerHTML +- **`captureSlots()`**: Override with custom selector + +### HTML Structure + +R: +```r +tag("my-widget", list( + id = "widget1", # Used for namespace + `data-title` = "My Title", # Becomes props.title + `data-count` = 5 # Becomes props.count (number) +)) +``` + +Python: +```python +ui.HTML('') +``` From 8da44cd947431adb13438099ad77d711e69c65c8 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 15 Jan 2026 13:23:12 -0500 Subject: [PATCH 3/3] docs: don't need `data-namespace` anymore --- shiny/shiny-react/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shiny/shiny-react/SKILL.md b/shiny/shiny-react/SKILL.md index a692de8..a9175f3 100644 --- a/shiny/shiny-react/SKILL.md +++ b/shiny/shiny-react/SKILL.md @@ -165,7 +165,7 @@ Use standard Shiny module patterns. The `post_message()` function automatically counter_ui <- function(id, title = "Counter") { card( card_header(title), - tags$tag("counter-widget", list(`data-namespace` = id)) + tags$tag("counter-widget", list(id = id)) ) } @@ -188,7 +188,7 @@ counter_server <- function(id) { def counter_ui(id: str, title: str = "Counter"): return ui.card( ui.card_header(title), - ui.HTML(f'') + ui.HTML(f'') ) @module.server