posit-dev / posit-dev/shinyreact

Enhanced renderers: expose round-trip inputs and update methods on the renderer

Open
#31 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
14
Forks
3
Avg merge
9h 12m
Merged PRs (30d)
74

Description

Summary

Today a @reactive_output (or any future @render_plotly, @render_table_react, etc.) is a one-way pipe: Python returns a value, the React component renders it. But many useful components also send signals back — selected rows, brushed regions, zoom extents, expanded nodes — and accept commands — "reset zoom", "select row 4", "update plot limits".

Today users would wire those by hand: pick useShinyInput names in JSX, declare matching inputs.<name> in Python, send custom_messages via session.send_custom_message for the command direction. Each downstream package re-invents the contract.

We should let the renderer be that contract. Inputs the component emits become fields on the renderer; commands the component accepts become methods on the renderer. The renderer already owns the output ID and has the session in scope, so it has everything it needs.

Precedent

@render.data_frame in Shiny for Python already does this:

@render.data_frame
def grid():
    return render.DataGrid(df, selection_mode=\"rows\")

@reactive.effect
def _():
    rows = grid.cell_selection()        # round-trip input, exposed as method on the renderer
    grid.update_cell_value(...)         # command, exposed as method on the renderer

We want the same ergonomics for reactive_output and any enhanced renderers downstream packages add (render_plotly, render_echart, render_ag_grid, …).

Proposed shape

@render_plotly
def my_plot():
    return px.scatter(df, x=\"x\", y=\"y\")

@reactive.effect
def _():
    pts: SelectedPoints | None = my_plot.selected_points()   # round-trip input
    extent: PlotExtent | None  = my_plot.zoom_extent()       # round-trip input

@reactive.effect
@reactive.event(input.reset)
def _():
    my_plot.reset_zoom()                                     # command → custom_message
    my_plot.set_limits(x=(0, 10), y=(0, 5))                  # command → custom_message

Mechanics:

  • Round-trip inputs are reactive values registered under derived names (e.g. f\"{id}__selected_points\"). They're exposed as callables on the renderer so the call site looks like a normal Shiny input read but doesn't need a separate inputs.<name> declaration.
  • Commands are methods on the renderer that wrap session.send_custom_message (or the shinyreact send_message equivalent) and target the renderer's own ID. The user never names the message type.
  • Discovery — round-trip inputs and commands are declared on the renderer subclass once (by the package author). Users don't re-declare them per app.

Why this is the right home

  • The renderer already captures id + session at decoration time.
  • Naming collisions are bounded by the output ID — no global input-name design needed.
  • Downstream packages (shinyplotly, shinyshadcn, etc.) get a clear extension point: subclass the renderer, declare round-trip inputs and commands once.
  • Pairs naturally with #30 (typed Inputs/Outputs codegen): the renderer's declared round-trips become typed attributes automatically.

Sketch of the base class API

class EnhancedRenderer(Renderer[T]):
    # Declarative — package authors fill these in
    round_trip_inputs: dict[str, type] = {}     # name → type
    commands: dict[str, type] = {}              # name → payload type

    def __getattr__(self, name):
        # round_trip_inputs[name] → reactive read
        # commands[name]          → bound method that calls send_message
        ...

Or, more explicitly, code-generate the subclass from a declaration so attribute access is statically typed (ties into #30).

Open questions

  • Naming: are the JS-side input names a fixed convention ({id}__selected_points) or configurable per renderer? Fixed is simpler and lets the React component look them up by the parent's ID; configurable adds flexibility nobody may need.
  • Lifecycle: how do round-trip inputs behave when the component unmounts / the output is hidden? Likely "last value sticks until next render" — same as Shiny inputs today.
  • Error surface for commands: command sent before the component mounts — silently queue, or warn? Shiny's existing update_* family silently no-ops; matching that is probably right.
  • Async commands (where the JS side acks): out of scope for v1; renderer methods are fire-and-forget like today's update_*.
  • Typing story: exposing selected_points: Callable[[], SelectedPoints | None] requires either codegen (#30) or a Generic[Inputs, Commands] base class with __class_getitem__ magic. Codegen is the more honest answer.
  • Discoverability: do we want a registry so devtools can list "every output and its round-trip inputs / commands" for debugging?
  • Does this subsume send_message? Once commands live on the renderer, the bare send_message(session, type, data) API is for cases without a corresponding output — worth keeping but de-emphasized.

Out of scope

  • R package equivalent.
  • Two-way binding semantics (where the component owns canonical state and Python is the secondary). Renderers stay output-driven; round-trip inputs are observations of component state, not authoritative state.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No implementation files or tests are named. Start by locating Renderer, reactive_output, and the existing render.data_frame implementation, then resolve the open API questions for declared round-trip inputs and commands. Done means an agreed, documented renderer extension point with tests covering reactive reads and command delivery.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, react, typescript
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.