Skip to content

Data-bound chart components 2/7: typed figure handles - #462

Open
FarhanAliRaza wants to merge 1 commit into
stack/1-design-and-pinsfrom
stack/2-typed-handles
Open

Data-bound chart components 2/7: typed figure handles#462
FarhanAliRaza wants to merge 1 commit into
stack/1-design-and-pinsfrom
stack/2-typed-handles

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #461. Base is stack/1-design-and-pins — the Files-changed tab shows only this layer.

Problem

Chart state vars carried a bare token string, so the component's source was untyped. reflex_xy.chart(Dash.points) — the wrong var — compiled fine and failed at hydrate as a blank mount with an err frame.

Change

Wrap the token in a frozen dataclass (handles.py: FigureHandle, plus DataHandle[S] for the tier above) and give the component a Var[FigureHandle] figure prop. The wrong var or a raw string now fails at create() — page evaluation — with the framework's own TypeError, before a browser is involved.

@reflex_xy.figure vars, register(), and inline() all return handles. The empty-token handle keeps the existing "not ready / no chart" sentinel, so the var type stays non-optional.

Two compile-time refusals come with it:

  • kernel-backed event props (on_point_hover/on_point_click/on_select_end) on a static src source now raise instead of silently never firing;
  • the positional live spellings chart(var) / chart(token_string) warn while routing to figure=.

chart(Chart) — the positional static form — is deliberately NOT deprecated. It's the only route for arbitrary Charts such as facet grids, and it predates no replacement.

Public helpers that take "a figure" (append, set_view, reset_view, select, clear_selection, release) accept a handle or its bare token for one release cycle.

The wrapper reduces figure and legacy token to one subscription token; nothing below the subscribe path changes.

Spec

reflex-integration.md §3.1 (handle-valued vars), §5 (figure= prop, deprecation shim, event refusal), file map.

Test plan

  • uv run pytest tests/reflex_adapter tests/test_validation_timing.py — 170 passed
  • pre-commit run --all-files, ruff check, ruff format --check, ty check — clean

Review in cubic

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8334620f-6223-4243-8fba-5ecd7e98153c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces typed FigureHandle values for live Reflex charts and routes them through a typed figure= component prop, while retaining temporary compatibility for token strings and positional live sources.

  • Adds serialized FigureHandle and generic DataHandle dataclasses.
  • Updates figure vars, registration helpers, and mutation helpers to produce or accept typed handles.
  • Normalizes the frontend’s typed handle and legacy token forms into one subscription token.
  • Adds compile-time source and event validation and updates adapter tests, examples, and the integration specification.

Confidence Score: 4/5

The PR is not yet safe to merge because static charts can still fail page evaluation when a kernel-backed event prop is explicitly disabled with None.

The static-source validation constructs its offender list from keyword presence rather than configured handler values, so on_point_hover=None, on_point_click=None, or on_select_end=None still raises instead of behaving as an absent handler.

Files Needing Attention: python/reflex_xy/component.py

Important Files Changed

Filename Overview
python/reflex_xy/component.py Adds the typed live-source path, positional compatibility shim, and static-event validation; the previously reported disabled-handler rejection remains.
python/reflex_xy/handles.py Defines immutable, serializable typed handles and the compatibility token normalizer.
python/reflex_xy/vars.py Changes synchronous and asynchronous figure vars from bare strings to FigureHandle values while preserving the empty-token sentinel.
python/reflex_xy/assets/XYChart.jsx Reduces typed handles and legacy token props to one live subscription token throughout the component lifecycle.
python/reflex_xy/app.py Normalizes handle and string inputs before forwarding public figure operations to the registry.
python/reflex_xy/init.py Exports the handle types and changes registration helpers to return typed handles.
spec/design/reflex-integration.md Documents the handle-valued state contract, typed component prop, compatibility period, and static-event refusal.

Reviews (2): Last reviewed commit: "feat(reflex): typed figure handles and a..." | Re-trigger Greptile

# source would be silent no-ops at runtime — fail the compile
# with the reason instead.
if props.get("src") is not None:
offenders = [name for name in _KERNEL_EVENT_PROPS if name in props]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Disabled handlers block static charts

When a static chart receives a kernel-backed event prop explicitly set to None, membership in props alone marks it as active, causing page evaluation to raise ValueError even though no handler is configured.

Suggested change
offenders = [name for name in _KERNEL_EVENT_PROPS if name in props]
offenders = [
name for name in _KERNEL_EVENT_PROPS if props.get(name) is not None
]

Knowledge Base Used: reflex-xy: the Reflex integration package

@codspeed-hq

codspeed-hq Bot commented Aug 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 109 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing stack/2-typed-handles (d0886d6) with stack/1-design-and-pins (c0ebb88)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="python/reflex_xy/assets/XYChart.jsx">

<violation number="1" location="python/reflex_xy/assets/XYChart.jsx:285">
P2: When both live spellings are present, an empty `figure.token` currently falls back to the legacy `token`, so a "not ready" handle can incorrectly resubscribe to an old token. Using explicit figure precedence (nullish handling instead of truthiness fallback) preserves the empty-handle sentinel and matches the documented `figure`-wins behavior.</violation>
</file>

<file name="python/reflex_xy/handles.py">

<violation number="1" location="python/reflex_xy/handles.py:78">
P2: Figure-only helper paths can now accept a `DataHandle` and treat its token as a figure token, which hides caller mistakes and can route invalid tokens into registry operations. Restricting `token_of` to `FigureHandle` (plus legacy `str`) keeps the compatibility shim while preserving the intended type guard.</violation>
</file>

<file name="tests/reflex_adapter/test_component.py">

<violation number="1" location="tests/reflex_adapter/test_component.py:52">
P3: The assertion was loosened from the precise `'token:"tok-abc"'` to a bare `'"tok-abc"' in rendered`, which no longer proves the token reaches the `figure` prop (it would pass if the token landed on any prop). Since the entire PR is about routing the token through the typed `figure` prop, consider anchoring the check to the figure prop — e.g. assert `'figure' in rendered` alongside, or match `'{"token": "tok-abc"}'` — so a regression back to the legacy token path is caught.</violation>
</file>

<file name="spec/design/reflex-integration.md">

<violation number="1" location="spec/design/reflex-integration.md:484">
P3: This added line says the typed-`figure` mis-typed source "fails at compile", but §3.1 (and the PR's own description) specifies the same R1 failure as happening at `create()` — page evaluation, not build-time compile. In Reflex, `create()` runs when the component tree is evaluated at render, so the compile claim in this section is inaccurate and contradicts §3.1. Recommend rewording to "fail at `create()` (page evaluation)" to keep the two sections consistent.</violation>
</file>

<file name="python/reflex_xy/component.py">

<violation number="1" location="python/reflex_xy/component.py:128">
P2: Static-source handler refusal currently treats `None` event props as active handlers and can raise on harmless conditional forwarding. Checking for non-`None` values keeps the refusal for real kernel handlers without blocking disabled props.</violation>

<violation number="2" location="python/reflex_xy/component.py:381">
P3: The deprecation warning emitted when a bare `token_string` or legacy `str`-typed var is passed positionally tells the user to "use chart(figure=...)", but `figure=` is the typed `Var[FigureHandle]` prop that now rejects raw strings with a `TypeError` at `create()` (see the new `test_component_rejects_wrong_var_and_raw_string_at_compile`). So the exact users this warning targets — string-token sources — are pointed at an API that refuses their input. Recommend guidance that reflects what actually works for them: wrap the token in a `FigureHandle` (`chart(figure=FigureHandle("..."))`), or explicitly keep the one-release-cycle `token` path, rather than the generic "use figure=` suggestion.</violation>
</file>

<file name="python/reflex_xy/__init__.py">

<violation number="1" location="python/reflex_xy/__init__.py:224">
P2: Invalid `release()` arguments can now fail silently instead of surfacing a type error, which makes leaked registrations harder to debug when cleanup calls are wrong. Aligning `release()` with the other public helper normalization path (`_token`) would keep misuse fail-fast and consistent.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

// One subscription token from the two live spellings. `figure` is the
// typed handle ({token}); the bare `token` string is the deprecated wire.
// An empty handle token means "not ready" — no subscription yet.
const liveToken = (figure && figure.token) || token || null;

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When both live spellings are present, an empty figure.token currently falls back to the legacy token, so a "not ready" handle can incorrectly resubscribe to an old token. Using explicit figure precedence (nullish handling instead of truthiness fallback) preserves the empty-handle sentinel and matches the documented figure-wins behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/reflex_xy/assets/XYChart.jsx, line 285:

<comment>When both live spellings are present, an empty `figure.token` currently falls back to the legacy `token`, so a "not ready" handle can incorrectly resubscribe to an old token. Using explicit figure precedence (nullish handling instead of truthiness fallback) preserves the empty-handle sentinel and matches the documented `figure`-wins behavior.</comment>

<file context>
@@ -276,6 +279,10 @@ export function XYChart(props) {
+  // One subscription token from the two live spellings. `figure` is the
+  // typed handle ({token}); the bare `token` string is the deprecated wire.
+  // An empty handle token means "not ready" — no subscription yet.
+  const liveToken = (figure && figure.token) || token || null;
   const elRef = useRef(null); // inner chart mount (wiped on payload swaps)
   const outerRef = useRef(null); // stable wrapper: events, tooltip slot
</file context>
Suggested change
const liveToken = (figure && figure.token) || token || null;
const liveToken = figure != null ? (figure.token ?? null) : (token ?? null);
Fix with cubic

(``append``, ``set_view``, ``release``, ...) accept both a handle and the
old-style bare token string through this normalizer.
"""
if isinstance(source, (FigureHandle, DataHandle)):

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Figure-only helper paths can now accept a DataHandle and treat its token as a figure token, which hides caller mistakes and can route invalid tokens into registry operations. Restricting token_of to FigureHandle (plus legacy str) keeps the compatibility shim while preserving the intended type guard.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/reflex_xy/handles.py, line 78:

<comment>Figure-only helper paths can now accept a `DataHandle` and treat its token as a figure token, which hides caller mistakes and can route invalid tokens into registry operations. Restricting `token_of` to `FigureHandle` (plus legacy `str`) keeps the compatibility shim while preserving the intended type guard.</comment>

<file context>
@@ -0,0 +1,82 @@
+    (``append``, ``set_view``, ``release``, ...) accept both a handle and the
+    old-style bare token string through this normalizer.
+    """
+    if isinstance(source, (FigureHandle, DataHandle)):
+        return source.token
+    if isinstance(source, str):
</file context>
Fix with cubic

# source would be silent no-ops at runtime — fail the compile
# with the reason instead.
if props.get("src") is not None:
offenders = [name for name in _KERNEL_EVENT_PROPS if name in props]

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Static-source handler refusal currently treats None event props as active handlers and can raise on harmless conditional forwarding. Checking for non-None values keeps the refusal for real kernel handlers without blocking disabled props.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/reflex_xy/component.py, line 128:

<comment>Static-source handler refusal currently treats `None` event props as active handlers and can raise on harmless conditional forwarding. Checking for non-`None` values keeps the refusal for real kernel handlers without blocking disabled props.</comment>

<file context>
@@ -102,6 +118,26 @@ class XYChart(rx.Component):
+            # source would be silent no-ops at runtime — fail the compile
+            # with the reason instead.
+            if props.get("src") is not None:
+                offenders = [name for name in _KERNEL_EVENT_PROPS if name in props]
+                if offenders:
+                    msg = (
</file context>
Suggested change
offenders = [name for name in _KERNEL_EVENT_PROPS if name in props]
offenders = [name for name in _KERNEL_EVENT_PROPS if name in props and props[name] is not None]
Fix with cubic

globals()["registry"] = registry

registry.release(token)
registry.release(token_of(token) or "")

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Invalid release() arguments can now fail silently instead of surfacing a type error, which makes leaked registrations harder to debug when cleanup calls are wrong. Aligning release() with the other public helper normalization path (_token) would keep misuse fail-fast and consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/reflex_xy/__init__.py, line 224:

<comment>Invalid `release()` arguments can now fail silently instead of surfacing a type error, which makes leaked registrations harder to debug when cleanup calls are wrong. Aligning `release()` with the other public helper normalization path (`_token`) would keep misuse fail-fast and consistent.</comment>

<file context>
@@ -202,16 +211,17 @@ def index():
     globals()["registry"] = registry
 
-    registry.release(token)
+    registry.release(token_of(token) or "")
 
 
</file context>
Suggested change
registry.release(token_of(token) or "")
token_str = token_of(token)
if token_str is None:
msg = f"expected a FigureHandle or figure token string, got {type(token).__name__}"
raise TypeError(msg)
registry.release(token_str)
Fix with cubic

assert str(comp.library).startswith("$/public/external/reflex_xy/assets/XYChart")
rendered = str(comp)
assert 'token:"tok-abc"' in rendered
assert '"tok-abc"' in rendered # the handle's token reaches the figure prop

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The assertion was loosened from the precise 'token:"tok-abc"' to a bare '"tok-abc"' in rendered, which no longer proves the token reaches the figure prop (it would pass if the token landed on any prop). Since the entire PR is about routing the token through the typed figure prop, consider anchoring the check to the figure prop — e.g. assert 'figure' in rendered alongside, or match '{"token": "tok-abc"}' — so a regression back to the legacy token path is caught.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/reflex_adapter/test_component.py, line 52:

<comment>The assertion was loosened from the precise `'token:"tok-abc"'` to a bare `'"tok-abc"' in rendered`, which no longer proves the token reaches the `figure` prop (it would pass if the token landed on any prop). Since the entire PR is about routing the token through the typed `figure` prop, consider anchoring the check to the figure prop — e.g. assert `'figure' in rendered` alongside, or match `'{"token": "tok-abc"}'` — so a regression back to the legacy token path is caught.</comment>

<file context>
@@ -40,11 +40,16 @@ def app_cwd(tmp_path, monkeypatch):
     assert str(comp.library).startswith("$/public/external/reflex_xy/assets/XYChart")
     rendered = str(comp)
-    assert 'token:"tok-abc"' in rendered
+    assert '"tok-abc"' in rendered  # the handle's token reaches the figure prop
     assert "onPointHover" in rendered
     assert "picked" in rendered  # the reflex event dispatch is in the prop
</file context>
Fix with cubic

`register()`/`inline()`, landing in the typed `figure` prop
(`Var[FigureHandle]`) and riding the socket data plane. Because the prop is
`Var`-typed, `chart(figure=Dash.points)` and `chart(figure="raw string")`
fail at compile with the framework's `TypeError` (R1). A Chart/Figure

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This added line says the typed-figure mis-typed source "fails at compile", but §3.1 (and the PR's own description) specifies the same R1 failure as happening at create() — page evaluation, not build-time compile. In Reflex, create() runs when the component tree is evaluated at render, so the compile claim in this section is inaccurate and contradicts §3.1. Recommend rewording to "fail at create() (page evaluation)" to keep the two sections consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At spec/design/reflex-integration.md, line 484:

<comment>This added line says the typed-`figure` mis-typed source "fails at compile", but §3.1 (and the PR's own description) specifies the same R1 failure as happening at `create()` — page evaluation, not build-time compile. In Reflex, `create()` runs when the component tree is evaluated at render, so the compile claim in this section is inaccurate and contradicts §3.1. Recommend rewording to "fail at `create()` (page evaluation)" to keep the two sections consistent.</comment>

<file context>
@@ -469,12 +476,31 @@ reflex_xy.chart(
+`register()`/`inline()`, landing in the typed `figure` prop
+(`Var[FigureHandle]`) and riding the socket data plane. Because the prop is
+`Var`-typed, `chart(figure=Dash.points)` and `chart(figure="raw string")`
+fail at compile with the framework's `TypeError` (R1). A Chart/Figure
+passed positionally compiles to a payload asset and lands in the `src`
+prop, which the wrapper fetches and renders kernel-less — the static tier
</file context>
Fix with cubic

Comment on lines +381 to +384
_warn_positional(
"chart(figure=...) — register()/inline() return a FigureHandle, and "
"@reflex_xy.figure vars are FigureHandle-valued"
)

@cubic-dev-ai cubic-dev-ai Bot Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The deprecation warning emitted when a bare token_string or legacy str-typed var is passed positionally tells the user to "use chart(figure=...)", but figure= is the typed Var[FigureHandle] prop that now rejects raw strings with a TypeError at create() (see the new test_component_rejects_wrong_var_and_raw_string_at_compile). So the exact users this warning targets — string-token sources — are pointed at an API that refuses their input. Recommend guidance that reflects what actually works for them: wrap the token in a FigureHandle (chart(figure=FigureHandle("..."))), or explicitly keep the one-release-cycle token path, rather than the generic "use figure=` suggestion.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At python/reflex_xy/component.py, line 381:

<comment>The deprecation warning emitted when a bare `token_string` or legacy `str`-typed var is passed positionally tells the user to "use chart(figure=...)", but `figure=` is the typed `Var[FigureHandle]` prop that now rejects raw strings with a `TypeError` at `create()` (see the new `test_component_rejects_wrong_var_and_raw_string_at_compile`). So the exact users this warning targets — string-token sources — are pointed at an API that refuses their input. Recommend guidance that reflects what actually works for them: wrap the token in a `FigureHandle` (`chart(figure=FigureHandle("..."))`), or explicitly keep the one-release-cycle `token` path, rather than the generic "use figure=` suggestion.</comment>

<file context>
@@ -299,7 +357,31 @@ def chart(
+        if tailwind_manifest:
+            props["tailwind_class_tokens"] = _tailwind_scan_literal(tailwind_manifest)
+    elif isinstance(source, (str, rx.Var)):
+        _warn_positional(
+            "chart(figure=...) — register()/inline() return a FigureHandle, and "
+            "@reflex_xy.figure vars are FigureHandle-valued"
</file context>
Suggested change
_warn_positional(
"chart(figure=...) — register()/inline() return a FigureHandle, and "
"@reflex_xy.figure vars are FigureHandle-valued"
)
_warn_positional(
"chart(figure=FigureHandle(...)) — register()/inline() now return a "
"FigureHandle; pass it via figure= (a raw string is rejected by the "
"typed figure prop)"
)
Fix with cubic

Chart state vars carried a bare token string, so the component's source was
untyped: reflex_xy.chart(Dash.points) — the wrong var — compiled fine and
failed at hydrate as a blank mount with an err frame.

Wrap the token in a frozen dataclass (handles.py: FigureHandle, and
DataHandle[S] for the tier above this one) and give the component a
Var[FigureHandle] `figure` prop. The wrong var or a raw string now fails at
create(), which is page evaluation — the framework's own TypeError, before
a browser is involved. @reflex_xy.figure vars, register(), and inline() all
return handles; the empty-token handle keeps the existing "not ready / no
chart" sentinel, so the var type stays non-optional.

Two compile-time refusals come with it: kernel-backed event props
(on_point_hover/on_point_click/on_select_end) on a static src source now
raise instead of silently never firing, and the positional live spellings
chart(var) / chart(token_string) warn while routing to figure=. The static
positional chart(Chart) form is NOT deprecated — it remains the only route
for arbitrary Charts such as facet grids. Public helpers that take "a
figure" (append, set_view, reset_view, select, clear_selection, release)
accept a handle or its bare token for one release cycle.

The wrapper reduces `figure` and legacy `token` to one subscription token;
nothing below the subscribe path changes.

Spec: reflex-integration.md §3.1 (handle-valued vars), §5 (figure= prop,
deprecation shim, event refusal), file map.
@FarhanAliRaza
FarhanAliRaza force-pushed the stack/2-typed-handles branch from c42e175 to d0886d6 Compare August 5, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant