Skip to content

Skip rendering None in all situations #1171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 12 additions & 0 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ Unreleased
the overall responsiveness of your app, particularly when handling larger renders
that would otherwise block faster renders from being processed.

**Changed**

- :pull:`1171` - Previously ``None``, when present in an HTML element, would render as
the string ``"None"``. Now ``None`` will not render at all. This is consistent with
how ``None`` is handled when returned from components. It also makes it easier to
conditionally render elements. For example, previously you would have needed to use a
fragment to conditionally render an element (e.g.
``something if condition else html._()``). Now you can write:
``something if condition else None``. The latter now has the minor performance
advantage of not needing to create and render a fragment.


v1.0.2
------

Expand Down
15 changes: 8 additions & 7 deletions src/py/reactpy/reactpy/core/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
EventHandlerDict,
LayoutEventMessage,
LayoutUpdateMessage,
VdomChild,
VdomDict,
VdomJson,
)
Expand Down Expand Up @@ -189,9 +190,7 @@ async def _render_component(
# wrap the model in a fragment (i.e. tagName="") to ensure components have
# a separate node in the model state tree. This could be removed if this
# components are given a node in the tree some other way
wrapper_model: VdomDict = {"tagName": ""}
if raw_model is not None:
wrapper_model["children"] = [raw_model]
wrapper_model: VdomDict = {"tagName": "", "children": [raw_model]}
await self._render_model(exit_stack, old_state, new_state, wrapper_model)
except Exception as error:
logger.exception(f"Failed to render {component}")
Expand Down Expand Up @@ -332,7 +331,7 @@ async def _render_model_children(
child_type_key_tuples = list(_process_child_type_and_key(raw_children))

new_keys = {item[2] for item in child_type_key_tuples}
if len(new_keys) != len(raw_children):
if len(new_keys) != len(child_type_key_tuples):
key_counter = Counter(item[2] for item in child_type_key_tuples)
duplicate_keys = [key for key, count in key_counter.items() if count > 1]
msg = f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
Expand Down Expand Up @@ -423,7 +422,7 @@ async def _render_model_children_without_old_state(
child_type_key_tuples = list(_process_child_type_and_key(raw_children))

new_keys = {item[2] for item in child_type_key_tuples}
if len(new_keys) != len(raw_children):
if len(new_keys) != len(child_type_key_tuples):
key_counter = Counter(item[2] for item in child_type_key_tuples)
duplicate_keys = [key for key, count in key_counter.items() if count > 1]
msg = f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
Expand Down Expand Up @@ -721,10 +720,12 @@ async def get(self) -> _Type:


def _process_child_type_and_key(
children: list[Any],
children: list[VdomChild],
) -> Iterator[tuple[Any, _ElementType, Any]]:
for index, child in enumerate(children):
if isinstance(child, dict):
if child is None:
continue
elif isinstance(child, dict):
child_type = _DICT_TYPE
key = child.get("key")
elif isinstance(child, ComponentType):
Expand Down
11 changes: 2 additions & 9 deletions src/py/reactpy/reactpy/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ async def __aexit__(
VdomAttributes = Mapping[str, Any]
"""Describes the attributes of a :class:`VdomDict`"""

VdomChild: TypeAlias = "ComponentType | VdomDict | str"
VdomChild: TypeAlias = "ComponentType | VdomDict | str | None | Any"
"""A single child element of a :class:`VdomDict`"""

VdomChildren: TypeAlias = "Sequence[VdomChild] | VdomChild"
Expand All @@ -100,14 +100,7 @@ async def __aexit__(

class _VdomDictOptional(TypedDict, total=False):
key: Key | None
children: Sequence[
# recursive types are not allowed yet:
# https://github.com/python/mypy/issues/731
ComponentType
| dict[str, Any]
| str
| Any
]
children: Sequence[ComponentType | VdomChild]
attributes: VdomAttributes
eventHandlers: EventHandlerDict
importSource: ImportSourceDict
Expand Down
51 changes: 42 additions & 9 deletions src/py/reactpy/tests/test_core/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,6 @@ def SimpleComponent():
)


async def test_component_can_return_none():
@reactpy.component
def SomeComponent():
return None

async with reactpy.Layout(SomeComponent()) as layout:
assert (await layout.render())["model"] == {"tagName": ""}


async def test_nested_component_layout():
parent_set_state = reactpy.Ref(None)
child_set_state = reactpy.Ref(None)
Expand Down Expand Up @@ -1310,3 +1301,45 @@ def child_2():

assert child_1_render_count.current == 1
assert child_2_render_count.current == 1


async def test_none_does_not_render():
@component
def Root():
return html.div(None, Child())

@component
def Child():
return None

async with layout_runner(Layout(Root())) as runner:
tree = await runner.render()
assert tree == {
"tagName": "",
"children": [
{"tagName": "div", "children": [{"tagName": "", "children": []}]}
],
}


async def test_conditionally_render_none_does_not_trigger_state_change_in_siblings():
toggle_condition = Ref()
effect_run_count = Ref(0)

@component
def Root():
condition, toggle_condition.current = use_toggle(True)
return html.div("text" if condition else None, Child())

@component
def Child():
@reactpy.use_effect
def effect():
effect_run_count.current += 1

async with layout_runner(Layout(Root())) as runner:
await runner.render()
poll(lambda: effect_run_count.current).until_equals(1)
toggle_condition.current()
await runner.render()
assert effect_run_count.current == 1