` instead of ``
+- Configuration via attributes flows to React props
+- Namespace support via element `id`
+
+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. **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?** → 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
@@ -253,6 +429,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}
+ )
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('')
+```