diff --git a/reacton/core.py b/reacton/core.py index 08b1d01..4937bea 100644 --- a/reacton/core.py +++ b/reacton/core.py @@ -2505,6 +2505,12 @@ def _set_rerender_needed(self, reason: str): self._rerender_needed_reasons.append(RerenderReason(reason=reason)) self._rerender_needed = True + def _has_effect_work(self, context: "ComponentContext") -> bool: + for effect in context.effects: + if not effect.executed or effect.next is not None: + return True + return False + def _render(self, element: Element, default_key: str, parent_key: str): if not isinstance(element, Element): raise TypeError(f"Expected element, not {element}") @@ -2572,6 +2578,7 @@ def _render(self, element: Element, default_key: str, parent_key: str): and context.children.get(key) is context_previous and not context_previous.needs_render and not context_previous.needs_render_descendant + and not self._has_effect_work(context_previous) and not context_previous.exceptions_self and not context_previous.exceptions_children and context_previous.root_element is not None diff --git a/reacton/core_test.py b/reacton/core_test.py index e988750..fb4b9cb 100644 --- a/reacton/core_test.py +++ b/reacton/core_test.py @@ -3973,3 +3973,118 @@ def click_during_render(): assert isinstance(box.children[0], widgets.HTML) assert "click boom" in box.children[0].value rc.close() + + +def test_failed_root_effect_is_retried_on_later_tree_walk(): + log = [] + fail = [False] + set_value = lambda x: None # noqa + + @react.component + def App(): + nonlocal set_value + value, set_value = react.use_state(0) + + def effect(): + log.append(value) + if fail[0]: + raise RuntimeError("effect boom") + + react.use_effect(effect, [value]) + return w.Label(value=f"value {value}") + + box, rc = react.render(App(), handle_error=False) + try: + assert log == [0] + + fail[0] = True + with pytest.raises(RuntimeError, match="effect boom"): + set_value(1) + assert log == [0, 1] + + fail[0] = False + rc.render(rc.element) + assert log == [0, 1, 1] + assert box.children[0].value == "value 1" + finally: + rc.close() + + +def test_failed_effect_in_memoized_child_is_retried_on_later_tree_walk(): + log = [] + fail = [False] + set_child_value = lambda x: None # noqa + + @react.component + def Child(): + nonlocal set_child_value + value, set_child_value = react.use_state(0) + + def effect(): + log.append(value) + if fail[0]: + raise RuntimeError("child effect boom") + + react.use_effect(effect, [value]) + return w.Label(value=f"child {value}") + + @react.component + def App(): + child = react.use_memo(lambda: Child(), []) + return w.VBox(children=[child]) + + box, rc = react.render(App(), handle_error=False) + try: + assert log == [0] + + fail[0] = True + with pytest.raises(RuntimeError, match="child effect boom"): + set_child_value(1) + assert log == [0, 1] + + fail[0] = False + rc.render(rc.element) + assert log == [0, 1, 1] + assert box.children[0].children[0].value == "child 1" + finally: + rc.close() + + +def test_effect_cleanup_exception_does_not_retry_cleaned_effect(): + log = [] + fail_cleanup = [False] + set_value = lambda x: None # noqa + + @react.component + def App(): + nonlocal set_value + value, set_value = react.use_state(0) + + def effect(): + log.append(f"effect {value}") + + def cleanup(): + log.append(f"cleanup {value}") + if fail_cleanup[0]: + raise RuntimeError("cleanup boom") + + return cleanup + + react.use_effect(effect, [value]) + return w.Label(value=f"value {value}") + + box, rc = react.render(App(), handle_error=False) + try: + assert log == ["effect 0"] + + fail_cleanup[0] = True + with pytest.raises(RuntimeError, match="cleanup boom"): + set_value(1) + assert log == ["effect 0", "cleanup 0", "effect 1"] + + fail_cleanup[0] = False + rc.render(rc.element) + assert log == ["effect 0", "cleanup 0", "effect 1"] + assert box.children[0].value == "value 1" + finally: + rc.close()