Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions reacton/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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
Expand Down
115 changes: 115 additions & 0 deletions reacton/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading