diff --git a/reacton/core.py b/reacton/core.py index 08b1d01..98f46d5 100644 --- a/reacton/core.py +++ b/reacton/core.py @@ -2827,10 +2827,13 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): else: widget_previous = context.widgets[key] + # effects of child components run while we walk the kwargs, and they may + # change this element's kwargs (like the default renderer, walk a copy) + el_kwargs = el.kwargs.copy() orphan_ids: Set[str] = set() if widget_previous is None: # initial create - kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate) + kwargs = self._visit_children_values(el_kwargs, key, parent_key, self._reconsolidate) if el.is_shared and el in self._shared_widgets: raise RuntimeError(f"Element ({el}) was already in self._shared_widgets") widget = None @@ -2844,13 +2847,13 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): self._store_widget(context, el, key, widget) elif el_prev is not None and el_prev.component == el.component: # update the existing widget in place - kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate) + kwargs = self._visit_children_values(el_kwargs, key, parent_key, self._reconsolidate) if not context.exceptions_children: # the same element whose kwargs hold no elements: nothing can have changed. # With elements (a container), the kwargs resolve to widgets, so compare # them with what the widget holds: equal means setting them is a no-op # (and a value changed from the frontend is still set back) - if el is not el_prev or not (_values_identical(kwargs, el.kwargs) or _widget_holds_values(widget_previous, kwargs)): + if el is not el_prev or not (_values_identical(kwargs, el_kwargs) or _widget_holds_values(widget_previous, kwargs)): try: el._update_widget(widget_previous, el_prev, kwargs) except BaseException as e: @@ -2862,7 +2865,7 @@ def _reconsolidate(self, el: Element, default_key: str, parent_key: str): assert el_prev is not None, "widget_previous is not None, but el_prev is" # a different widget type at the same key: replace self._remove_element(el_prev, key, parent_key=parent_key) - kwargs = self._visit_children_values(el.kwargs, key, parent_key, self._reconsolidate) + kwargs = self._visit_children_values(el_kwargs, key, parent_key, self._reconsolidate) widget = None if not context.exceptions_children: try: diff --git a/reacton/core_test.py b/reacton/core_test.py index e988750..9f0c835 100644 --- a/reacton/core_test.py +++ b/reacton/core_test.py @@ -3973,3 +3973,33 @@ def click_during_render(): assert isinstance(box.children[0], widgets.HTML) assert "click boom" in box.children[0].value rc.close() + + +def test_child_effect_changes_parent_element_kwargs(): + # the effect runs while the parent's kwargs are walked; changing them (adding a key) + # must not break that walk + set_count = lambda x: None # noqa + holder: dict = {} + + @react.component + def Child(): + def effect(): + holder["box"].kwargs.update(box_style="success") + + use_effect(effect) + return w.Label(value="child") + + @react.component + def App(): + nonlocal set_count + count, set_count = react.use_state(0) + holder["box"] = w.VBox(children=[Child(), w.Label(value=str(count))]) + return holder["box"] + + box, rc = react.render(App(), handle_error=False) + vbox = box.children[0] + assert vbox.children[0].value == "child" + set_count(1) + assert box.children[0] is vbox + assert vbox.children[1].value == "1" + rc.close()