From e94c1102db01639a8317215e925adc919bb29da2 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 6 Jul 2026 21:20:25 +0530 Subject: [PATCH 01/21] Wrap Recharts Sankey chart --- docs/library/graphing/charts/sankeychart.md | 146 ++++++++++++++++++ .../reflex_components_recharts/__init__.py | 2 + .../src/reflex_components_recharts/charts.py | 80 +++++++++- .../src/reflex_components_recharts/general.py | 1 + pyi_hashes.json | 4 +- .../components/graphing/test_recharts.py | 8 + 6 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 docs/library/graphing/charts/sankeychart.md diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md new file mode 100644 index 00000000000..bea8e864dd6 --- /dev/null +++ b/docs/library/graphing/charts/sankeychart.md @@ -0,0 +1,146 @@ +--- +components: + - rx.recharts.SankeyChart +title: Sankey Chart +meta_description: "Create Sankey charts in Python with Reflex. Build interactive Recharts Sankey diagrams to visualize weighted flows between stages, categories, or systems." +--- + +# Sankey Chart + +```python exec +import random + +import reflex as rx +``` + +Sankey charts in Reflex are built on [Recharts](https://recharts.org/), a React charting library, and created in pure Python. A Sankey chart visualizes weighted flows between nodes, making it useful for showing movement through stages, resource allocation, user journeys, and other source-to-target relationships. + +## Simple Example + +An `rx.recharts.sankey_chart()` takes a `data` dictionary with `nodes` and `links`. Links refer to nodes by zero-based index. + +```python demo graphing +sankey_data = { + "nodes": [ + {"name": "Website"}, + {"name": "Landing Page"}, + {"name": "Product Page"}, + {"name": "Checkout"}, + {"name": "Purchase"}, + ], + "links": [ + {"source": 0, "target": 1, "value": 1200}, + {"source": 1, "target": 2, "value": 900}, + {"source": 2, "target": 3, "value": 420}, + {"source": 3, "target": 4, "value": 260}, + ], +} + + +def sankey_simple(): + return rx.recharts.sankey_chart( + rx.recharts.graphing_tooltip(), + data=sankey_data, + node_padding=24, + node_width=12, + link_curvature=0.55, + width="100%", + height=320, + ) +``` + +## Stateful Example + +Chart data can be tied to a State var. This example randomizes the flow values when the button is clicked. + +```python demo exec +class SankeyState(rx.State): + data = { + "nodes": [ + {"name": "Marketing"}, + {"name": "Trial"}, + {"name": "Sales"}, + {"name": "Support"}, + {"name": "Retained"}, + ], + "links": [ + {"source": 0, "target": 1, "value": 600}, + {"source": 1, "target": 2, "value": 320}, + {"source": 2, "target": 4, "value": 210}, + {"source": 1, "target": 3, "value": 180}, + {"source": 3, "target": 4, "value": 130}, + ], + } + + @rx.event + def randomize_flows(self): + for link in self.data["links"]: + link["value"] = random.randint(80, 700) + + +def sankey_stateful(): + return rx.vstack( + rx.recharts.sankey_chart( + rx.recharts.graphing_tooltip(), + data=SankeyState.data, + node_padding=18, + node_width=14, + width="100%", + height=320, + ), + rx.button("Randomize flows", on_click=SankeyState.randomize_flows), + width="100%", + ) +``` + +## Custom Node Types And Styles + +Use fields on each node to describe node types and per-node styling. Pass `node` or `link` dictionaries to control shared Sankey styling. + +```python demo graphing +styled_sankey_data = { + "nodes": [ + {"name": "Sources", "type": "source", "fill": rx.color("blue", 8)}, + {"name": "Direct", "type": "channel", "fill": rx.color("green", 8)}, + {"name": "Search", "type": "channel", "fill": rx.color("grass", 8)}, + {"name": "Paid", "type": "channel", "fill": rx.color("amber", 8)}, + {"name": "Revenue", "type": "outcome", "fill": rx.color("purple", 8)}, + ], + "links": [ + {"source": 0, "target": 1, "value": 350}, + {"source": 0, "target": 2, "value": 500}, + {"source": 0, "target": 3, "value": 220}, + {"source": 1, "target": 4, "value": 190}, + {"source": 2, "target": 4, "value": 260}, + {"source": 3, "target": 4, "value": 150}, + ], +} + + +def sankey_custom_styles(): + return rx.recharts.sankey_chart( + rx.recharts.graphing_tooltip(), + data=styled_sankey_data, + node={ + "stroke": rx.color("gray", 12), + "strokeWidth": 1, + }, + link={ + "stroke": rx.color("gray", 8), + "strokeOpacity": 0.35, + }, + node_padding=22, + node_width=16, + link_curvature=0.45, + width="100%", + height=340, + ) +``` + +## Related Charts + +Explore more chart types you can build with Reflex and Recharts in pure Python: + +- [Treemap](/docs/library/graphing/charts/treemap) +- [Funnel Chart](/docs/library/graphing/charts/funnelchart) +- [Pie Chart](/docs/library/graphing/charts/piechart) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index bb327bbb54d..cb5bdff211b 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -56,6 +56,8 @@ "ScatterChart", "funnel_chart", "FunnelChart", + "sankey_chart", + "SankeyChart", "treemap", "Treemap", ], diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 6bfedd0a451..f0879ebff77 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence -from typing import Any, ClassVar +from typing import Any, ClassVar, TypedDict from reflex_base.components.component import Component, field from reflex_base.constants import EventTriggers @@ -516,6 +516,83 @@ class FunnelChart(ChartBase): ] +class SankeyNode(TypedDict, total=False): + """A node in a Sankey chart.""" + + name: str + type: str + fill: str | Color + stroke: str | Color + strokeWidth: int | float + strokeOpacity: int | float + + +class SankeyLink(TypedDict): + """A weighted link between two Sankey chart nodes.""" + + source: int + target: int + value: int | float + + +class SankeyData(TypedDict): + """The source data for a Sankey chart.""" + + nodes: Sequence[SankeyNode] + links: Sequence[SankeyLink] + + +class SankeyChart(ChartBase): + """A Sankey chart component in Recharts.""" + + tag = "Sankey" + + alias = "RechartsSankeyChart" + + name_key: Var[str] = field(doc='The key of each node name. Default: "name"') + + data_key: Var[str | int] = field(doc='The key of each link value. Default: "value"') + + data: Var[SankeyData] = field( + doc="The source data, including nodes and the weighted links between them." + ) + + margin: Var[dict[str, Any]] = field( + doc='The sizes of whitespace around the chart. Default: {"top": 5, "right": 5, "bottom": 5, "left": 5}' + ) + + node: Var[Any] = field( + doc="The configuration object or custom renderer used to draw nodes." + ) + + link: Var[Any] = field( + doc="The configuration object or custom renderer used to draw links." + ) + + sort: Var[bool] = field( + doc="Whether to sort nodes on the y-axis or display them in data order." + ) + + node_padding: Var[int] = field(doc="The padding between nodes.") + + node_width: Var[int] = field(doc="The width of each node.") + + link_width: Var[int] = field(doc="The width of each link.") + + link_curvature: Var[float] = field(doc="The curvature of each link.") + + iterations: Var[int] = field( + doc="The number of layout iterations used to position nodes and links." + ) + + # Valid children components + _valid_children: ClassVar[list[str]] = [ + "Legend", + "GraphingTooltip", + "Defs", + ] + + class Treemap(RechartsCharts): """A Treemap chart component in Recharts.""" @@ -598,4 +675,5 @@ def create(cls, *children, **props) -> Component: radial_bar_chart = RadialBarChart.create scatter_chart = ScatterChart.create funnel_chart = FunnelChart.create +sankey_chart = SankeyChart.create treemap = Treemap.create diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index d3dcaa97926..14611aff8aa 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -66,6 +66,7 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): "RadialBarChart", "ResponsiveContainer", "ScatterChart", + "SankeyChart", "Treemap", "ComposedChart", "FunnelChart", diff --git a/pyi_hashes.json b/pyi_hashes.json index f5b97d72d2b..7a6097c07ad 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -111,9 +111,9 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "39e4144cef066bbff8c27b36a205a54b", "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "86fc106181638c6a0a2a199332be817f", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", - "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "359e123d9a046557ce05a96ce10313f5", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "280a7cd51298ee676f3d076104133f44", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "2a415854f1507ffcfb166884c318d0c4", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 3e4268eb891..710e2c23712 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -5,6 +5,7 @@ PieChart, RadarChart, RadialBarChart, + SankeyChart, ScatterChart, ) from reflex_components_recharts.general import ResponsiveContainer @@ -50,3 +51,10 @@ def test_scatter_chart(): sc = ScatterChart.create() assert isinstance(sc, ResponsiveContainer) assert isinstance(sc.children[0], ScatterChart) + + +def test_sankey_chart(): + sc = SankeyChart.create() + assert isinstance(sc, ResponsiveContainer) + assert isinstance(sc.children[0], SankeyChart) + assert sc.children[0].render()["name"] == "RechartsSankeyChart" From 574f8510d78fe041f9775d7bb0e7924316e8c580 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Mon, 6 Jul 2026 22:05:21 +0530 Subject: [PATCH 02/21] Tighten Sankey data typing --- .../src/reflex_components_recharts/charts.py | 18 ++++++++++++------ pyi_hashes.json | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index f0879ebff77..1a9bbc404ea 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -10,6 +10,7 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import Var +from typing_extensions import NotRequired from reflex_components_recharts.general import ResponsiveContainer @@ -516,15 +517,15 @@ class FunnelChart(ChartBase): ] -class SankeyNode(TypedDict, total=False): +class SankeyNode(TypedDict): """A node in a Sankey chart.""" name: str - type: str - fill: str | Color - stroke: str | Color - strokeWidth: int | float - strokeOpacity: int | float + type: NotRequired[str] + fill: NotRequired[str | Color] + stroke: NotRequired[str | Color] + strokeWidth: NotRequired[int | float] + strokeOpacity: NotRequired[int | float] class SankeyLink(TypedDict): @@ -533,6 +534,11 @@ class SankeyLink(TypedDict): source: int target: int value: int | float + fill: NotRequired[str | Color] + fillOpacity: NotRequired[int | float] + stroke: NotRequired[str | Color] + strokeWidth: NotRequired[int | float] + strokeOpacity: NotRequired[int | float] class SankeyData(TypedDict): diff --git a/pyi_hashes.json b/pyi_hashes.json index 7a6097c07ad..446ed12fb5d 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "2a415854f1507ffcfb166884c318d0c4", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "362e3055c2ebff0881860f87a459940e", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", From 00fa111c952d0d359b135a053f0cbcffd4a5a78c Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Fri, 10 Jul 2026 08:44:42 +0530 Subject: [PATCH 03/21] Tighten Sankey chart wrapper props --- .../src/reflex_components_recharts/charts.py | 6 ++---- pyi_hashes.json | 2 +- tests/units/components/graphing/test_recharts.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 1a9bbc404ea..b2014617e44 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -2,7 +2,7 @@ from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Mapping, Sequence from typing import Any, ClassVar, TypedDict from reflex_base.components.component import Component, field @@ -559,7 +559,7 @@ class SankeyChart(ChartBase): data_key: Var[str | int] = field(doc='The key of each link value. Default: "value"') - data: Var[SankeyData] = field( + data: Var[SankeyData | Mapping[str, Any]] = field( doc="The source data, including nodes and the weighted links between them." ) @@ -583,8 +583,6 @@ class SankeyChart(ChartBase): node_width: Var[int] = field(doc="The width of each node.") - link_width: Var[int] = field(doc="The width of each link.") - link_curvature: Var[float] = field(doc="The curvature of each link.") iterations: Var[int] = field( diff --git a/pyi_hashes.json b/pyi_hashes.json index 446ed12fb5d..a1600cae2ae 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "362e3055c2ebff0881860f87a459940e", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "f7f9008d855ec70a3a7bc1485a4b0d7a", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 710e2c23712..1e3252ee1a5 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -10,6 +10,8 @@ ) from reflex_components_recharts.general import ResponsiveContainer +import reflex as rx + def test_area_chart(): ac = AreaChart.create() @@ -58,3 +60,15 @@ def test_sankey_chart(): assert isinstance(sc, ResponsiveContainer) assert isinstance(sc.children[0], SankeyChart) assert sc.children[0].render()["name"] == "RechartsSankeyChart" + assert "link_width" not in SankeyChart.get_props() + + +def test_sankey_chart_accepts_unannotated_state_data(): + class SankeyState(rx.State): + data = { + "nodes": [{"name": "A"}, {"name": "B"}], + "links": [{"source": 0, "target": 1, "value": 1}], + } + + sc = SankeyChart.create(data=SankeyState.data) + assert isinstance(sc, ResponsiveContainer) From cc9bd95de0c2d1b9e04c6a08bd1835871e8fdbd6 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Fri, 10 Jul 2026 09:06:29 +0530 Subject: [PATCH 04/21] Add Sankey chart changelog fragment --- packages/reflex-components-recharts/news/6708.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-components-recharts/news/6708.feature.md diff --git a/packages/reflex-components-recharts/news/6708.feature.md b/packages/reflex-components-recharts/news/6708.feature.md new file mode 100644 index 00000000000..8fd63838ddc --- /dev/null +++ b/packages/reflex-components-recharts/news/6708.feature.md @@ -0,0 +1 @@ +Added a Recharts Sankey chart wrapper. From 900ab4da1795a4f7e549ccf0c5ea5364abf87217 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 29 Jul 2026 17:42:12 -0700 Subject: [PATCH 05/21] Enable proper custom sankey node and link renderers * Implement use_hook_var, use_chart_width, and use_id * Add memo-based decorator wrappers for `sankey_chart.node` and `sankey_chart.link` that do the needful to convert the decorated function into a custom component capable of being passed to sankey_chart as props. Updated the docs to show a proper custom link+node example; removed broken partial customization example (merged working pieces with the stateful example). --- docs/library/graphing/charts/sankeychart.md | 100 ++++++++++-- .../src/reflex_base/vars/__init__.py | 3 + .../src/reflex_base/vars/special.py | 38 +++++ .../reflex_components_recharts/__init__.py | 1 + .../src/reflex_components_recharts/charts.py | 142 +++++++++++++++++- .../src/reflex_components_recharts/general.py | 14 ++ pyi_hashes.json | 6 +- 7 files changed, 283 insertions(+), 21 deletions(-) create mode 100644 packages/reflex-base/src/reflex_base/vars/special.py diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index bea8e864dd6..71565cee3c9 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -83,6 +83,15 @@ def sankey_stateful(): rx.recharts.sankey_chart( rx.recharts.graphing_tooltip(), data=SankeyState.data, + node={ + "fill": rx.color("accent", 7), + "stroke": rx.color("accent", 10), + "strokeWidth": 2, + }, + link={ + "stroke": rx.color("gray", 7), + "strokeOpacity": 0.35, + }, node_padding=18, node_width=14, width="100%", @@ -93,9 +102,18 @@ def sankey_stateful(): ) ``` -## Custom Node Types And Styles -Use fields on each node to describe node types and per-node styling. Pass `node` or `link` dictionaries to control shared Sankey styling. +## Full Node / Link Customization + +For complete control over node and link rendering, pass a +`@rx.recharts.sankey_chart.node` or `@rx.recharts.sankey_chart.link` decorated +function that returns an svg-based component. The function receives a +`Var[SankeyNodeProps]` or `Var[SankeyLinkProps]` object with the node or link +data `payload`, as well as the object's position and dimensions. You can use these +properties to construct a custom node or link. + +Because the component renders inside the SVG element of the chart, you can only +use `rx.el.svg` components to construct the custom node or link. ```python demo graphing styled_sankey_data = { @@ -117,21 +135,73 @@ styled_sankey_data = { } -def sankey_custom_styles(): +def sankey_custom_render(): + @rx.recharts.sankey_chart.node + def custom_node( + node: rx.Var[rx.recharts.sankey_chart.SankeyNodeProps], + ) -> rx.Component: + # Determine if the node is at the right edge of the chart to adjust the label position accordingly. + is_out = node.x + node.width + 6 > rx.recharts.use_chart_width() + return rx.fragment( + rx.el.svg.text( + node.payload.name, + x=rx.cond(is_out, node.x - 6, node.x + node.width + 6), + y=node.y + node.height / 2, + text_anchor=rx.cond(is_out, "end", "start"), + stroke=rx.color("gray", 12), + font_size=10, + ), + rx.el.svg.rect( + x=node.x, + y=node.y, + width=node.width, + height=node.height, + # Accessing custom keys in the payload needs a `dict` cast. + fill=node.payload.to(dict)["fill"], + stroke=rx.color("gray", 12), + stroke_width=1, + ), + ) + + @rx.recharts.sankey_chart.link + def custom_link( + link: rx.Var[rx.recharts.sankey_chart.SankeyLinkProps], + ) -> rx.Component: + link_id = rx.vars.use_id() + source = link.payload.source.to(dict) + target = link.payload.target.to(dict) + return rx.fragment( + rx.el.svg.linear_gradient( + rx.el.svg.stop(offset="0%", stop_color=source["fill"]), + rx.el.svg.stop(offset="100%", stop_color=target["fill"]), + id=link_id, + ), + rx.el.svg.text( + link.payload.value, + x=(link.sourceX + link.targetX) / 2, + y=(link.sourceY + link.targetY) / 2, + text_anchor="middle", + stroke=rx.color("gray", 12), + font_size=10, + ), + rx.el.svg.path( + d=( + f"M{link.sourceX},{link.sourceY} " + f"C{link.sourceControlX},{link.sourceY} " + f"{link.targetControlX},{link.targetY} " + f"{link.targetX},{link.targetY}" + ), + fill="none", + stroke=f"url(#{link_id})", + stroke_opacity=0.35, + stroke_width=link.linkWidth, + ), + ) + return rx.recharts.sankey_chart( - rx.recharts.graphing_tooltip(), data=styled_sankey_data, - node={ - "stroke": rx.color("gray", 12), - "strokeWidth": 1, - }, - link={ - "stroke": rx.color("gray", 8), - "strokeOpacity": 0.35, - }, - node_padding=22, - node_width=16, - link_curvature=0.45, + node=custom_node, + link=custom_link, width="100%", height=340, ) diff --git a/packages/reflex-base/src/reflex_base/vars/__init__.py b/packages/reflex-base/src/reflex_base/vars/__init__.py index c986cf1fd36..2c78f8964df 100644 --- a/packages/reflex-base/src/reflex_base/vars/__init__.py +++ b/packages/reflex-base/src/reflex_base/vars/__init__.py @@ -28,6 +28,7 @@ LiteralStringVar, StringVar, ) +from .special import use_hook_var, use_id __all__ = [ "EMPTY_VAR_INT", @@ -66,6 +67,8 @@ "number", "object", "sequence", + "use_hook_var", + "use_id", "var_operation", "var_operation_return", ] diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py new file mode 100644 index 00000000000..3d05ef63670 --- /dev/null +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -0,0 +1,38 @@ +"""Special Vars for rendering values from the environment.""" + +from typing import Any + +from reflex_base.utils.types import GenericType +from reflex_base.vars.base import Var, VarData, get_unique_variable_name + + +def use_hook_var(library: str, hook: str, _var_type: GenericType = Any) -> Var: + """Get a Var representing a React hook's value. + + The value will depend on the context of the component in which it is used. + + Args: + library: The library to import the hook from. + hook: The name of the hook. + _var_type: The type of the Var. + + Returns: + A Var representing the React hook. + """ + return Var( + var_name := get_unique_variable_name(), + _var_type=_var_type, + _var_data=VarData( + imports={library: hook}, + hooks=(f"const {var_name} = {hook}();",), + ), + ).guess_type() + + +def use_id() -> Var[str]: + """Get the stable React useId hook value for a component. + + Returns: + A Var representing the useId hook value. + """ + return use_hook_var(library="react", hook="useId", _var_type=str) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index cb5bdff211b..a2ff29dc436 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -75,6 +75,7 @@ "LabelList", "cell", "Cell", + "use_chart_width", ], "polar": [ "pie", diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index b2014617e44..afc0d7ddbdb 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -2,14 +2,18 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence -from typing import Any, ClassVar, TypedDict +import inspect +from collections.abc import Callable, Mapping, Sequence +from types import SimpleNamespace +from typing import Any, ClassVar, TypedDict, get_args, get_origin from reflex_base.components.component import Component, field +from reflex_base.components.memo import _MemoComponentWrapper, memo from reflex_base.constants import EventTriggers from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import Var +from reflex_base.vars.object import RestProp from typing_extensions import NotRequired from reflex_components_recharts.general import ResponsiveContainer @@ -548,6 +552,121 @@ class SankeyData(TypedDict): links: Sequence[SankeyLink] +class SankeyNodePayload(TypedDict): + """The payload for a Sankey chart node.""" + + name: str + sourceNodes: list[int] + sourceLinks: list[int] + targetLinks: list[int] + targetNodes: list[int] + value: int | float + depth: int + x: int | float + dx: int | float + y: int | float + dy: int | float + + +class SankeyNodeProps(TypedDict): + """The props for a custom Sankey chart node.""" + + height: int + width: int + payload: SankeyNodePayload + index: int + x: int + y: int + + +class SankeyLinkPayload(TypedDict): + """The payload for a Sankey chart link.""" + + source: int + target: int + value: int | float + index: int + width: int | float + sy: int | float + ty: int | float + + +class SankeyLinkProps(TypedDict): + """The props for a custom Sankey chart link.""" + + sourceX: int + targetX: int + sourceY: int + targetY: int + sourceControlX: int + targetControlX: int + sourceRelativeY: int + targetRelativeY: int + linkWidth: int + index: int + payload: SankeyLinkPayload + + +def sankey_node( + fn: Callable, +) -> _MemoComponentWrapper: + """A decorator to create a custom Sankey chart node. + + Args: + fn: A function that takes a SankeyNodeProps and returns a Reflex component. + + Returns: + A function that takes a SankeyNodeProps and returns a Reflex component. + """ + sig = inspect.signature(fn) + if ( + len(sig.parameters) != 1 + or (first_param := sig.parameters[next(iter(sig.parameters))]) is None + or get_origin(first_param.annotation) is not Var + or not (args := get_args(first_param.annotation)) + or args[0] is not SankeyNodeProps + ): + msg = f"@sankey_node decorated function must take a single argument of type SankeyNodeProps, got {sig.parameters}" + raise TypeError(msg) + + def _wrapper(rest: RestProp) -> Component: + return fn(**{first_param.name: rest.to(SankeyNodeProps)}) + + _wrapper.__name__ = fn.__name__ + _wrapper.__module__ = fn.__module__ + return memo(wrapper=None)(_wrapper) + + +def sankey_link( + fn: Callable, +) -> _MemoComponentWrapper: + """A decorator to create a custom Sankey chart link. + + Args: + fn: A function that takes a SankeyLinkProps and returns a Reflex component. + + Returns: + A function that takes a SankeyLinkProps and returns a Reflex component. + """ + sig = inspect.signature(fn) + if ( + len(sig.parameters) != 1 + or (first_param := sig.parameters[next(iter(sig.parameters))]) is None + or get_origin(first_param.annotation) is not Var + or not (args := get_args(first_param.annotation)) + or args[0] is not SankeyLinkProps + ): + msg = f"@sankey_link decorated function must take a single argument of type SankeyLinkProps, got {sig.parameters}" + raise TypeError(msg) + + def _wrapper(rest: RestProp) -> Component: + return fn(**{first_param.name: rest.to(SankeyLinkProps)}) + + _wrapper.__name__ = fn.__name__ + _wrapper.__module__ = fn.__module__ + return memo(wrapper=None)(_wrapper) + + class SankeyChart(ChartBase): """A Sankey chart component in Recharts.""" @@ -597,6 +716,23 @@ class SankeyChart(ChartBase): ] +class SankeyNamespace(SimpleNamespace): + """A namespace for the Sankey chart components.""" + + node = staticmethod(sankey_node) + link = staticmethod(sankey_link) + __call__ = staticmethod(SankeyChart.create) + + # For type checking + SankeyNode = SankeyNode + SankeyLink = SankeyLink + SankeyData = SankeyData + SankeyNodePayload = SankeyNodePayload + SankeyNodeProps = SankeyNodeProps + SankeyLinkPayload = SankeyLinkPayload + SankeyLinkProps = SankeyLinkProps + + class Treemap(RechartsCharts): """A Treemap chart component in Recharts.""" @@ -679,5 +815,5 @@ def create(cls, *children, **props) -> Component: radial_bar_chart = RadialBarChart.create scatter_chart = ScatterChart.create funnel_chart = FunnelChart.create -sankey_chart = SankeyChart.create +sankey_chart = SankeyNamespace() treemap = Treemap.create diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index 14611aff8aa..f76089ab17c 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -9,6 +9,7 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import LiteralVar, Var +from reflex_base.vars.special import use_hook_var from .recharts import ( LiteralAnimationEasing, @@ -300,6 +301,19 @@ class Cell(Recharts): ) +def use_chart_width() -> Var[int | None]: + """Get the chart width as a var. + + Outside of a chart context, this will be None/undefined. + + Returns: + The chart width var. + """ + return use_hook_var( + library=Recharts.library or "", hook="useChartWidth", _var_type=int | None + ) + + responsive_container = ResponsiveContainer.create legend = Legend.create graphing_tooltip = tooltip = GraphingTooltip.create diff --git a/pyi_hashes.json b/pyi_hashes.json index 3582a788e34..31c686be09c 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -111,10 +111,10 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "39e4144cef066bbff8c27b36a205a54b", "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "86fc106181638c6a0a2a199332be817f", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", - "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "c2ab419b100855925a6511b53b668f0f", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "de544a2efb54d0d90639933ccfa65b77", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "f7f9008d855ec70a3a7bc1485a4b0d7a", - "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "f5e3491c4e1f69ba89085682888888a9", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "8e7f47e0eb774fdc3b1ce62b40ea4e56", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "be94d0ac11cca0bd0cfb2519b759897b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "58521fcd1b514804f534d97624e82c9a", From 6c9066d70fc3c73f730cbbe0e7c1f0921c2a1597 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 29 Jul 2026 17:48:01 -0700 Subject: [PATCH 06/21] reflex-components-recharts depends on reflex-base `use_hook_var` and `use_id` --- packages/reflex-components-recharts/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 7f82ea8baf7..02b85bd3058 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" -dependencies = ["reflex-base >= 0.9.0"] +dependencies = ["reflex-base >= 0.9.7.post31.dev0"] [tool.hatch.version] source = "uv-dynamic-versioning" From cb445be262a2dc826edac4483ea0e400bf88e25a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 01:01:56 +0000 Subject: [PATCH 07/21] Add changelog, tests, and docs for reflex_base.vars.special - Add reflex-base news entry for the new use_hook_var/use_id APIs (#6708) - Extend recharts news entry to mention custom node/link renderers and use_chart_width - Add unit tests for reflex_base.vars.special and rx.recharts.use_chart_width - Document hook vars in the var_system API reference and wrapping-react custom-code-and-hooks pages; point to the helpers from the sankey docs Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QQXD9PDAbVVDieBfgSHiPX --- docs/api-reference/var_system.md | 34 ++++++++++ docs/library/graphing/charts/sankeychart.md | 4 ++ docs/wrapping-react/custom-code-and-hooks.md | 17 +++++ packages/reflex-base/news/6708.feature.md | 1 + .../news/6708.feature.md | 2 +- .../components/graphing/test_recharts.py | 14 ++++- tests/units/reflex_base/vars/test_special.py | 63 +++++++++++++++++++ 7 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 packages/reflex-base/news/6708.feature.md create mode 100644 tests/units/reflex_base/vars/test_special.py diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md index 469ee8dca83..224509f5c94 100644 --- a/docs/api-reference/var_system.md +++ b/docs/api-reference/var_system.md @@ -78,3 +78,37 @@ def factorial(value: NumberVar): Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1. Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`. + +## Hook Vars + +Some values only exist on the frontend and are exposed through React hooks. +`rx.vars.use_hook_var()` binds the return value of a no-argument hook to a unique variable name and returns it as a `Var`. +The hook call and the import of the hook are automatically included in any component that uses the var, so the value reflects the context of the component it is rendered in. + +```py +chart_width = rx.vars.use_hook_var( + library="recharts@3.8.1", + hook="useChartWidth", + _var_type=int | None, +) +``` + +A component using `chart_width` will import `useChartWidth` from `recharts` and render `const = useChartWidth();` in its body, so `chart_width` can be used like any other `Var[int | None]`. + +For the common case of React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` containing a stable unique id for the rendered component. +This is useful for linking SVG elements to `defs` such as gradients or filters: + +```py +def gradient_rect() -> rx.Component: + gradient_id = rx.vars.use_id() + return rx.el.svg( + rx.el.svg.linear_gradient( + rx.el.svg.stop(offset="0%", stop_color="gold"), + rx.el.svg.stop(offset="100%", stop_color="tomato"), + id=gradient_id, + ), + rx.el.svg.rect(fill=f"url(#{gradient_id})", width=64, height=64), + width=64, + height=64, + ) +``` diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 71565cee3c9..4af28eb6703 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -115,6 +115,10 @@ properties to construct a custom node or link. Because the component renders inside the SVG element of the chart, you can only use `rx.el.svg` components to construct the custom node or link. +The example below also uses `rx.recharts.use_chart_width()` to read the +rendered chart width and `rx.vars.use_id()` to generate a unique id that links +each link's gradient definition to the path that references it. + ```python demo graphing styled_sankey_data = { "nodes": [ diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md index c35f908d678..10c0cfc71c4 100644 --- a/docs/wrapping-react/custom-code-and-hooks.md +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -114,3 +114,20 @@ export function Div_7178f430b7b371af8a12d8265d65ab9b() { ```md alert info # You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. ``` + +## Using a Hook's Return Value + +`add_hooks` inserts hook statements into the component, but the values they define are not directly accessible from Python. When you need the return value of a no-argument hook, use `rx.vars.use_hook_var()`, which binds the hook call to a unique variable name and returns it as a `Var`. The hook statement and its import are automatically included in any component where the var is used, so it composes with regular props and var operations. + +```python +import reflex as rx + + +def use_chart_width() -> rx.Var[int | None]: + """Get the width of the enclosing recharts chart as a var.""" + return rx.vars.use_hook_var( + library="recharts@3.8.1", hook="useChartWidth", _var_type=int | None + ) +``` + +For React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` with a stable unique id for the component being rendered, e.g. for linking SVG elements to gradient or filter definitions. diff --git a/packages/reflex-base/news/6708.feature.md b/packages/reflex-base/news/6708.feature.md new file mode 100644 index 00000000000..21e05d1f2f9 --- /dev/null +++ b/packages/reflex-base/news/6708.feature.md @@ -0,0 +1 @@ +Add `rx.vars.use_hook_var()` to create a `Var` bound to the value of a no-argument React hook imported from a given library, and `rx.vars.use_id()` to get React's stable `useId` value for the rendered component. diff --git a/packages/reflex-components-recharts/news/6708.feature.md b/packages/reflex-components-recharts/news/6708.feature.md index 8fd63838ddc..9627636e7b7 100644 --- a/packages/reflex-components-recharts/news/6708.feature.md +++ b/packages/reflex-components-recharts/news/6708.feature.md @@ -1 +1 @@ -Added a Recharts Sankey chart wrapper. +Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers, and `rx.recharts.use_chart_width()` for reading the rendered chart width as a `Var`. diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 1e3252ee1a5..9a626c7dc12 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -8,7 +8,8 @@ SankeyChart, ScatterChart, ) -from reflex_components_recharts.general import ResponsiveContainer +from reflex_components_recharts.general import ResponsiveContainer, use_chart_width +from reflex_components_recharts.recharts import Recharts import reflex as rx @@ -72,3 +73,14 @@ class SankeyState(rx.State): sc = SankeyChart.create(data=SankeyState.data) assert isinstance(sc, ResponsiveContainer) + + +def test_use_chart_width(): + width = use_chart_width() + assert width._var_type == (int | None) + var_data = width._get_all_var_data() + assert var_data is not None + assert var_data.hooks == (f"const {width!s} = useChartWidth();",) + assert dict(var_data.imports)[Recharts.library or ""] == ( + rx.ImportVar(tag="useChartWidth"), + ) diff --git a/tests/units/reflex_base/vars/test_special.py b/tests/units/reflex_base/vars/test_special.py new file mode 100644 index 00000000000..c86322507c0 --- /dev/null +++ b/tests/units/reflex_base/vars/test_special.py @@ -0,0 +1,63 @@ +"""Tests for reflex_base.vars.special hook-backed vars.""" + +from typing import Any + +from reflex_base.components.component import Component +from reflex_base.utils.imports import ImportVar +from reflex_base.vars.number import NumberVar +from reflex_base.vars.sequence import StringVar +from reflex_base.vars.special import use_hook_var, use_id + + +class HookComponent(Component): + """A minimal component for asserting hook var hoisting.""" + + library = "test-lib" + + tag = "HookComponent" + + +def test_use_hook_var_var_data(): + """use_hook_var carries the hook statement and its import in VarData.""" + v = use_hook_var(library="some-lib", hook="useThing") + assert v._var_type is Any + vd = v._get_all_var_data() + assert vd is not None + assert vd.hooks == (f"const {v!s} = useThing();",) + assert vd.imports == (("some-lib", (ImportVar(tag="useThing"),)),) + + +def test_use_hook_var_guesses_var_type(): + """The returned Var is downcast to the class matching _var_type.""" + count = use_hook_var(library="some-lib", hook="useCount", _var_type=int) + assert isinstance(count, NumberVar) + assert count._var_type is int + + maybe = use_hook_var(library="some-lib", hook="useMaybe", _var_type=int | None) + assert maybe._var_type == (int | None) + + +def test_use_hook_var_names_are_unique(): + """Each call binds the hook value to a fresh variable name.""" + names = {str(use_hook_var(library="some-lib", hook="useThing")) for _ in range(5)} + assert len(names) == 5 + + +def test_use_id(): + """use_id returns a str Var bound to React's useId hook.""" + v = use_id() + assert isinstance(v, StringVar) + assert v._var_type is str + vd = v._get_all_var_data() + assert vd is not None + assert vd.hooks == (f"const {v!s} = useId();",) + assert vd.imports == (("react", (ImportVar(tag="useId"),)),) + + +def test_hook_var_hoisted_into_component(): + """A component using a hook var renders the hook and pulls its import.""" + v = use_id() + comp = HookComponent.create(id=v) + assert f"const {v!s} = useId();" in comp._get_all_hooks() + assert ImportVar(tag="useId") in comp._get_all_imports()["react"] + assert comp.render()["props"] == [f"id:{v!s}"] From ad5de4fac9f0f9a57d3a9844813249ce78d78b08 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 17:41:43 +0530 Subject: [PATCH 08/21] Fix Sankey payload typing and package build --- .../reflex-components-recharts/pyproject.toml | 13 +++++++++++-- .../src/reflex_components_recharts/charts.py | 13 +++++-------- tests/units/components/graphing/test_recharts.py | 16 ++++++++++++++++ uv.lock | 6 +++++- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 02b85bd3058..279b7207dcd 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -7,7 +7,10 @@ readme = "README.md" authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" -dependencies = ["reflex-base >= 0.9.7.post31.dev0"] +dependencies = [ + "reflex-base >= 0.9.7.post31.dev0", + "reflex-components-core >= 0.9.0", +] [tool.hatch.version] source = "uv-dynamic-versioning" @@ -21,7 +24,13 @@ targets.sdist.artifacts = ["*.pyi"] targets.wheel.artifacts = ["*.pyi"] [tool.hatch.build.hooks.reflex-pyi] -dependencies = ["ruff", "reflex-base"] +dependencies = [ + "ruff", + "reflex-base", + "reflex-components-core", + "reflex-components-lucide", + "reflex-components-sonner", +] [build-system] requires = ["hatchling", "uv-dynamic-versioning", "hatch-reflex-pyi"] diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index afc0d7ddbdb..a628f69f427 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -556,10 +556,8 @@ class SankeyNodePayload(TypedDict): """The payload for a Sankey chart node.""" name: str - sourceNodes: list[int] - sourceLinks: list[int] - targetLinks: list[int] - targetNodes: list[int] + sourceLinks: list[SankeyLinkPayload] + targetLinks: list[SankeyLinkPayload] value: int | float depth: int x: int | float @@ -582,11 +580,10 @@ class SankeyNodeProps(TypedDict): class SankeyLinkPayload(TypedDict): """The payload for a Sankey chart link.""" - source: int - target: int + source: SankeyNodePayload + target: SankeyNodePayload value: int | float - index: int - width: int | float + dy: int | float sy: int | float ty: int | float diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 9a626c7dc12..8f0d44fd828 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,3 +1,5 @@ +from typing import get_type_hints + from reflex_components_recharts.charts import ( AreaChart, BarChart, @@ -6,6 +8,9 @@ RadarChart, RadialBarChart, SankeyChart, + SankeyLinkPayload, + SankeyLinkProps, + SankeyNodePayload, ScatterChart, ) from reflex_components_recharts.general import ResponsiveContainer, use_chart_width @@ -75,6 +80,17 @@ class SankeyState(rx.State): assert isinstance(sc, ResponsiveContainer) +def test_sankey_link_payload_matches_recharts_runtime_shape(): + link_payload_hints = get_type_hints(SankeyLinkPayload) + assert link_payload_hints["source"] is SankeyNodePayload + assert link_payload_hints["target"] is SankeyNodePayload + assert "dy" in link_payload_hints + assert "width" not in link_payload_hints + assert "index" not in link_payload_hints + + assert get_type_hints(SankeyLinkProps)["index"] is int + + def test_use_chart_width(): width = use_chart_width() assert width._var_type == (int | None) diff --git a/uv.lock b/uv.lock index e6efb6377a0..7f66bb55932 100644 --- a/uv.lock +++ b/uv.lock @@ -3940,10 +3940,14 @@ name = "reflex-components-recharts" source = { editable = "packages/reflex-components-recharts" } dependencies = [ { name = "reflex-base" }, + { name = "reflex-components-core" }, ] [package.metadata] -requires-dist = [{ name = "reflex-base", editable = "packages/reflex-base" }] +requires-dist = [ + { name = "reflex-base", editable = "packages/reflex-base" }, + { name = "reflex-components-core", editable = "packages/reflex-components-core" }, +] [[package]] name = "reflex-components-sonner" From 7a9c1589cdeadc46be19dc814184d063a1927cdd Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 21:55:28 +0530 Subject: [PATCH 09/21] Address Sankey renderer review feedback --- docs/library/graphing/charts/sankeychart.md | 2 +- .../src/reflex_base/vars/special.py | 25 +++- .../src/reflex_components_recharts/charts.py | 107 +++++++++++------- pyi_hashes.json | 2 +- .../components/graphing/test_recharts.py | 40 ++++++- 5 files changed, 128 insertions(+), 48 deletions(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 4af28eb6703..9b6877113b1 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -152,7 +152,7 @@ def sankey_custom_render(): x=rx.cond(is_out, node.x - 6, node.x + node.width + 6), y=node.y + node.height / 2, text_anchor=rx.cond(is_out, "end", "start"), - stroke=rx.color("gray", 12), + fill=rx.color("gray", 12), font_size=10, ), rx.el.svg.rect( diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py index 3d05ef63670..38a6ed1311e 100644 --- a/packages/reflex-base/src/reflex_base/vars/special.py +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -1,12 +1,31 @@ """Special Vars for rendering values from the environment.""" -from typing import Any +from types import UnionType +from typing import Any, TypeVar, cast, overload + +from typing_extensions import TypeForm from reflex_base.utils.types import GenericType from reflex_base.vars.base import Var, VarData, get_unique_variable_name +HOOK_VAR_TYPE = TypeVar("HOOK_VAR_TYPE") + + +@overload +def use_hook_var(library: str, hook: str) -> Var[Any]: ... + + +@overload +def use_hook_var( + library: str, hook: str, _var_type: TypeForm[HOOK_VAR_TYPE] +) -> Var[HOOK_VAR_TYPE]: ... + + +@overload +def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var: ... + -def use_hook_var(library: str, hook: str, _var_type: GenericType = Any) -> Var: +def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var: """Get a Var representing a React hook's value. The value will depend on the context of the component in which it is used. @@ -21,7 +40,7 @@ def use_hook_var(library: str, hook: str, _var_type: GenericType = Any) -> Var: """ return Var( var_name := get_unique_variable_name(), - _var_type=_var_type, + _var_type=cast(GenericType, _var_type), _var_data=VarData( imports={library: hook}, hooks=(f"const {var_name} = {hook}();",), diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index a628f69f427..5ee75bc3eed 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -5,7 +5,7 @@ import inspect from collections.abc import Callable, Mapping, Sequence from types import SimpleNamespace -from typing import Any, ClassVar, TypedDict, get_args, get_origin +from typing import Any, ClassVar, TypedDict, get_args, get_origin, get_type_hints from reflex_base.components.component import Component, field from reflex_base.components.memo import _MemoComponentWrapper, memo @@ -569,12 +569,12 @@ class SankeyNodePayload(TypedDict): class SankeyNodeProps(TypedDict): """The props for a custom Sankey chart node.""" - height: int - width: int + height: int | float + width: int | float payload: SankeyNodePayload index: int - x: int - y: int + x: int | float + y: int | float class SankeyLinkPayload(TypedDict): @@ -591,49 +591,88 @@ class SankeyLinkPayload(TypedDict): class SankeyLinkProps(TypedDict): """The props for a custom Sankey chart link.""" - sourceX: int - targetX: int - sourceY: int - targetY: int - sourceControlX: int - targetControlX: int - sourceRelativeY: int - targetRelativeY: int - linkWidth: int + sourceX: int | float + targetX: int | float + sourceY: int | float + targetY: int | float + sourceControlX: int | float + targetControlX: int | float + sourceRelativeY: int | float + targetRelativeY: int | float + linkWidth: int | float index: int payload: SankeyLinkPayload -def sankey_node( +def _sankey_renderer( fn: Callable, + props_type: type, + decorator_name: str, ) -> _MemoComponentWrapper: - """A decorator to create a custom Sankey chart node. + """Create a memoized Sankey renderer with a typed rest-prop parameter. Args: - fn: A function that takes a SankeyNodeProps and returns a Reflex component. + fn: The renderer function to wrap. + props_type: The TypedDict props type expected by the renderer. + decorator_name: The public decorator name used in error messages. Returns: - A function that takes a SankeyNodeProps and returns a Reflex component. + A memoized renderer wrapper. """ sig = inspect.signature(fn) + if len(sig.parameters) != 1: + msg = ( + f"@{decorator_name} decorated function must take a single argument " + f"of type {props_type.__name__}, got {sig.parameters}" + ) + raise TypeError(msg) + + first_param = next(iter(sig.parameters.values())) + if first_param.kind not in ( + inspect.Parameter.POSITIONAL_OR_KEYWORD, + inspect.Parameter.KEYWORD_ONLY, + ): + msg = ( + f"@{decorator_name} decorated function parameter must be callable " + f"by keyword, got {first_param.kind.description}" + ) + raise TypeError(msg) + + hints = get_type_hints(fn, include_extras=True) + param_annotation = hints.get(first_param.name, first_param.annotation) if ( - len(sig.parameters) != 1 - or (first_param := sig.parameters[next(iter(sig.parameters))]) is None - or get_origin(first_param.annotation) is not Var - or not (args := get_args(first_param.annotation)) - or args[0] is not SankeyNodeProps + get_origin(param_annotation) is not Var + or not (args := get_args(param_annotation)) + or args[0] is not props_type ): - msg = f"@sankey_node decorated function must take a single argument of type SankeyNodeProps, got {sig.parameters}" + msg = ( + f"@{decorator_name} decorated function must take a single argument " + f"of type {props_type.__name__}, got {sig.parameters}" + ) raise TypeError(msg) def _wrapper(rest: RestProp) -> Component: - return fn(**{first_param.name: rest.to(SankeyNodeProps)}) + return fn(**{first_param.name: rest.to(props_type)}) _wrapper.__name__ = fn.__name__ _wrapper.__module__ = fn.__module__ return memo(wrapper=None)(_wrapper) +def sankey_node( + fn: Callable, +) -> _MemoComponentWrapper: + """A decorator to create a custom Sankey chart node. + + Args: + fn: A function that takes a SankeyNodeProps and returns a Reflex component. + + Returns: + A function that takes a SankeyNodeProps and returns a Reflex component. + """ + return _sankey_renderer(fn, SankeyNodeProps, "sankey_node") + + def sankey_link( fn: Callable, ) -> _MemoComponentWrapper: @@ -645,23 +684,7 @@ def sankey_link( Returns: A function that takes a SankeyLinkProps and returns a Reflex component. """ - sig = inspect.signature(fn) - if ( - len(sig.parameters) != 1 - or (first_param := sig.parameters[next(iter(sig.parameters))]) is None - or get_origin(first_param.annotation) is not Var - or not (args := get_args(first_param.annotation)) - or args[0] is not SankeyLinkProps - ): - msg = f"@sankey_link decorated function must take a single argument of type SankeyLinkProps, got {sig.parameters}" - raise TypeError(msg) - - def _wrapper(rest: RestProp) -> Component: - return fn(**{first_param.name: rest.to(SankeyLinkProps)}) - - _wrapper.__name__ = fn.__name__ - _wrapper.__module__ = fn.__module__ - return memo(wrapper=None)(_wrapper) + return _sankey_renderer(fn, SankeyLinkProps, "sankey_link") class SankeyChart(ChartBase): diff --git a/pyi_hashes.json b/pyi_hashes.json index 31c686be09c..6d04c51c78f 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "de544a2efb54d0d90639933ccfa65b77", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "8e7f47e0eb774fdc3b1ce62b40ea4e56", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "ea68076b408c2675e9a6a616d01b5a8a", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "be94d0ac11cca0bd0cfb2519b759897b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 8f0d44fd828..a7f2b0df287 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,5 +1,6 @@ from typing import get_type_hints +import pytest from reflex_components_recharts.charts import ( AreaChart, BarChart, @@ -11,7 +12,9 @@ SankeyLinkPayload, SankeyLinkProps, SankeyNodePayload, + SankeyNodeProps, ScatterChart, + sankey_chart, ) from reflex_components_recharts.general import ResponsiveContainer, use_chart_width from reflex_components_recharts.recharts import Recharts @@ -88,7 +91,42 @@ def test_sankey_link_payload_matches_recharts_runtime_shape(): assert "width" not in link_payload_hints assert "index" not in link_payload_hints - assert get_type_hints(SankeyLinkProps)["index"] is int + link_props_hints = get_type_hints(SankeyLinkProps) + assert link_props_hints["index"] is int + assert link_props_hints["linkWidth"] == (int | float) + + +def test_sankey_renderer_decorators_accept_deferred_annotations(): + namespace = { + "rx": rx, + "SankeyLinkProps": SankeyLinkProps, + "SankeyNodeProps": SankeyNodeProps, + } + exec( + """ +from __future__ import annotations + + +def custom_node(node: rx.Var[SankeyNodeProps]) -> rx.Component: + return rx.fragment() + + +def custom_link(link: rx.Var[SankeyLinkProps]) -> rx.Component: + return rx.fragment() +""", + namespace, + ) + + assert callable(sankey_chart.node(namespace["custom_node"])) + assert callable(sankey_chart.link(namespace["custom_link"])) + + +def test_sankey_renderer_decorator_rejects_positional_only_parameter(): + def custom_node(node: rx.Var[SankeyNodeProps], /) -> rx.Component: + return rx.fragment() + + with pytest.raises(TypeError, match="keyword"): + sankey_chart.node(custom_node) def test_use_chart_width(): From 6a09841b801afe48fd9743bc340d0d3c9db7008c Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 23:31:46 +0530 Subject: [PATCH 10/21] Address Sankey docs and namespace feedback --- docs/library/graphing/charts/sankeychart.md | 5 +-- .../src/reflex_base/vars/special.py | 9 +++-- .../reflex-components-recharts/pyproject.toml | 2 +- .../src/reflex_components_recharts/charts.py | 13 ++++--- pyi_hashes.json | 2 +- .../components/graphing/test_recharts.py | 16 +++++++-- tests/units/reflex_base/vars/test_special.py | 35 +++++++++++++++---- 7 files changed, 60 insertions(+), 22 deletions(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 9b6877113b1..6d0d9a0e424 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -9,6 +9,7 @@ meta_description: "Create Sankey charts in Python with Reflex. Build interactive ```python exec import random +from typing import Any import reflex as rx ``` @@ -55,7 +56,7 @@ Chart data can be tied to a State var. This example randomizes the flow values w ```python demo exec class SankeyState(rx.State): - data = { + data: dict[str, Any] = { "nodes": [ {"name": "Marketing"}, {"name": "Trial"}, @@ -185,7 +186,7 @@ def sankey_custom_render(): x=(link.sourceX + link.targetX) / 2, y=(link.sourceY + link.targetY) / 2, text_anchor="middle", - stroke=rx.color("gray", 12), + fill=rx.color("gray", 12), font_size=10, ), rx.el.svg.path( diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py index 38a6ed1311e..97c34b99fb6 100644 --- a/packages/reflex-base/src/reflex_base/vars/special.py +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -5,6 +5,7 @@ from typing_extensions import TypeForm +from reflex_base.utils.imports import ImportVar from reflex_base.utils.types import GenericType from reflex_base.vars.base import Var, VarData, get_unique_variable_name @@ -38,12 +39,14 @@ def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var: Returns: A Var representing the React hook. """ + var_name = get_unique_variable_name() + hook_alias = f"{hook}_{var_name}" return Var( - var_name := get_unique_variable_name(), + var_name, _var_type=cast(GenericType, _var_type), _var_data=VarData( - imports={library: hook}, - hooks=(f"const {var_name} = {hook}();",), + imports={library: ImportVar(tag=hook, alias=hook_alias)}, + hooks=(f"const {var_name} = {hook_alias}();",), ), ).guess_type() diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 279b7207dcd..063a67470d1 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "reflex-base >= 0.9.7.post31.dev0", + "reflex-base >= 0.9.7", "reflex-components-core >= 0.9.0", ] diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index 5ee75bc3eed..e80253aedbd 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -4,11 +4,10 @@ import inspect from collections.abc import Callable, Mapping, Sequence -from types import SimpleNamespace from typing import Any, ClassVar, TypedDict, get_args, get_origin, get_type_hints -from reflex_base.components.component import Component, field -from reflex_base.components.memo import _MemoComponentWrapper, memo +from reflex_base.components.component import Component, ComponentNamespace, field +from reflex_base.components.memo import memo from reflex_base.constants import EventTriggers from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec @@ -608,7 +607,7 @@ def _sankey_renderer( fn: Callable, props_type: type, decorator_name: str, -) -> _MemoComponentWrapper: +) -> Callable[..., Component]: """Create a memoized Sankey renderer with a typed rest-prop parameter. Args: @@ -661,7 +660,7 @@ def _wrapper(rest: RestProp) -> Component: def sankey_node( fn: Callable, -) -> _MemoComponentWrapper: +) -> Callable[..., Component]: """A decorator to create a custom Sankey chart node. Args: @@ -675,7 +674,7 @@ def sankey_node( def sankey_link( fn: Callable, -) -> _MemoComponentWrapper: +) -> Callable[..., Component]: """A decorator to create a custom Sankey chart link. Args: @@ -736,7 +735,7 @@ class SankeyChart(ChartBase): ] -class SankeyNamespace(SimpleNamespace): +class SankeyNamespace(ComponentNamespace): """A namespace for the Sankey chart components.""" node = staticmethod(sankey_node) diff --git a/pyi_hashes.json b/pyi_hashes.json index 6d04c51c78f..267364a2f4f 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,7 +113,7 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "de544a2efb54d0d90639933ccfa65b77", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "ea68076b408c2675e9a6a616d01b5a8a", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "d410ba40f14146c3491832a2c4f12132", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "be94d0ac11cca0bd0cfb2519b759897b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index a7f2b0df287..b870849342d 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,6 +1,10 @@ from typing import get_type_hints import pytest +from reflex_base.components.component import ( + ComponentNamespace, + evaluate_style_namespaces, +) from reflex_components_recharts.charts import ( AreaChart, BarChart, @@ -72,6 +76,13 @@ def test_sankey_chart(): assert "link_width" not in SankeyChart.get_props() +def test_sankey_chart_namespace_can_be_used_as_style_key(): + assert isinstance(sankey_chart, ComponentNamespace) + assert evaluate_style_namespaces({sankey_chart: {"height": "20rem"}}) == { + SankeyChart.create: {"height": "20rem"} + } + + def test_sankey_chart_accepts_unannotated_state_data(): class SankeyState(rx.State): data = { @@ -134,7 +145,8 @@ def test_use_chart_width(): assert width._var_type == (int | None) var_data = width._get_all_var_data() assert var_data is not None - assert var_data.hooks == (f"const {width!s} = useChartWidth();",) + hook_alias = f"useChartWidth_{width!s}" + assert var_data.hooks == (f"const {width!s} = {hook_alias}();",) assert dict(var_data.imports)[Recharts.library or ""] == ( - rx.ImportVar(tag="useChartWidth"), + rx.ImportVar(tag="useChartWidth", alias=hook_alias), ) diff --git a/tests/units/reflex_base/vars/test_special.py b/tests/units/reflex_base/vars/test_special.py index c86322507c0..11c49c1bd2a 100644 --- a/tests/units/reflex_base/vars/test_special.py +++ b/tests/units/reflex_base/vars/test_special.py @@ -23,8 +23,9 @@ def test_use_hook_var_var_data(): assert v._var_type is Any vd = v._get_all_var_data() assert vd is not None - assert vd.hooks == (f"const {v!s} = useThing();",) - assert vd.imports == (("some-lib", (ImportVar(tag="useThing"),)),) + hook_alias = f"useThing_{v!s}" + assert vd.hooks == (f"const {v!s} = {hook_alias}();",) + assert vd.imports == (("some-lib", (ImportVar(tag="useThing", alias=hook_alias),)),) def test_use_hook_var_guesses_var_type(): @@ -43,6 +44,26 @@ def test_use_hook_var_names_are_unique(): assert len(names) == 5 +def test_same_named_hook_imports_are_aliased(): + """Same-named hooks from different libraries do not collide in JS imports.""" + first = use_hook_var(library="first-lib", hook="useThing") + second = use_hook_var(library="second-lib", hook="useThing") + first_alias = f"useThing_{first!s}" + second_alias = f"useThing_{second!s}" + first_var_data = first._get_all_var_data() + second_var_data = second._get_all_var_data() + + assert first_alias != second_alias + assert first_var_data is not None + assert second_var_data is not None + assert first_var_data.imports == ( + ("first-lib", (ImportVar(tag="useThing", alias=first_alias),)), + ) + assert second_var_data.imports == ( + ("second-lib", (ImportVar(tag="useThing", alias=second_alias),)), + ) + + def test_use_id(): """use_id returns a str Var bound to React's useId hook.""" v = use_id() @@ -50,14 +71,16 @@ def test_use_id(): assert v._var_type is str vd = v._get_all_var_data() assert vd is not None - assert vd.hooks == (f"const {v!s} = useId();",) - assert vd.imports == (("react", (ImportVar(tag="useId"),)),) + hook_alias = f"useId_{v!s}" + assert vd.hooks == (f"const {v!s} = {hook_alias}();",) + assert vd.imports == (("react", (ImportVar(tag="useId", alias=hook_alias),)),) def test_hook_var_hoisted_into_component(): """A component using a hook var renders the hook and pulls its import.""" v = use_id() comp = HookComponent.create(id=v) - assert f"const {v!s} = useId();" in comp._get_all_hooks() - assert ImportVar(tag="useId") in comp._get_all_imports()["react"] + hook_alias = f"useId_{v!s}" + assert f"const {v!s} = {hook_alias}();" in comp._get_all_hooks() + assert ImportVar(tag="useId", alias=hook_alias) in comp._get_all_imports()["react"] assert comp.render()["props"] == [f"id:{v!s}"] From aa25c05d0a7088de162b846d31f40baab9eaf0c7 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Tue, 4 Aug 2026 08:58:49 +0530 Subject: [PATCH 11/21] Address final Sankey review nits --- docs/library/graphing/charts/sankeychart.md | 16 ++++++++-------- .../reflex-base/src/reflex_base/vars/special.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 6d0d9a0e424..d6fe99c2791 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -181,14 +181,6 @@ def sankey_custom_render(): rx.el.svg.stop(offset="100%", stop_color=target["fill"]), id=link_id, ), - rx.el.svg.text( - link.payload.value, - x=(link.sourceX + link.targetX) / 2, - y=(link.sourceY + link.targetY) / 2, - text_anchor="middle", - fill=rx.color("gray", 12), - font_size=10, - ), rx.el.svg.path( d=( f"M{link.sourceX},{link.sourceY} " @@ -201,6 +193,14 @@ def sankey_custom_render(): stroke_opacity=0.35, stroke_width=link.linkWidth, ), + rx.el.svg.text( + link.payload.value, + x=(link.sourceX + link.targetX) / 2, + y=(link.sourceY + link.targetY) / 2, + text_anchor="middle", + fill=rx.color("gray", 12), + font_size=10, + ), ) return rx.recharts.sankey_chart( diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py index 97c34b99fb6..32e46bace65 100644 --- a/packages/reflex-base/src/reflex_base/vars/special.py +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -23,7 +23,7 @@ def use_hook_var( @overload -def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var: ... +def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var[Any]: ... def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var: From 43e4f1ab64691afd57b0ec250778a1b1a3b3e756 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 13 Aug 2026 08:28:37 +0530 Subject: [PATCH 12/21] Fix Sankey docs renderer type --- docs/library/graphing/charts/sankeychart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index d6fe99c2791..87c1da5aab2 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -150,7 +150,7 @@ def sankey_custom_render(): return rx.fragment( rx.el.svg.text( node.payload.name, - x=rx.cond(is_out, node.x - 6, node.x + node.width + 6), + x=rx.cond(is_out, node.x - 6, node.x + node.width + 6).to(int), y=node.y + node.height / 2, text_anchor=rx.cond(is_out, "end", "start"), fill=rx.color("gray", 12), From 3e662db59344b15aeae2ead7a35723646cfab0d7 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Wed, 9 Sep 2026 14:48:52 +0530 Subject: [PATCH 13/21] fix docs Sankey SVG coordinate types --- docs/library/graphing/charts/sankeychart.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 87c1da5aab2..6dbcbf1f9fa 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -151,7 +151,7 @@ def sankey_custom_render(): rx.el.svg.text( node.payload.name, x=rx.cond(is_out, node.x - 6, node.x + node.width + 6).to(int), - y=node.y + node.height / 2, + y=(node.y + node.height / 2).to(int), text_anchor=rx.cond(is_out, "end", "start"), fill=rx.color("gray", 12), font_size=10, @@ -195,8 +195,8 @@ def sankey_custom_render(): ), rx.el.svg.text( link.payload.value, - x=(link.sourceX + link.targetX) / 2, - y=(link.sourceY + link.targetY) / 2, + x=((link.sourceX + link.targetX) / 2).to(int), + y=((link.sourceY + link.targetY) / 2).to(int), text_anchor="middle", fill=rx.color("gray", 12), font_size=10, From 07c41fde48741c704d6c46ab9f063e3a1da8938e Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Wed, 9 Sep 2026 16:35:31 +0530 Subject: [PATCH 14/21] fix Sankey node coordinate types --- docs/library/graphing/charts/sankeychart.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 6dbcbf1f9fa..06603ad0137 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -157,10 +157,10 @@ def sankey_custom_render(): font_size=10, ), rx.el.svg.rect( - x=node.x, - y=node.y, - width=node.width, - height=node.height, + x=node.x.to(int), + y=node.y.to(int), + width=node.width.to(int), + height=node.height.to(int), # Accessing custom keys in the payload needs a `dict` cast. fill=node.payload.to(dict)["fill"], stroke=rx.color("gray", 12), From ff15e6adad4a0b2055a0ee759db99db9288eeabf Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Wed, 9 Sep 2026 17:02:08 +0530 Subject: [PATCH 15/21] refactor hook identifiers into constants --- packages/reflex-base/src/reflex_base/vars/special.py | 4 +++- .../src/reflex_components_recharts/general.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py index 32e46bace65..2f72be6ede8 100644 --- a/packages/reflex-base/src/reflex_base/vars/special.py +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -10,6 +10,8 @@ from reflex_base.vars.base import Var, VarData, get_unique_variable_name HOOK_VAR_TYPE = TypeVar("HOOK_VAR_TYPE") +_REACT_LIBRARY = "react" +_USE_ID_HOOK = "useId" @overload @@ -57,4 +59,4 @@ def use_id() -> Var[str]: Returns: A Var representing the useId hook value. """ - return use_hook_var(library="react", hook="useId", _var_type=str) + return use_hook_var(library=_REACT_LIBRARY, hook=_USE_ID_HOOK, _var_type=str) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index c961ea45c21..c4bbe087b1e 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -21,6 +21,8 @@ Recharts, ) +_USE_CHART_WIDTH_HOOK = "useChartWidth" + class ResponsiveContainer(Recharts, MemoizationLeaf): """A base class for responsive containers in Recharts.""" @@ -310,7 +312,9 @@ def use_chart_width() -> Var[int | None]: The chart width var. """ return use_hook_var( - library=Recharts.library or "", hook="useChartWidth", _var_type=int | None + library=Recharts.library or "", + hook=_USE_CHART_WIDTH_HOOK, + _var_type=int | None, ) From d80d7774cd02c34afd8f60b0dfa01fe85c075333 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Wed, 9 Sep 2026 17:17:05 +0530 Subject: [PATCH 16/21] update generated pyi hash --- pyi_hashes.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index 0d19551469b..73dc1892c50 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -114,7 +114,7 @@ "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "64c1dd9815c47304e10624f87e8d0896", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "d410ba40f14146c3491832a2c4f12132", - "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "8c5b5f983ff6b8682e171c3e8a146a40", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "fe22b3cf69e9ae3d425cc88c3511525b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f170ac685b6ba5892370166c80684db3", From 9c1bc20b15c3792a00214f95b2fd6f0864600987 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 10 Sep 2026 14:19:47 +0530 Subject: [PATCH 17/21] fix Sankey typing and hook documentation --- docs/api-reference/var_system.md | 3 +++ .../reflex-components-recharts/pyproject.toml | 2 +- .../src/reflex_components_recharts/charts.py | 23 +++++++++++++------ .../src/reflex_components_recharts/general.py | 3 ++- .../reflex_components_recharts/recharts.py | 6 +++-- tests/units/reflex_base/vars/test_special.py | 10 ++++++++ 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md index 224509f5c94..54aca1cac5b 100644 --- a/docs/api-reference/var_system.md +++ b/docs/api-reference/var_system.md @@ -95,10 +95,13 @@ chart_width = rx.vars.use_hook_var( A component using `chart_width` will import `useChartWidth` from `recharts` and render `const = useChartWidth();` in its body, so `chart_width` can be used like any other `Var[int | None]`. +Hook vars are scoped to the component that consumes them. Use a hook var within one component body, or within one `@rx.memo` or custom renderer body. Do not share the same hook var between sibling components, because each consumer creates its own hook value. + For the common case of React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` containing a stable unique id for the rendered component. This is useful for linking SVG elements to `defs` such as gradients or filters: ```py +@rx.memo def gradient_rect() -> rx.Component: gradient_id = rx.vars.use_id() return rx.el.svg( diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 063a67470d1..279b7207dcd 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "reflex-base >= 0.9.7", + "reflex-base >= 0.9.7.post31.dev0", "reflex-components-core >= 0.9.0", ] diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index bc1e9256cfa..be4aae3c6a8 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -621,6 +621,15 @@ class SankeyLinkProps(TypedDict): payload: SankeyLinkPayload +_SankeyNodeType = SankeyNode +_SankeyLinkType = SankeyLink +_SankeyDataType = SankeyData +_SankeyNodePayloadType = SankeyNodePayload +_SankeyNodePropsType = SankeyNodeProps +_SankeyLinkPayloadType = SankeyLinkPayload +_SankeyLinkPropsType = SankeyLinkProps + + def _sankey_renderer( fn: Callable, props_type: type, @@ -761,13 +770,13 @@ class SankeyNamespace(ComponentNamespace): __call__ = staticmethod(SankeyChart.create) # For type checking - SankeyNode = SankeyNode - SankeyLink = SankeyLink - SankeyData = SankeyData - SankeyNodePayload = SankeyNodePayload - SankeyNodeProps = SankeyNodeProps - SankeyLinkPayload = SankeyLinkPayload - SankeyLinkProps = SankeyLinkProps + SankeyNode = _SankeyNodeType + SankeyLink = _SankeyLinkType + SankeyData = _SankeyDataType + SankeyNodePayload = _SankeyNodePayloadType + SankeyNodeProps = _SankeyNodePropsType + SankeyLinkPayload = _SankeyLinkPayloadType + SankeyLinkProps = _SankeyLinkPropsType class Treemap(RechartsCharts): diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index c4bbe087b1e..0332c0f4ef5 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -12,6 +12,7 @@ from reflex_base.vars.special import use_hook_var from .recharts import ( + _RECHARTS_LIBRARY, LiteralAnimationEasing, LiteralIconType, LiteralLayout, @@ -312,7 +313,7 @@ def use_chart_width() -> Var[int | None]: The chart width var. """ return use_hook_var( - library=Recharts.library or "", + library=_RECHARTS_LIBRARY, hook=_USE_CHART_WIDTH_HOOK, _var_type=int | None, ) diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py index 8ea9458ad4a..13cd60ecff0 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py @@ -4,11 +4,13 @@ from reflex_base.components.component import Component, MemoizationLeaf, NoSSRComponent +_RECHARTS_LIBRARY = "recharts@3.8.1" + class Recharts(Component): """A component that wraps a recharts lib.""" - library = "recharts@3.8.1" + library = _RECHARTS_LIBRARY def _get_style(self) -> dict: return {"wrapperStyle": self.style} @@ -17,7 +19,7 @@ def _get_style(self) -> dict: class RechartsCharts(NoSSRComponent, MemoizationLeaf): """A component that wraps a recharts lib.""" - library = "recharts@3.8.1" + library = _RECHARTS_LIBRARY LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"] diff --git a/tests/units/reflex_base/vars/test_special.py b/tests/units/reflex_base/vars/test_special.py index 11c49c1bd2a..186204bb00a 100644 --- a/tests/units/reflex_base/vars/test_special.py +++ b/tests/units/reflex_base/vars/test_special.py @@ -84,3 +84,13 @@ def test_hook_var_hoisted_into_component(): assert f"const {v!s} = {hook_alias}();" in comp._get_all_hooks() assert ImportVar(tag="useId", alias=hook_alias) in comp._get_all_imports()["react"] assert comp.render()["props"] == [f"id:{v!s}"] + + +def test_hook_var_is_scoped_to_each_consuming_component(): + """A hook var must be consumed within one component body.""" + v = use_id() + first = HookComponent.create(id=v) + second = HookComponent.create(id=v) + + assert first._get_all_hooks() == second._get_all_hooks() + assert list(first._get_all_hooks()) == [f"const {v!s} = useId_{v!s}();"] From 561e403151c271f1d842960de915eec9e7b66371 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 10 Sep 2026 16:49:16 +0530 Subject: [PATCH 18/21] Update generated Recharts stub hashes --- pyi_hashes.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyi_hashes.json b/pyi_hashes.json index a457784f349..62819b061c9 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -113,10 +113,10 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "64c1dd9815c47304e10624f87e8d0896", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "d410ba40f14146c3491832a2c4f12132", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "156bddcab75a04bfab77ca37af2843c6", "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "fe22b3cf69e9ae3d425cc88c3511525b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", - "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "f2a9cf6db58d169289fbc76de6813300", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f170ac685b6ba5892370166c80684db3", "reflex/__init__.pyi": "a3e1782fab4a9aed55f66cc98af8c217", "reflex/components/__init__.pyi": "9facd05a776d0641432696bbf8e34388", From c1939a2d93c1e4e84c69951d89ad99f52245cc01 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 10 Sep 2026 19:52:37 +0500 Subject: [PATCH 19/21] Drop the hook vars from the Sankey wrapper and export its TypedDicts The Sankey chart does not need `use_hook_var`, `use_id` or `use_chart_width`. Those vars hoist their hook into every component that consumes them, so two siblings sharing one `use_id()` value get different ids and the documented gradient example rendered nothing. That is a compiler ownership question, and it belongs in its own change rather than in a component wrapper. Remove the `reflex_base.vars.special` module, its docs and test, and the `reflex-base` development pin they required. Rebuild the custom link example on the link `index`, which is unique within a chart, and anchor the outcome label from the node payload instead of the chart width. Export the Sankey TypedDicts from the package module so that `rx.Var[rx.recharts.SankeyNodeProps]` is a valid type expression. The namespace attributes were reached through an instance, which pyright rejects in a type expression. Claude-Session: https://claude.ai/code/session_01QWny288Mh7ymvkb4n6eBB7 --- docs/api-reference/var_system.md | 37 ------- docs/library/graphing/charts/sankeychart.md | 16 ++-- docs/wrapping-react/custom-code-and-hooks.md | 17 ---- packages/reflex-base/news/6708.feature.md | 1 - .../src/reflex_base/vars/__init__.py | 3 - .../src/reflex_base/vars/special.py | 62 ------------ .../news/6708.feature.md | 2 +- .../reflex-components-recharts/pyproject.toml | 2 +- .../reflex_components_recharts/__init__.py | 8 +- .../src/reflex_components_recharts/charts.py | 18 ---- .../src/reflex_components_recharts/general.py | 19 ---- .../reflex_components_recharts/recharts.py | 6 +- pyi_hashes.json | 8 +- .../components/graphing/test_recharts.py | 27 +++--- tests/units/reflex_base/vars/test_special.py | 96 ------------------- 15 files changed, 38 insertions(+), 284 deletions(-) delete mode 100644 packages/reflex-base/news/6708.feature.md delete mode 100644 packages/reflex-base/src/reflex_base/vars/special.py delete mode 100644 tests/units/reflex_base/vars/test_special.py diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md index 54aca1cac5b..469ee8dca83 100644 --- a/docs/api-reference/var_system.md +++ b/docs/api-reference/var_system.md @@ -78,40 +78,3 @@ def factorial(value: NumberVar): Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1. Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`. - -## Hook Vars - -Some values only exist on the frontend and are exposed through React hooks. -`rx.vars.use_hook_var()` binds the return value of a no-argument hook to a unique variable name and returns it as a `Var`. -The hook call and the import of the hook are automatically included in any component that uses the var, so the value reflects the context of the component it is rendered in. - -```py -chart_width = rx.vars.use_hook_var( - library="recharts@3.8.1", - hook="useChartWidth", - _var_type=int | None, -) -``` - -A component using `chart_width` will import `useChartWidth` from `recharts` and render `const = useChartWidth();` in its body, so `chart_width` can be used like any other `Var[int | None]`. - -Hook vars are scoped to the component that consumes them. Use a hook var within one component body, or within one `@rx.memo` or custom renderer body. Do not share the same hook var between sibling components, because each consumer creates its own hook value. - -For the common case of React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` containing a stable unique id for the rendered component. -This is useful for linking SVG elements to `defs` such as gradients or filters: - -```py -@rx.memo -def gradient_rect() -> rx.Component: - gradient_id = rx.vars.use_id() - return rx.el.svg( - rx.el.svg.linear_gradient( - rx.el.svg.stop(offset="0%", stop_color="gold"), - rx.el.svg.stop(offset="100%", stop_color="tomato"), - id=gradient_id, - ), - rx.el.svg.rect(fill=f"url(#{gradient_id})", width=64, height=64), - width=64, - height=64, - ) -``` diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index 06603ad0137..b7571a116bd 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -116,9 +116,9 @@ properties to construct a custom node or link. Because the component renders inside the SVG element of the chart, you can only use `rx.el.svg` components to construct the custom node or link. -The example below also uses `rx.recharts.use_chart_width()` to read the -rendered chart width and `rx.vars.use_id()` to generate a unique id that links -each link's gradient definition to the path that references it. +The example below uses the link `index` to give each link's gradient +definition an id that the path references. The index is unique within one +chart, so prefix the id if a page renders several charts with custom links. ```python demo graphing styled_sankey_data = { @@ -143,10 +143,10 @@ styled_sankey_data = { def sankey_custom_render(): @rx.recharts.sankey_chart.node def custom_node( - node: rx.Var[rx.recharts.sankey_chart.SankeyNodeProps], + node: rx.Var[rx.recharts.SankeyNodeProps], ) -> rx.Component: - # Determine if the node is at the right edge of the chart to adjust the label position accordingly. - is_out = node.x + node.width + 6 > rx.recharts.use_chart_width() + # Outcome nodes sit at the right edge, so their label goes on the left. + is_out = node.payload.to(dict)["type"] == "outcome" return rx.fragment( rx.el.svg.text( node.payload.name, @@ -170,9 +170,9 @@ def sankey_custom_render(): @rx.recharts.sankey_chart.link def custom_link( - link: rx.Var[rx.recharts.sankey_chart.SankeyLinkProps], + link: rx.Var[rx.recharts.SankeyLinkProps], ) -> rx.Component: - link_id = rx.vars.use_id() + link_id = f"sankey-link-{link.index}" source = link.payload.source.to(dict) target = link.payload.target.to(dict) return rx.fragment( diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md index 10c0cfc71c4..c35f908d678 100644 --- a/docs/wrapping-react/custom-code-and-hooks.md +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -114,20 +114,3 @@ export function Div_7178f430b7b371af8a12d8265d65ab9b() { ```md alert info # You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. ``` - -## Using a Hook's Return Value - -`add_hooks` inserts hook statements into the component, but the values they define are not directly accessible from Python. When you need the return value of a no-argument hook, use `rx.vars.use_hook_var()`, which binds the hook call to a unique variable name and returns it as a `Var`. The hook statement and its import are automatically included in any component where the var is used, so it composes with regular props and var operations. - -```python -import reflex as rx - - -def use_chart_width() -> rx.Var[int | None]: - """Get the width of the enclosing recharts chart as a var.""" - return rx.vars.use_hook_var( - library="recharts@3.8.1", hook="useChartWidth", _var_type=int | None - ) -``` - -For React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` with a stable unique id for the component being rendered, e.g. for linking SVG elements to gradient or filter definitions. diff --git a/packages/reflex-base/news/6708.feature.md b/packages/reflex-base/news/6708.feature.md deleted file mode 100644 index 21e05d1f2f9..00000000000 --- a/packages/reflex-base/news/6708.feature.md +++ /dev/null @@ -1 +0,0 @@ -Add `rx.vars.use_hook_var()` to create a `Var` bound to the value of a no-argument React hook imported from a given library, and `rx.vars.use_id()` to get React's stable `useId` value for the rendered component. diff --git a/packages/reflex-base/src/reflex_base/vars/__init__.py b/packages/reflex-base/src/reflex_base/vars/__init__.py index 2c78f8964df..c986cf1fd36 100644 --- a/packages/reflex-base/src/reflex_base/vars/__init__.py +++ b/packages/reflex-base/src/reflex_base/vars/__init__.py @@ -28,7 +28,6 @@ LiteralStringVar, StringVar, ) -from .special import use_hook_var, use_id __all__ = [ "EMPTY_VAR_INT", @@ -67,8 +66,6 @@ "number", "object", "sequence", - "use_hook_var", - "use_id", "var_operation", "var_operation_return", ] diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py deleted file mode 100644 index 2f72be6ede8..00000000000 --- a/packages/reflex-base/src/reflex_base/vars/special.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Special Vars for rendering values from the environment.""" - -from types import UnionType -from typing import Any, TypeVar, cast, overload - -from typing_extensions import TypeForm - -from reflex_base.utils.imports import ImportVar -from reflex_base.utils.types import GenericType -from reflex_base.vars.base import Var, VarData, get_unique_variable_name - -HOOK_VAR_TYPE = TypeVar("HOOK_VAR_TYPE") -_REACT_LIBRARY = "react" -_USE_ID_HOOK = "useId" - - -@overload -def use_hook_var(library: str, hook: str) -> Var[Any]: ... - - -@overload -def use_hook_var( - library: str, hook: str, _var_type: TypeForm[HOOK_VAR_TYPE] -) -> Var[HOOK_VAR_TYPE]: ... - - -@overload -def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var[Any]: ... - - -def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var: - """Get a Var representing a React hook's value. - - The value will depend on the context of the component in which it is used. - - Args: - library: The library to import the hook from. - hook: The name of the hook. - _var_type: The type of the Var. - - Returns: - A Var representing the React hook. - """ - var_name = get_unique_variable_name() - hook_alias = f"{hook}_{var_name}" - return Var( - var_name, - _var_type=cast(GenericType, _var_type), - _var_data=VarData( - imports={library: ImportVar(tag=hook, alias=hook_alias)}, - hooks=(f"const {var_name} = {hook_alias}();",), - ), - ).guess_type() - - -def use_id() -> Var[str]: - """Get the stable React useId hook value for a component. - - Returns: - A Var representing the useId hook value. - """ - return use_hook_var(library=_REACT_LIBRARY, hook=_USE_ID_HOOK, _var_type=str) diff --git a/packages/reflex-components-recharts/news/6708.feature.md b/packages/reflex-components-recharts/news/6708.feature.md index 9627636e7b7..e9420097751 100644 --- a/packages/reflex-components-recharts/news/6708.feature.md +++ b/packages/reflex-components-recharts/news/6708.feature.md @@ -1 +1 @@ -Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers, and `rx.recharts.use_chart_width()` for reading the rendered chart width as a `Var`. +Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers. diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index f7392f8c312..3e3b8848cdb 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "reflex-base >= 0.9.7.post31.dev0", + "reflex-base >= 0.9.7", "reflex-components-core >= 0.9.0", ] diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index 07a376f7912..f31a2604f55 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -58,6 +58,13 @@ "FunnelChart", "sankey_chart", "SankeyChart", + "SankeyNode", + "SankeyLink", + "SankeyData", + "SankeyNodePayload", + "SankeyNodeProps", + "SankeyLinkPayload", + "SankeyLinkProps", "treemap", "Treemap", ], @@ -75,7 +82,6 @@ "LabelList", "cell", "Cell", - "use_chart_width", "layer", "Layer", "rectangle", diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py index be4aae3c6a8..2cdf4df9d6a 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/charts.py @@ -621,15 +621,6 @@ class SankeyLinkProps(TypedDict): payload: SankeyLinkPayload -_SankeyNodeType = SankeyNode -_SankeyLinkType = SankeyLink -_SankeyDataType = SankeyData -_SankeyNodePayloadType = SankeyNodePayload -_SankeyNodePropsType = SankeyNodeProps -_SankeyLinkPayloadType = SankeyLinkPayload -_SankeyLinkPropsType = SankeyLinkProps - - def _sankey_renderer( fn: Callable, props_type: type, @@ -769,15 +760,6 @@ class SankeyNamespace(ComponentNamespace): link = staticmethod(sankey_link) __call__ = staticmethod(SankeyChart.create) - # For type checking - SankeyNode = _SankeyNodeType - SankeyLink = _SankeyLinkType - SankeyData = _SankeyDataType - SankeyNodePayload = _SankeyNodePayloadType - SankeyNodeProps = _SankeyNodePropsType - SankeyLinkPayload = _SankeyLinkPayloadType - SankeyLinkProps = _SankeyLinkPropsType - class Treemap(RechartsCharts): """A Treemap chart component in Recharts.""" diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index 0332c0f4ef5..e2928795091 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -9,10 +9,8 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import LiteralVar, Var -from reflex_base.vars.special import use_hook_var from .recharts import ( - _RECHARTS_LIBRARY, LiteralAnimationEasing, LiteralIconType, LiteralLayout, @@ -22,8 +20,6 @@ Recharts, ) -_USE_CHART_WIDTH_HOOK = "useChartWidth" - class ResponsiveContainer(Recharts, MemoizationLeaf): """A base class for responsive containers in Recharts.""" @@ -304,21 +300,6 @@ class Cell(Recharts): ) -def use_chart_width() -> Var[int | None]: - """Get the chart width as a var. - - Outside of a chart context, this will be None/undefined. - - Returns: - The chart width var. - """ - return use_hook_var( - library=_RECHARTS_LIBRARY, - hook=_USE_CHART_WIDTH_HOOK, - _var_type=int | None, - ) - - class SvgElement(Recharts): """A Recharts component that renders a plain SVG element, which accepts the style prop directly rather than through recharts' wrapperStyle. diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py index 492800031a5..bebf93a8a07 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py @@ -4,13 +4,11 @@ from reflex_base.components.component import Component, MemoizationLeaf, NoSSRComponent -_RECHARTS_LIBRARY = "recharts@3.10.1" - class Recharts(Component): """A component that wraps a recharts lib.""" - library = _RECHARTS_LIBRARY + library = "recharts@3.10.1" def _get_style(self) -> dict: return {"wrapperStyle": self.style} @@ -19,7 +17,7 @@ def _get_style(self) -> dict: class RechartsCharts(NoSSRComponent, MemoizationLeaf): """A component that wraps a recharts lib.""" - library = _RECHARTS_LIBRARY + library = "recharts@3.10.1" LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"] diff --git a/pyi_hashes.json b/pyi_hashes.json index 62819b061c9..d42513b5895 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -111,12 +111,12 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "39e4144cef066bbff8c27b36a205a54b", "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "8aa4c31da45479cab21ed6d597013008", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", - "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "64c1dd9815c47304e10624f87e8d0896", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "8ce4313f4287dd0ea39cb35e90cf14bb", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", - "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "156bddcab75a04bfab77ca37af2843c6", - "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "fe22b3cf69e9ae3d425cc88c3511525b", + "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "23a5a3670cb1070f733a2d0c65623733", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "45fa9c7a3da5614dfe5bd7e7c62aaa92", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", - "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "f2a9cf6db58d169289fbc76de6813300", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f170ac685b6ba5892370166c80684db3", "reflex/__init__.pyi": "a3e1782fab4a9aed55f66cc98af8c217", "reflex/components/__init__.pyi": "9facd05a776d0641432696bbf8e34388", diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index b870849342d..753b0aa745c 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -20,8 +20,7 @@ ScatterChart, sankey_chart, ) -from reflex_components_recharts.general import ResponsiveContainer, use_chart_width -from reflex_components_recharts.recharts import Recharts +from reflex_components_recharts.general import ResponsiveContainer import reflex as rx @@ -140,13 +139,17 @@ def custom_node(node: rx.Var[SankeyNodeProps], /) -> rx.Component: sankey_chart.node(custom_node) -def test_use_chart_width(): - width = use_chart_width() - assert width._var_type == (int | None) - var_data = width._get_all_var_data() - assert var_data is not None - hook_alias = f"useChartWidth_{width!s}" - assert var_data.hooks == (f"const {width!s} = {hook_alias}();",) - assert dict(var_data.imports)[Recharts.library or ""] == ( - rx.ImportVar(tag="useChartWidth", alias=hook_alias), - ) +def test_sankey_typed_dicts_exported_from_package(): + import reflex_components_recharts as recharts + from reflex_components_recharts import charts + + for name in ( + "SankeyNode", + "SankeyLink", + "SankeyData", + "SankeyNodePayload", + "SankeyNodeProps", + "SankeyLinkPayload", + "SankeyLinkProps", + ): + assert getattr(recharts, name) is getattr(charts, name) diff --git a/tests/units/reflex_base/vars/test_special.py b/tests/units/reflex_base/vars/test_special.py deleted file mode 100644 index 186204bb00a..00000000000 --- a/tests/units/reflex_base/vars/test_special.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Tests for reflex_base.vars.special hook-backed vars.""" - -from typing import Any - -from reflex_base.components.component import Component -from reflex_base.utils.imports import ImportVar -from reflex_base.vars.number import NumberVar -from reflex_base.vars.sequence import StringVar -from reflex_base.vars.special import use_hook_var, use_id - - -class HookComponent(Component): - """A minimal component for asserting hook var hoisting.""" - - library = "test-lib" - - tag = "HookComponent" - - -def test_use_hook_var_var_data(): - """use_hook_var carries the hook statement and its import in VarData.""" - v = use_hook_var(library="some-lib", hook="useThing") - assert v._var_type is Any - vd = v._get_all_var_data() - assert vd is not None - hook_alias = f"useThing_{v!s}" - assert vd.hooks == (f"const {v!s} = {hook_alias}();",) - assert vd.imports == (("some-lib", (ImportVar(tag="useThing", alias=hook_alias),)),) - - -def test_use_hook_var_guesses_var_type(): - """The returned Var is downcast to the class matching _var_type.""" - count = use_hook_var(library="some-lib", hook="useCount", _var_type=int) - assert isinstance(count, NumberVar) - assert count._var_type is int - - maybe = use_hook_var(library="some-lib", hook="useMaybe", _var_type=int | None) - assert maybe._var_type == (int | None) - - -def test_use_hook_var_names_are_unique(): - """Each call binds the hook value to a fresh variable name.""" - names = {str(use_hook_var(library="some-lib", hook="useThing")) for _ in range(5)} - assert len(names) == 5 - - -def test_same_named_hook_imports_are_aliased(): - """Same-named hooks from different libraries do not collide in JS imports.""" - first = use_hook_var(library="first-lib", hook="useThing") - second = use_hook_var(library="second-lib", hook="useThing") - first_alias = f"useThing_{first!s}" - second_alias = f"useThing_{second!s}" - first_var_data = first._get_all_var_data() - second_var_data = second._get_all_var_data() - - assert first_alias != second_alias - assert first_var_data is not None - assert second_var_data is not None - assert first_var_data.imports == ( - ("first-lib", (ImportVar(tag="useThing", alias=first_alias),)), - ) - assert second_var_data.imports == ( - ("second-lib", (ImportVar(tag="useThing", alias=second_alias),)), - ) - - -def test_use_id(): - """use_id returns a str Var bound to React's useId hook.""" - v = use_id() - assert isinstance(v, StringVar) - assert v._var_type is str - vd = v._get_all_var_data() - assert vd is not None - hook_alias = f"useId_{v!s}" - assert vd.hooks == (f"const {v!s} = {hook_alias}();",) - assert vd.imports == (("react", (ImportVar(tag="useId", alias=hook_alias),)),) - - -def test_hook_var_hoisted_into_component(): - """A component using a hook var renders the hook and pulls its import.""" - v = use_id() - comp = HookComponent.create(id=v) - hook_alias = f"useId_{v!s}" - assert f"const {v!s} = {hook_alias}();" in comp._get_all_hooks() - assert ImportVar(tag="useId", alias=hook_alias) in comp._get_all_imports()["react"] - assert comp.render()["props"] == [f"id:{v!s}"] - - -def test_hook_var_is_scoped_to_each_consuming_component(): - """A hook var must be consumed within one component body.""" - v = use_id() - first = HookComponent.create(id=v) - second = HookComponent.create(id=v) - - assert first._get_all_hooks() == second._get_all_hooks() - assert list(first._get_all_hooks()) == [f"const {v!s} = useId_{v!s}();"] From 5cfa5c2b3baf66b4eb4a24c0040f035f98243965 Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 10 Sep 2026 22:44:09 +0500 Subject: [PATCH 20/21] Restore the hook vars and make rx.el.svg a single render scope Restore `use_hook_var`, `use_id` and `use_chart_width`. A hook var is called once per compiled component, so every element that reads it must render inside the same one. The documented gradient example broke because each child of a plain `rx.el.svg` compiled into its own memoized component and called `useId()` for itself. Mark `Svg` as `MemoizationMode(recursive=False)`, the same snapshot boundary that select, dialog and accordion use. The svg root and its descendants now compile into one component, so `defs` and the elements that reference them share one hook value. The docs example goes back to a plain function, and a compile-level regression test asserts one memo unit and one hook call for two siblings under one svg. Export the Sankey TypedDicts from the package module so that `rx.Var[rx.recharts.SankeyNodeProps]` is a valid type expression. Claude-Session: https://claude.ai/code/session_01EXS4neepFi4cuR4TQoNhTi --- docs/api-reference/var_system.md | 36 ++++++++ docs/library/graphing/charts/sankeychart.md | 12 +-- docs/wrapping-react/custom-code-and-hooks.md | 17 ++++ packages/reflex-base/news/6708.feature.md | 1 + .../src/reflex_base/vars/__init__.py | 3 + .../src/reflex_base/vars/special.py | 64 ++++++++++++++ .../news/6708.bugfix.md | 1 + .../el/elements/media.py | 9 +- .../news/6708.feature.md | 2 +- .../reflex-components-recharts/pyproject.toml | 2 +- .../reflex_components_recharts/__init__.py | 1 + .../src/reflex_components_recharts/general.py | 19 ++++ .../reflex_components_recharts/recharts.py | 6 +- pyi_hashes.json | 6 +- tests/units/compiler/test_memoize_plugin.py | 25 ++++++ .../components/graphing/test_recharts.py | 27 +++--- tests/units/reflex_base/vars/test_special.py | 86 +++++++++++++++++++ 17 files changed, 288 insertions(+), 29 deletions(-) create mode 100644 packages/reflex-base/news/6708.feature.md create mode 100644 packages/reflex-base/src/reflex_base/vars/special.py create mode 100644 packages/reflex-components-core/news/6708.bugfix.md create mode 100644 tests/units/reflex_base/vars/test_special.py diff --git a/docs/api-reference/var_system.md b/docs/api-reference/var_system.md index 469ee8dca83..eb0348391b7 100644 --- a/docs/api-reference/var_system.md +++ b/docs/api-reference/var_system.md @@ -78,3 +78,39 @@ def factorial(value: NumberVar): Use `js_expression` to pass explicit JavaScript expressions; in the `multiply_array_values` example, we pass in a JavaScript expression that calculates the product of all elements in an array called `a` by using the reduce method to multiply each element with the accumulated result, starting from an initial value of 1. Later, we leverage `rx.cond` in the' factorial' function, we instantiate an array using the `range` function, and pass this array to `multiply_array_values`. + +## Hook Vars + +Some values only exist on the frontend and are exposed through React hooks. +`rx.vars.use_hook_var()` binds the return value of a no-argument hook to a unique variable name and returns it as a `Var`. +The hook call and the import of the hook are automatically included in any component that uses the var, so the value reflects the context of the component it is rendered in. + +```py +chart_width = rx.vars.use_hook_var( + library="recharts@3.8.1", + hook="useChartWidth", + _var_type=int | None, +) +``` + +A component using `chart_width` will import `useChartWidth` from `recharts` and render `const = useChartWidth();` in its body, so `chart_width` can be used like any other `Var[int | None]`. + +A hook var is evaluated once per compiled component, so every element that reads it must render inside the same one. An `rx.el.svg` root, an `@rx.memo` body, and a custom renderer body each compile into a single component and satisfy this. + +For the common case of React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` containing a stable unique id for the rendered component. +This is useful for linking SVG elements to `defs` such as gradients or filters: + +```py +def gradient_rect() -> rx.Component: + gradient_id = rx.vars.use_id() + return rx.el.svg( + rx.el.svg.linear_gradient( + rx.el.svg.stop(offset="0%", stop_color="gold"), + rx.el.svg.stop(offset="100%", stop_color="tomato"), + id=gradient_id, + ), + rx.el.svg.rect(fill=f"url(#{gradient_id})", width=64, height=64), + width=64, + height=64, + ) +``` diff --git a/docs/library/graphing/charts/sankeychart.md b/docs/library/graphing/charts/sankeychart.md index b7571a116bd..0ff1ad9134d 100644 --- a/docs/library/graphing/charts/sankeychart.md +++ b/docs/library/graphing/charts/sankeychart.md @@ -116,9 +116,9 @@ properties to construct a custom node or link. Because the component renders inside the SVG element of the chart, you can only use `rx.el.svg` components to construct the custom node or link. -The example below uses the link `index` to give each link's gradient -definition an id that the path references. The index is unique within one -chart, so prefix the id if a page renders several charts with custom links. +The example below also uses `rx.recharts.use_chart_width()` to read the +rendered chart width and `rx.vars.use_id()` to generate a unique id that links +each link's gradient definition to the path that references it. ```python demo graphing styled_sankey_data = { @@ -145,8 +145,8 @@ def sankey_custom_render(): def custom_node( node: rx.Var[rx.recharts.SankeyNodeProps], ) -> rx.Component: - # Outcome nodes sit at the right edge, so their label goes on the left. - is_out = node.payload.to(dict)["type"] == "outcome" + # Determine if the node is at the right edge of the chart to adjust the label position accordingly. + is_out = node.x + node.width + 6 > rx.recharts.use_chart_width() return rx.fragment( rx.el.svg.text( node.payload.name, @@ -172,7 +172,7 @@ def sankey_custom_render(): def custom_link( link: rx.Var[rx.recharts.SankeyLinkProps], ) -> rx.Component: - link_id = f"sankey-link-{link.index}" + link_id = rx.vars.use_id() source = link.payload.source.to(dict) target = link.payload.target.to(dict) return rx.fragment( diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md index c35f908d678..10c0cfc71c4 100644 --- a/docs/wrapping-react/custom-code-and-hooks.md +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -114,3 +114,20 @@ export function Div_7178f430b7b371af8a12d8265d65ab9b() { ```md alert info # You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. ``` + +## Using a Hook's Return Value + +`add_hooks` inserts hook statements into the component, but the values they define are not directly accessible from Python. When you need the return value of a no-argument hook, use `rx.vars.use_hook_var()`, which binds the hook call to a unique variable name and returns it as a `Var`. The hook statement and its import are automatically included in any component where the var is used, so it composes with regular props and var operations. + +```python +import reflex as rx + + +def use_chart_width() -> rx.Var[int | None]: + """Get the width of the enclosing recharts chart as a var.""" + return rx.vars.use_hook_var( + library="recharts@3.8.1", hook="useChartWidth", _var_type=int | None + ) +``` + +For React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` with a stable unique id for the component being rendered, e.g. for linking SVG elements to gradient or filter definitions. diff --git a/packages/reflex-base/news/6708.feature.md b/packages/reflex-base/news/6708.feature.md new file mode 100644 index 00000000000..21e05d1f2f9 --- /dev/null +++ b/packages/reflex-base/news/6708.feature.md @@ -0,0 +1 @@ +Add `rx.vars.use_hook_var()` to create a `Var` bound to the value of a no-argument React hook imported from a given library, and `rx.vars.use_id()` to get React's stable `useId` value for the rendered component. diff --git a/packages/reflex-base/src/reflex_base/vars/__init__.py b/packages/reflex-base/src/reflex_base/vars/__init__.py index c986cf1fd36..2c78f8964df 100644 --- a/packages/reflex-base/src/reflex_base/vars/__init__.py +++ b/packages/reflex-base/src/reflex_base/vars/__init__.py @@ -28,6 +28,7 @@ LiteralStringVar, StringVar, ) +from .special import use_hook_var, use_id __all__ = [ "EMPTY_VAR_INT", @@ -66,6 +67,8 @@ "number", "object", "sequence", + "use_hook_var", + "use_id", "var_operation", "var_operation_return", ] diff --git a/packages/reflex-base/src/reflex_base/vars/special.py b/packages/reflex-base/src/reflex_base/vars/special.py new file mode 100644 index 00000000000..62caf5e389f --- /dev/null +++ b/packages/reflex-base/src/reflex_base/vars/special.py @@ -0,0 +1,64 @@ +"""Special Vars for rendering values from the environment.""" + +from types import UnionType +from typing import Any, TypeVar, cast, overload + +from typing_extensions import TypeForm + +from reflex_base.utils.imports import ImportVar +from reflex_base.utils.types import GenericType +from reflex_base.vars.base import Var, VarData, get_unique_variable_name + +HOOK_VAR_TYPE = TypeVar("HOOK_VAR_TYPE") +_REACT_LIBRARY = "react" +_USE_ID_HOOK = "useId" + + +@overload +def use_hook_var(library: str, hook: str) -> Var[Any]: ... + + +@overload +def use_hook_var( + library: str, hook: str, _var_type: TypeForm[HOOK_VAR_TYPE] +) -> Var[HOOK_VAR_TYPE]: ... + + +@overload +def use_hook_var(library: str, hook: str, _var_type: UnionType) -> Var[Any]: ... + + +def use_hook_var(library: str, hook: str, _var_type: Any = Any) -> Var: + """Get a Var representing a React hook's value. + + The hook is called once in each compiled component that reads the var, so + every element sharing one value must render inside the same component, such + as an ``rx.el.svg`` root, an ``@rx.memo`` body, or a custom renderer body. + + Args: + library: The library to import the hook from. + hook: The name of the hook. + _var_type: The type of the Var. + + Returns: + A Var representing the React hook. + """ + var_name = get_unique_variable_name() + hook_alias = f"{hook}_{var_name}" + return Var( + var_name, + _var_type=cast(GenericType, _var_type), + _var_data=VarData( + imports={library: ImportVar(tag=hook, alias=hook_alias)}, + hooks=(f"const {var_name} = {hook_alias}();",), + ), + ).guess_type() + + +def use_id() -> Var[str]: + """Get the stable React useId hook value for a component. + + Returns: + A Var representing the useId hook value. + """ + return use_hook_var(library=_REACT_LIBRARY, hook=_USE_ID_HOOK, _var_type=str) diff --git a/packages/reflex-components-core/news/6708.bugfix.md b/packages/reflex-components-core/news/6708.bugfix.md new file mode 100644 index 00000000000..5fe178bf4ad --- /dev/null +++ b/packages/reflex-components-core/news/6708.bugfix.md @@ -0,0 +1 @@ +`rx.el.svg` and its children now render as one memoized component, so `defs` such as gradients and the elements that reference them by id always share one render scope. diff --git a/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py index 66b7981ef92..e8e2f964718 100644 --- a/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/media.py @@ -4,6 +4,7 @@ from reflex_base.components.component import Component, ComponentNamespace, field from reflex_base.constants.colors import Color +from reflex_base.constants.compiler import MemoizationMode from reflex_base.vars.base import Var from reflex_components_core.el.elements.inline import ReferrerPolicy @@ -274,9 +275,15 @@ class Source(VoidBaseHTML): class Svg(BaseHTML): - """Display the svg element.""" + """Display the svg element. + + The svg root and its descendants render as a single memoized component, + so ``defs`` and the elements that reference them by id share one render + scope and hook vars such as ``rx.vars.use_id()`` resolve to one value. + """ tag = "svg" + _memoization_mode = MemoizationMode(recursive=False) width: Var[str | int] = field(doc="The width of the svg.") height: Var[str | int] = field(doc="The height of the svg.") xmlns: Var[str] = field(doc="The XML namespace declaration.") diff --git a/packages/reflex-components-recharts/news/6708.feature.md b/packages/reflex-components-recharts/news/6708.feature.md index e9420097751..9627636e7b7 100644 --- a/packages/reflex-components-recharts/news/6708.feature.md +++ b/packages/reflex-components-recharts/news/6708.feature.md @@ -1 +1 @@ -Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers. +Added a Recharts Sankey chart wrapper (`rx.recharts.sankey_chart`) with support for custom node and link renderers, and `rx.recharts.use_chart_width()` for reading the rendered chart width as a `Var`. diff --git a/packages/reflex-components-recharts/pyproject.toml b/packages/reflex-components-recharts/pyproject.toml index 3e3b8848cdb..f7392f8c312 100644 --- a/packages/reflex-components-recharts/pyproject.toml +++ b/packages/reflex-components-recharts/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] maintainers = [{ name = "Khaleel Al-Adhami", email = "khaleel@reflex.dev" }] requires-python = ">=3.10" dependencies = [ - "reflex-base >= 0.9.7", + "reflex-base >= 0.9.7.post31.dev0", "reflex-components-core >= 0.9.0", ] diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py index f31a2604f55..d0f5b8d6162 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/__init__.py @@ -82,6 +82,7 @@ "LabelList", "cell", "Cell", + "use_chart_width", "layer", "Layer", "rectangle", diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py index e2928795091..0332c0f4ef5 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/general.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/general.py @@ -9,8 +9,10 @@ from reflex_base.constants.colors import Color from reflex_base.event import EventHandler, no_args_event_spec from reflex_base.vars.base import LiteralVar, Var +from reflex_base.vars.special import use_hook_var from .recharts import ( + _RECHARTS_LIBRARY, LiteralAnimationEasing, LiteralIconType, LiteralLayout, @@ -20,6 +22,8 @@ Recharts, ) +_USE_CHART_WIDTH_HOOK = "useChartWidth" + class ResponsiveContainer(Recharts, MemoizationLeaf): """A base class for responsive containers in Recharts.""" @@ -300,6 +304,21 @@ class Cell(Recharts): ) +def use_chart_width() -> Var[int | None]: + """Get the chart width as a var. + + Outside of a chart context, this will be None/undefined. + + Returns: + The chart width var. + """ + return use_hook_var( + library=_RECHARTS_LIBRARY, + hook=_USE_CHART_WIDTH_HOOK, + _var_type=int | None, + ) + + class SvgElement(Recharts): """A Recharts component that renders a plain SVG element, which accepts the style prop directly rather than through recharts' wrapperStyle. diff --git a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py index bebf93a8a07..492800031a5 100644 --- a/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py +++ b/packages/reflex-components-recharts/src/reflex_components_recharts/recharts.py @@ -4,11 +4,13 @@ from reflex_base.components.component import Component, MemoizationLeaf, NoSSRComponent +_RECHARTS_LIBRARY = "recharts@3.10.1" + class Recharts(Component): """A component that wraps a recharts lib.""" - library = "recharts@3.10.1" + library = _RECHARTS_LIBRARY def _get_style(self) -> dict: return {"wrapperStyle": self.style} @@ -17,7 +19,7 @@ def _get_style(self) -> dict: class RechartsCharts(NoSSRComponent, MemoizationLeaf): """A component that wraps a recharts lib.""" - library = "recharts@3.10.1" + library = _RECHARTS_LIBRARY LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"] diff --git a/pyi_hashes.json b/pyi_hashes.json index d42513b5895..d736a17b561 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -111,12 +111,12 @@ "packages/reflex-components-react-player/src/reflex_components_react_player/audio.pyi": "39e4144cef066bbff8c27b36a205a54b", "packages/reflex-components-react-player/src/reflex_components_react_player/react_player.pyi": "8aa4c31da45479cab21ed6d597013008", "packages/reflex-components-react-player/src/reflex_components_react_player/video.pyi": "2682dc6e825d25307d8390d01e2bd653", - "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "8ce4313f4287dd0ea39cb35e90cf14bb", + "packages/reflex-components-recharts/src/reflex_components_recharts/__init__.pyi": "3d12b679c0e1d709e7ca4f6937f4b015", "packages/reflex-components-recharts/src/reflex_components_recharts/cartesian.pyi": "3485aaabbfd3ca89908ae19425c7297e", "packages/reflex-components-recharts/src/reflex_components_recharts/charts.pyi": "23a5a3670cb1070f733a2d0c65623733", - "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "45fa9c7a3da5614dfe5bd7e7c62aaa92", + "packages/reflex-components-recharts/src/reflex_components_recharts/general.pyi": "fe22b3cf69e9ae3d425cc88c3511525b", "packages/reflex-components-recharts/src/reflex_components_recharts/polar.pyi": "99ebcfc07868061bdc3c2010d85a153f", - "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "4f6c26f8c76543cc41e2b9dc400ece8a", + "packages/reflex-components-recharts/src/reflex_components_recharts/recharts.pyi": "f2a9cf6db58d169289fbc76de6813300", "packages/reflex-components-sonner/src/reflex_components_sonner/toast.pyi": "f170ac685b6ba5892370166c80684db3", "reflex/__init__.pyi": "a3e1782fab4a9aed55f66cc98af8c217", "reflex/components/__init__.pyi": "9facd05a776d0641432696bbf8e34388", diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index c7b8615423d..48e2c35bde2 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -2620,3 +2620,28 @@ def test_each_memo_wrapper_emits_one_component_module_file() -> None: "for Plain, one for WithProp, and one snapshot wrapper for the " f"LeafComponent boundary. Got: {sorted(ctx.memoize_wrappers)}" ) + + +def test_svg_boundary_shares_hook_var_between_children() -> None: + """Elements under one ``rx.el.svg`` read a hook var from a single hook call.""" + from reflex_base.vars.special import use_id + from reflex_components_core.el.elements.media import LinearGradient, Rect, Svg + + from reflex.compiler.compiler import compile_memo_components + + def page() -> Component: + gradient_id = use_id() + return Svg.create( + LinearGradient.create(id=gradient_id), + Rect.create(fill=f"url(#{gradient_id})"), + ) + + ctx, page_ctx = _compile_single_page(page) + memo_files, _ = compile_memo_components( + memos=tuple(ctx.auto_memo_components.values()) + ) + memo_code = "\n".join(code for _, code in memo_files) + + assert len(ctx.memoize_wrappers) == 1 + assert len(re.findall(r"= useId_\w+\(\);", memo_code)) == 1 + assert not any("useId" in hook for hook in page_ctx.hooks) diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index 753b0aa745c..b870849342d 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -20,7 +20,8 @@ ScatterChart, sankey_chart, ) -from reflex_components_recharts.general import ResponsiveContainer +from reflex_components_recharts.general import ResponsiveContainer, use_chart_width +from reflex_components_recharts.recharts import Recharts import reflex as rx @@ -139,17 +140,13 @@ def custom_node(node: rx.Var[SankeyNodeProps], /) -> rx.Component: sankey_chart.node(custom_node) -def test_sankey_typed_dicts_exported_from_package(): - import reflex_components_recharts as recharts - from reflex_components_recharts import charts - - for name in ( - "SankeyNode", - "SankeyLink", - "SankeyData", - "SankeyNodePayload", - "SankeyNodeProps", - "SankeyLinkPayload", - "SankeyLinkProps", - ): - assert getattr(recharts, name) is getattr(charts, name) +def test_use_chart_width(): + width = use_chart_width() + assert width._var_type == (int | None) + var_data = width._get_all_var_data() + assert var_data is not None + hook_alias = f"useChartWidth_{width!s}" + assert var_data.hooks == (f"const {width!s} = {hook_alias}();",) + assert dict(var_data.imports)[Recharts.library or ""] == ( + rx.ImportVar(tag="useChartWidth", alias=hook_alias), + ) diff --git a/tests/units/reflex_base/vars/test_special.py b/tests/units/reflex_base/vars/test_special.py new file mode 100644 index 00000000000..11c49c1bd2a --- /dev/null +++ b/tests/units/reflex_base/vars/test_special.py @@ -0,0 +1,86 @@ +"""Tests for reflex_base.vars.special hook-backed vars.""" + +from typing import Any + +from reflex_base.components.component import Component +from reflex_base.utils.imports import ImportVar +from reflex_base.vars.number import NumberVar +from reflex_base.vars.sequence import StringVar +from reflex_base.vars.special import use_hook_var, use_id + + +class HookComponent(Component): + """A minimal component for asserting hook var hoisting.""" + + library = "test-lib" + + tag = "HookComponent" + + +def test_use_hook_var_var_data(): + """use_hook_var carries the hook statement and its import in VarData.""" + v = use_hook_var(library="some-lib", hook="useThing") + assert v._var_type is Any + vd = v._get_all_var_data() + assert vd is not None + hook_alias = f"useThing_{v!s}" + assert vd.hooks == (f"const {v!s} = {hook_alias}();",) + assert vd.imports == (("some-lib", (ImportVar(tag="useThing", alias=hook_alias),)),) + + +def test_use_hook_var_guesses_var_type(): + """The returned Var is downcast to the class matching _var_type.""" + count = use_hook_var(library="some-lib", hook="useCount", _var_type=int) + assert isinstance(count, NumberVar) + assert count._var_type is int + + maybe = use_hook_var(library="some-lib", hook="useMaybe", _var_type=int | None) + assert maybe._var_type == (int | None) + + +def test_use_hook_var_names_are_unique(): + """Each call binds the hook value to a fresh variable name.""" + names = {str(use_hook_var(library="some-lib", hook="useThing")) for _ in range(5)} + assert len(names) == 5 + + +def test_same_named_hook_imports_are_aliased(): + """Same-named hooks from different libraries do not collide in JS imports.""" + first = use_hook_var(library="first-lib", hook="useThing") + second = use_hook_var(library="second-lib", hook="useThing") + first_alias = f"useThing_{first!s}" + second_alias = f"useThing_{second!s}" + first_var_data = first._get_all_var_data() + second_var_data = second._get_all_var_data() + + assert first_alias != second_alias + assert first_var_data is not None + assert second_var_data is not None + assert first_var_data.imports == ( + ("first-lib", (ImportVar(tag="useThing", alias=first_alias),)), + ) + assert second_var_data.imports == ( + ("second-lib", (ImportVar(tag="useThing", alias=second_alias),)), + ) + + +def test_use_id(): + """use_id returns a str Var bound to React's useId hook.""" + v = use_id() + assert isinstance(v, StringVar) + assert v._var_type is str + vd = v._get_all_var_data() + assert vd is not None + hook_alias = f"useId_{v!s}" + assert vd.hooks == (f"const {v!s} = {hook_alias}();",) + assert vd.imports == (("react", (ImportVar(tag="useId", alias=hook_alias),)),) + + +def test_hook_var_hoisted_into_component(): + """A component using a hook var renders the hook and pulls its import.""" + v = use_id() + comp = HookComponent.create(id=v) + hook_alias = f"useId_{v!s}" + assert f"const {v!s} = {hook_alias}();" in comp._get_all_hooks() + assert ImportVar(tag="useId", alias=hook_alias) in comp._get_all_imports()["react"] + assert comp.render()["props"] == [f"id:{v!s}"] From e4f46c3b86ef8d1a2ecb4669310c9602c2b801ec Mon Sep 17 00:00:00 2001 From: Farhan Date: Thu, 10 Sep 2026 22:56:44 +0500 Subject: [PATCH 21/21] Cover the package-level Sankey exports and document hook var scoping Claude-Session: https://claude.ai/code/session_01EXS4neepFi4cuR4TQoNhTi --- docs/wrapping-react/custom-code-and-hooks.md | 2 ++ tests/units/components/graphing/test_recharts.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md index 10c0cfc71c4..b75e000ea69 100644 --- a/docs/wrapping-react/custom-code-and-hooks.md +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -131,3 +131,5 @@ def use_chart_width() -> rx.Var[int | None]: ``` For React's built-in [`useId`](https://react.dev/reference/react/useId), `rx.vars.use_id()` returns a `Var[str]` with a stable unique id for the component being rendered, e.g. for linking SVG elements to gradient or filter definitions. + +A hook var is evaluated once per compiled component, so every element that reads it must render inside the same one. An `rx.el.svg` root, an `@rx.memo` body, and a custom renderer body each compile into a single component and satisfy this. diff --git a/tests/units/components/graphing/test_recharts.py b/tests/units/components/graphing/test_recharts.py index b870849342d..c6a9875c82f 100644 --- a/tests/units/components/graphing/test_recharts.py +++ b/tests/units/components/graphing/test_recharts.py @@ -1,10 +1,12 @@ from typing import get_type_hints import pytest +import reflex_components_recharts as recharts from reflex_base.components.component import ( ComponentNamespace, evaluate_style_namespaces, ) +from reflex_components_recharts import charts from reflex_components_recharts.charts import ( AreaChart, BarChart, @@ -150,3 +152,16 @@ def test_use_chart_width(): assert dict(var_data.imports)[Recharts.library or ""] == ( rx.ImportVar(tag="useChartWidth", alias=hook_alias), ) + + +def test_sankey_typed_dicts_exported_from_package(): + for name in ( + "SankeyNode", + "SankeyLink", + "SankeyData", + "SankeyNodePayload", + "SankeyNodeProps", + "SankeyLinkPayload", + "SankeyLinkProps", + ): + assert getattr(recharts, name) is getattr(charts, name)