Skip to content

Commit 4015997

Browse files
committed
remove should render
also adds a regression test in the case a component does not render its children
1 parent 9760280 commit 4015997

File tree

6 files changed

+65
-147
lines changed

6 files changed

+65
-147
lines changed

src/idom/core/component.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,6 @@ def __init__(
5454
def render(self) -> ComponentType | VdomDict | str | None:
5555
return self.type(*self._args, **self._kwargs)
5656

57-
def should_render(self, new: Component) -> bool:
58-
return True
59-
6057
def __repr__(self) -> str:
6158
try:
6259
args = self._sig.bind(*self._args, **self._kwargs).arguments

src/idom/core/hooks.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -268,13 +268,6 @@ def use_context(context: Context[_Type]) -> _Type:
268268
# then we can safely access the context's default value
269269
return cast(_Type, context.__kwdefaults__["value"])
270270

271-
subscribers = provider._subscribers
272-
273-
@use_effect
274-
def subscribe_to_context_change() -> Callable[[], None]:
275-
subscribers.add(hook)
276-
return lambda: subscribers.remove(hook)
277-
278271
return provider._value
279272

280273

@@ -289,21 +282,12 @@ def __init__(
289282
self.children = children
290283
self.key = key
291284
self.type = type
292-
self._subscribers: set[LifeCycleHook] = set()
293285
self._value = value
294286

295287
def render(self) -> VdomDict:
296288
current_hook().set_context_provider(self)
297289
return vdom("", *self.children)
298290

299-
def should_render(self, new: ContextProvider[_Type]) -> bool:
300-
if not strictly_equal(self._value, new._value):
301-
for hook in self._subscribers:
302-
hook.set_context_provider(new)
303-
hook.schedule_render()
304-
return True
305-
return False
306-
307291
def __repr__(self) -> str:
308292
return f"{type(self).__name__}({self.type})"
309293

src/idom/core/layout.py

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -168,44 +168,35 @@ def _render_component(
168168
component: ComponentType,
169169
) -> None:
170170
life_cycle_state = new_state.life_cycle_state
171+
life_cycle_hook = life_cycle_state.hook
172+
171173
self._model_states_by_life_cycle_state_id[life_cycle_state.id] = new_state
172174

173-
if (
174-
old_state is not None
175-
and hasattr(old_state.model, "current")
176-
and old_state.is_component_state
177-
and not _check_should_render(
178-
old_state.life_cycle_state.component, component
179-
)
180-
):
181-
new_state.model.current = old_state.model.current
182-
else:
183-
life_cycle_hook = life_cycle_state.hook
184-
life_cycle_hook.affect_component_will_render(component)
185-
exit_stack.callback(life_cycle_hook.affect_layout_did_render)
186-
life_cycle_hook.set_current()
187-
try:
188-
raw_model = component.render()
189-
# wrap the model in a fragment (i.e. tagName="") to ensure components have
190-
# a separate node in the model state tree. This could be removed if this
191-
# components are given a node in the tree some other way
192-
wrapper_model: VdomDict = {"tagName": ""}
193-
if raw_model is not None:
194-
wrapper_model["children"] = [raw_model]
195-
self._render_model(exit_stack, old_state, new_state, wrapper_model)
196-
except Exception as error:
197-
logger.exception(f"Failed to render {component}")
198-
new_state.model.current = {
199-
"tagName": "",
200-
"error": (
201-
f"{type(error).__name__}: {error}"
202-
if IDOM_DEBUG_MODE.current
203-
else ""
204-
),
205-
}
206-
finally:
207-
life_cycle_hook.unset_current()
208-
life_cycle_hook.affect_component_did_render()
175+
life_cycle_hook.affect_component_will_render(component)
176+
exit_stack.callback(life_cycle_hook.affect_layout_did_render)
177+
life_cycle_hook.set_current()
178+
try:
179+
raw_model = component.render()
180+
# wrap the model in a fragment (i.e. tagName="") to ensure components have
181+
# a separate node in the model state tree. This could be removed if this
182+
# components are given a node in the tree some other way
183+
wrapper_model: VdomDict = {"tagName": ""}
184+
if raw_model is not None:
185+
wrapper_model["children"] = [raw_model]
186+
self._render_model(exit_stack, old_state, new_state, wrapper_model)
187+
except Exception as error:
188+
logger.exception(f"Failed to render {component}")
189+
new_state.model.current = {
190+
"tagName": "",
191+
"error": (
192+
f"{type(error).__name__}: {error}"
193+
if IDOM_DEBUG_MODE.current
194+
else ""
195+
),
196+
}
197+
finally:
198+
life_cycle_hook.unset_current()
199+
life_cycle_hook.affect_component_did_render()
209200

210201
try:
211202
parent = new_state.parent
@@ -464,14 +455,6 @@ def __repr__(self) -> str:
464455
return f"{type(self).__name__}({self.root})"
465456

466457

467-
def _check_should_render(old: ComponentType, new: ComponentType) -> bool:
468-
try:
469-
return old.should_render(new)
470-
except Exception:
471-
logger.exception(f"{old} component failed to check if {new} should be rendered")
472-
return False
473-
474-
475458
def _new_root_model_state(
476459
component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]
477460
) -> _ModelState:

src/idom/core/types.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,6 @@ class ComponentType(Protocol):
6565
def render(self) -> VdomDict | ComponentType | str | None:
6666
"""Render the component's view model."""
6767

68-
def should_render(self: _OwnType, new: _OwnType) -> bool:
69-
"""Whether the new component instance should be rendered."""
70-
7168

7269
_Render = TypeVar("_Render", covariant=True)
7370
_Event = TypeVar("_Event", contravariant=True)

tests/test_core/test_layout.py

Lines changed: 33 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from idom import html
1212
from idom.config import IDOM_DEBUG_MODE
1313
from idom.core.component import component
14-
from idom.core.hooks import use_effect, use_state
14+
from idom.core.hooks import create_context, use_context, use_effect, use_state
1515
from idom.core.layout import Layout, LayoutEvent, LayoutUpdate
1616
from idom.testing import (
1717
HookCatcher,
@@ -20,7 +20,7 @@
2020
capture_idom_logs,
2121
)
2222
from idom.utils import Ref
23-
from tests.tooling.hooks import use_toggle
23+
from tests.tooling.hooks import use_force_render, use_toggle
2424

2525

2626
@pytest.fixture(autouse=True)
@@ -1123,83 +1123,6 @@ def Child():
11231123
assert did_unmount.current
11241124

11251125

1126-
class ComponentShouldRender:
1127-
def __init__(self, child, should_render):
1128-
self.child = child or html.div()
1129-
self.should_render = should_render
1130-
self.key = None
1131-
self.type = self.__class__
1132-
1133-
def render(self):
1134-
return html.div(self.child)
1135-
1136-
1137-
async def test_component_should_render_always_true():
1138-
render_count = idom.Ref(0)
1139-
root_hook = HookCatcher()
1140-
1141-
@idom.component
1142-
@root_hook.capture
1143-
def Root():
1144-
return ComponentShouldRender(SomeComponent(), should_render=lambda new: True)
1145-
1146-
@idom.component
1147-
def SomeComponent():
1148-
render_count.current += 1
1149-
return html.div()
1150-
1151-
async with idom.Layout(Root()) as layout:
1152-
for _ in range(4):
1153-
await layout.render()
1154-
root_hook.latest.schedule_render()
1155-
1156-
assert render_count.current == 4
1157-
1158-
1159-
async def test_component_should_render_always_false():
1160-
render_count = idom.Ref(0)
1161-
root_hook = HookCatcher()
1162-
1163-
@idom.component
1164-
@root_hook.capture
1165-
def Root():
1166-
return ComponentShouldRender(SomeComponent(), should_render=lambda new: False)
1167-
1168-
@idom.component
1169-
def SomeComponent():
1170-
render_count.current += 1
1171-
return html.div()
1172-
1173-
async with idom.Layout(Root()) as layout:
1174-
for _ in range(4):
1175-
await layout.render()
1176-
root_hook.latest.schedule_render()
1177-
1178-
assert render_count.current == 1
1179-
1180-
1181-
async def test_component_error_in_should_render_is_handled_gracefully():
1182-
root_hook = HookCatcher()
1183-
1184-
@idom.component
1185-
@root_hook.capture
1186-
def Root():
1187-
def bad_should_render(new):
1188-
raise ValueError("The error message")
1189-
1190-
return ComponentShouldRender(html.div(), should_render=bad_should_render)
1191-
1192-
with assert_idom_did_log(
1193-
match_message=r".* component failed to check if .* should be rendered",
1194-
error_type=ValueError,
1195-
match_error="The error message",
1196-
):
1197-
async with idom.Layout(Root()) as layout:
1198-
await layout.render()
1199-
root_hook.latest.schedule_render()
1200-
await layout.render()
1201-
1202-
12031126
async def test_does_render_children_after_component():
12041127
"""Regression test for bug where layout was appending children to a stale ref
12051128
@@ -1221,7 +1144,6 @@ def Child():
12211144

12221145
async with idom.Layout(Parent()) as layout:
12231146
update = await layout.render()
1224-
print(update.new)
12251147
assert update.new == {
12261148
"tagName": "",
12271149
"children": [
@@ -1238,3 +1160,34 @@ def Child():
12381160
}
12391161
],
12401162
}
1163+
1164+
1165+
async def test_remove_context_provider_consumer_without_changing_context_value():
1166+
Context = idom.create_context(None)
1167+
toggle_remove_child = None
1168+
schedule_removed_child_render = None
1169+
1170+
@component
1171+
def Parent():
1172+
nonlocal toggle_remove_child
1173+
remove_child, toggle_remove_child = use_toggle()
1174+
return Context(html.div() if remove_child else Child(), value=None)
1175+
1176+
@component
1177+
def Child():
1178+
nonlocal schedule_removed_child_render
1179+
schedule_removed_child_render = use_force_render()
1180+
return None
1181+
1182+
async with idom.Layout(Parent()) as layout:
1183+
await layout.render()
1184+
1185+
# If the context provider does not render its children then internally tracked
1186+
# state for the removed child component might not be cleaned up propperly
1187+
toggle_remove_child()
1188+
await layout.render()
1189+
1190+
# If this removed child component has state which has not been cleaned up
1191+
# correctly, scheduling a render for it might cause an error.
1192+
schedule_removed_child_render()
1193+
await layout.render()

tests/tooling/hooks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
from idom import use_state
1+
from idom.core.hooks import current_hook, use_state
2+
3+
4+
def use_force_render():
5+
return current_hook().schedule_render
26

37

48
def use_toggle(init=False):

0 commit comments

Comments
 (0)