Skip to content

Commit 0621716

Browse files
authored
remove should_render (#870)
* remove should render also adds a regression test in the case a component does not render its children * changelog * remove more unused code * better test name * fix style
1 parent b04ec36 commit 0621716

File tree

8 files changed

+95
-309
lines changed

8 files changed

+95
-309
lines changed

docs/source/about/changelog.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,18 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
2323
Unreleased
2424
----------
2525

26-
No changes.
26+
**Removed**
27+
28+
- :pull:`870` - ``ComponentType.should_render()``. This method was implemented based on
29+
reading the React/Preact source code. As it turns out though it seems like it's mostly
30+
a vestige from the fact that both these libraries still support class-based
31+
components. The ability for components to not render also caused several bugs.
32+
33+
**Fixed**
34+
35+
- :issue:`846` - Nested context does no update value if outer context should not render.
36+
- :issue:`847` - Detached model state on render of context consumer if unmounted and
37+
context value does not change.
2738

2839

2940
v0.42.0

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 & 51 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
@@ -225,9 +216,6 @@ def _render_component(
225216
*old_parent_children[index + 1 :],
226217
],
227218
}
228-
finally:
229-
# avoid double render
230-
self._rendering_queue.remove_if_pending(life_cycle_state.id)
231219

232220
def _render_model(
233221
self,
@@ -464,14 +452,6 @@ def __repr__(self) -> str:
464452
return f"{type(self).__name__}({self.root})"
465453

466454

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-
475455
def _new_root_model_state(
476456
component: ComponentType, schedule_render: Callable[[_LifeCycleStateId], None]
477457
) -> _ModelState:
@@ -710,10 +690,6 @@ def put(self, value: _Type) -> None:
710690
self._loop.call_soon_threadsafe(self._queue.put_nowait, value)
711691
return None
712692

713-
def remove_if_pending(self, value: _Type) -> None:
714-
if value in self._pending:
715-
self._pending.remove(value)
716-
717693
async def get(self) -> _Type:
718694
while True:
719695
value = await self._queue.get()

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_hooks.py

Lines changed: 10 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -946,39 +946,6 @@ def test_context_repr():
946946
assert repr(sample_context()) == f"ContextProvider({sample_context})"
947947

948948

949-
async def test_use_context_only_renders_for_value_change():
950-
Context = idom.create_context(None)
951-
952-
provider_hook = HookCatcher()
953-
render_count = idom.Ref(0)
954-
set_state = idom.Ref()
955-
956-
@idom.component
957-
@provider_hook.capture
958-
def ComponentProvidesContext():
959-
state, set_state.current = idom.use_state(0)
960-
return Context(ComponentInContext(), value=state)
961-
962-
@idom.component
963-
def ComponentInContext():
964-
render_count.current += 1
965-
return html.div()
966-
967-
async with idom.Layout(ComponentProvidesContext()) as layout:
968-
await layout.render()
969-
assert render_count.current == 1
970-
971-
set_state.current(1)
972-
973-
await layout.render()
974-
assert render_count.current == 2
975-
976-
provider_hook.latest.schedule_render()
977-
978-
await layout.render()
979-
assert render_count.current == 2
980-
981-
982949
async def test_use_context_updates_components_even_if_memoized():
983950
Context = idom.create_context(None)
984951

@@ -1019,114 +986,26 @@ def MemoizedComponentUsesContext():
1019986
assert value.current == 2
1020987

1021988

1022-
async def test_nested_contexts_do_not_conflict():
989+
async def test_context_values_are_scoped():
1023990
Context = idom.create_context(None)
1024991

1025-
outer_value = idom.Ref(None)
1026-
inner_value = idom.Ref(None)
1027-
outer_render_count = idom.Ref(0)
1028-
inner_render_count = idom.Ref(0)
1029-
set_outer_value = idom.Ref()
1030-
set_root_value = idom.Ref()
1031-
1032-
@idom.component
1033-
def Root():
1034-
outer_value, set_root_value.current = idom.use_state(-1)
1035-
return Context(Outer(), value=outer_value)
1036-
1037-
@idom.component
1038-
def Outer():
1039-
inner_value, set_outer_value.current = idom.use_state(1)
1040-
outer_value.current = idom.use_context(Context)
1041-
outer_render_count.current += 1
1042-
return Context(Inner(), value=inner_value)
1043-
1044-
@idom.component
1045-
def Inner():
1046-
inner_value.current = idom.use_context(Context)
1047-
inner_render_count.current += 1
1048-
return html.div()
1049-
1050-
async with idom.Layout(Root()) as layout:
1051-
await layout.render()
1052-
assert outer_render_count.current == 1
1053-
assert inner_render_count.current == 1
1054-
assert outer_value.current == -1
1055-
assert inner_value.current == 1
1056-
1057-
set_root_value.current(-2)
1058-
1059-
await layout.render()
1060-
assert outer_render_count.current == 2
1061-
assert inner_render_count.current == 1
1062-
assert outer_value.current == -2
1063-
assert inner_value.current == 1
1064-
1065-
set_outer_value.current(2)
1066-
1067-
await layout.render()
1068-
assert outer_render_count.current == 3
1069-
assert inner_render_count.current == 2
1070-
assert outer_value.current == -2
1071-
assert inner_value.current == 2
1072-
1073-
1074-
async def test_neighboring_contexts_do_not_conflict():
1075-
LeftContext = idom.create_context(None)
1076-
RightContext = idom.create_context(None)
1077-
1078-
set_left = idom.Ref()
1079-
set_right = idom.Ref()
1080-
left_used_value = idom.Ref()
1081-
right_used_value = idom.Ref()
1082-
left_render_count = idom.Ref(0)
1083-
right_render_count = idom.Ref(0)
1084-
1085992
@idom.component
1086-
def Root():
1087-
left_value, set_left.current = idom.use_state(1)
1088-
right_value, set_right.current = idom.use_state(1)
1089-
return idom.html.div(
1090-
LeftContext(Left(), value=left_value),
1091-
RightContext(Right(), value=right_value),
993+
def Parent():
994+
return html._(
995+
Context(Context(Child1(), value=1), value="something-else"),
996+
Context(Child2(), value=2),
1092997
)
1093998

1094999
@idom.component
1095-
def Left():
1096-
left_render_count.current += 1
1097-
left_used_value.current = idom.use_context(LeftContext)
1098-
return idom.html.div()
1000+
def Child1():
1001+
assert idom.use_context(Context) == 1
10991002

11001003
@idom.component
1101-
def Right():
1102-
right_render_count.current += 1
1103-
right_used_value.current = idom.use_context(RightContext)
1104-
return idom.html.div()
1004+
def Child2():
1005+
assert idom.use_context(Context) == 2
11051006

1106-
async with idom.Layout(Root()) as layout:
1007+
async with Layout(Parent()) as layout:
11071008
await layout.render()
1108-
assert left_render_count.current == 1
1109-
assert right_render_count.current == 1
1110-
assert left_used_value.current == 1
1111-
assert right_used_value.current == 1
1112-
1113-
for i in range(2, 5):
1114-
set_left.current(i)
1115-
1116-
await layout.render()
1117-
assert left_render_count.current == i
1118-
assert right_render_count.current == 1
1119-
assert left_used_value.current == i
1120-
assert right_used_value.current == 1
1121-
1122-
for j in range(2, 5):
1123-
set_right.current(j)
1124-
1125-
await layout.render()
1126-
assert left_render_count.current == i
1127-
assert right_render_count.current == j
1128-
assert left_used_value.current == i
1129-
assert right_used_value.current == j
11301009

11311010

11321011
async def test_error_in_effect_cleanup_is_gracefully_handled():
@@ -1357,30 +1236,6 @@ def incr_effect_count():
13571236
assert effect_count.current == 1
13581237

13591238

1360-
@pytest.mark.parametrize("get_value", STRICT_EQUALITY_VALUE_CONSTRUCTORS)
1361-
async def test_use_context_compares_with_strict_equality(get_value):
1362-
hook = HookCatcher()
1363-
context = idom.create_context(None)
1364-
inner_render_count = idom.Ref(0)
1365-
1366-
@idom.component
1367-
@hook.capture
1368-
def OuterComponent():
1369-
return context(InnerComponent(), value=get_value())
1370-
1371-
@idom.component
1372-
def InnerComponent():
1373-
idom.use_context(context)
1374-
inner_render_count.current += 1
1375-
1376-
async with idom.Layout(OuterComponent()) as layout:
1377-
await layout.render()
1378-
assert inner_render_count.current == 1
1379-
hook.latest.schedule_render()
1380-
await layout.render()
1381-
assert inner_render_count.current == 1
1382-
1383-
13841239
async def test_use_state_named_tuple():
13851240
state = idom.Ref()
13861241

0 commit comments

Comments
 (0)