diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index b2d605890..0db168ba2 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -16,6 +16,7 @@ Unreleased ---------- **Added** + - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPy`` that can be used to run ReactPy in standalone mode via ASGI. - :pull:`1269` - Added ``reactpy.executors.asgi.ReactPyPyodide`` that can be used to run ReactPy in standalone mode via ASGI, but rendered entirely client-sided. - :pull:`1113` - Added ``reactpy.executors.asgi.ReactPyMiddleware`` that can be used to utilize ReactPy within any ASGI compatible framework. @@ -69,6 +70,7 @@ Unreleased **Fixed** - :pull:`1239` - Fixed a bug where script elements would not render to the DOM as plain text. +- :pull:`1271` - Fixed a bug where the ``key`` property provided via server-side ReactPy code was failing to propagate to the front-end JavaScript component. - :pull:`1254` - Fixed a bug where ``RuntimeError("Hook stack is in an invalid state")`` errors would be provided when using a webserver that reuses threads. v1.1.0 diff --git a/src/reactpy/_html.py b/src/reactpy/_html.py index 9f160b403..ffeee7072 100644 --- a/src/reactpy/_html.py +++ b/src/reactpy/_html.py @@ -104,6 +104,7 @@ def _fragment( event_handlers: EventHandlerDict, ) -> VdomDict: """An HTML fragment - this element will not appear in the DOM""" + attributes.pop("key", None) if attributes or event_handlers: msg = "Fragments cannot have attributes besides 'key'" raise TypeError(msg) diff --git a/src/reactpy/core/vdom.py b/src/reactpy/core/vdom.py index 4186ab5a6..6bc28dfd4 100644 --- a/src/reactpy/core/vdom.py +++ b/src/reactpy/core/vdom.py @@ -148,7 +148,7 @@ def __call__( ) -> VdomDict: """The entry point for the VDOM API, for example reactpy.html().""" attributes, children = separate_attributes_and_children(attributes_and_children) - key = attributes.pop("key", None) + key = attributes.get("key", None) attributes, event_handlers = separate_attributes_and_event_handlers(attributes) if REACTPY_CHECK_JSON_ATTRS.current: json.dumps(attributes) diff --git a/src/reactpy/transforms.py b/src/reactpy/transforms.py index 027fb3e3b..cdac48c7e 100644 --- a/src/reactpy/transforms.py +++ b/src/reactpy/transforms.py @@ -103,7 +103,7 @@ def infer_key_from_attributes(vdom: VdomDict) -> None: return # Infer 'key' from 'attributes.key' - key = attributes.pop("key", None) + key = attributes.get("key", None) # Infer 'key' from 'attributes.id' if key is None: diff --git a/tests/test_utils.py b/tests/test_utils.py index 7e334dda5..e494c29b3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -188,7 +188,7 @@ def test_string_to_reactpy(case): # 8: Infer ReactJS `key` from the `key` attribute { "source": '
', - "model": {"tagName": "div", "key": "my-key"}, + "model": {"tagName": "div", "attributes": {"key": "my-key"}, "key": "my-key"}, }, ], ) @@ -253,7 +253,7 @@ def test_non_html_tag_behavior(): "tagName": "my-tag", "attributes": {"data-x": "something"}, "children": [ - {"tagName": "my-other-tag", "key": "a-key"}, + {"tagName": "my-other-tag", "attributes": {"key": "a-key"}, "key": "a-key"}, ], } diff --git a/tests/test_web/js_fixtures/keys-properly-propagated.js b/tests/test_web/js_fixtures/keys-properly-propagated.js new file mode 100644 index 000000000..8d700397e --- /dev/null +++ b/tests/test_web/js_fixtures/keys-properly-propagated.js @@ -0,0 +1,14 @@ +import React from "https://esm.sh/react@19.0" +import ReactDOM from "https://esm.sh/react-dom@19.0/client" +import GridLayout from "https://esm.sh/react-grid-layout@1.5.0"; +export {GridLayout}; + +export function bind(node, config) { + const root = ReactDOM.createRoot(node); + return { + create: (type, props, children) => + React.createElement(type, props, children), + render: (element) => root.render(element, node), + unmount: () => root.unmount() + }; +} diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py index 8cd487c0c..4b5f980c4 100644 --- a/tests/test_web/test_module.py +++ b/tests/test_web/test_module.py @@ -208,6 +208,66 @@ async def test_imported_components_can_render_children(display: DisplayFixture): assert (await child.get_attribute("id")) == f"child-{index + 1}" +async def test_keys_properly_propagated(display: DisplayFixture): + """ + Fix https://github.com/reactive-python/reactpy/issues/1275 + + The `key` property was being lost in its propagation from the server-side ReactPy + definition to the front-end JavaScript. + + This property is required for certain JS components, such as the GridLayout from + react-grid-layout. + """ + module = reactpy.web.module_from_file( + "keys-properly-propagated", JS_FIXTURES_DIR / "keys-properly-propagated.js" + ) + GridLayout = reactpy.web.export(module, "GridLayout") + + await display.show( + lambda: GridLayout({ + "layout": [ + { + "i": "a", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "static": True, + }, + { + "i": "b", + "x": 1, + "y": 0, + "w": 3, + "h": 2, + "minW": 2, + "maxW": 4, + }, + { + "i": "c", + "x": 4, + "y": 0, + "w": 1, + "h": 2, + } + ], + "cols": 12, + "rowHeight": 30, + "width": 1200, + }, + reactpy.html.div({"key": "a"}, "a"), + reactpy.html.div({"key": "b"}, "b"), + reactpy.html.div({"key": "c"}, "c"), + ) + ) + + parent = await display.page.wait_for_selector(".react-grid-layout", state="attached") + children = await parent.query_selector_all("div") + + # The children simply will not render unless they receive the key prop + assert len(children) == 3 + + def test_module_from_string(): reactpy.web.module_from_string("temp", "old") with assert_reactpy_did_log(r"Existing web module .* will be replaced with"):