From 9300edf1ecfb830e739be754620da6a591e1134b Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 12 Sep 2026 04:01:29 +0500 Subject: [PATCH 1/5] perf(compile): read only set props and cache literal Var dispatch Component render, Var collection, and the prop-component scan walked every declared prop through the field descriptor to find the few that are set. Iterate the instance dict plus class-level defaults instead. Cache the literal Var class per exact value type, short-circuit app-wrap dedupe on identity, skip the generic tag protocol for plain tags, and hoist the memoize plugin's component imports. Docs site dry compile (511 pages): 47 s to 40 s. Claude-Session: https://claude.ai/code/session_01PmizE1eQhtYZyVs1RK2ke3 --- .../+compile-prop-hot-paths.performance.md | 1 + .../src/reflex_base/components/component.py | 58 +++++++-- .../src/reflex_base/components/tags/tag.py | 3 + .../reflex-base/src/reflex_base/vars/base.py | 52 ++++++-- reflex/compiler/plugins/memoize.py | 7 +- tests/units/components/test_component.py | 49 ++++++++ tests/units/components/test_tag.py | 26 ++++ tests/units/reflex_base/vars/test_base.py | 116 ++++++++++++++++++ 8 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 packages/reflex-base/news/+compile-prop-hot-paths.performance.md diff --git a/packages/reflex-base/news/+compile-prop-hot-paths.performance.md b/packages/reflex-base/news/+compile-prop-hot-paths.performance.md new file mode 100644 index 00000000000..8342a4a07b8 --- /dev/null +++ b/packages/reflex-base/news/+compile-prop-hot-paths.performance.md @@ -0,0 +1 @@ +Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index adf8d36fd0e..8cf95482f9c 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -24,6 +24,7 @@ from reflex_base.components.dynamic import load_dynamic_serializer from reflex_base.components.field import BaseField, FieldBasedMeta from reflex_base.components.tags import Tag +from reflex_base.components.tags.tag import render_prop from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode from reflex_base.constants.compiler import SpecialAttributes from reflex_base.event import ( @@ -1161,7 +1162,7 @@ def _render(self, props: dict[str, Any] | None = None) -> Tag: if props is None: # Add component props to the tag. props = { - attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props() + prop.removesuffix("_"): value for prop, value in self._iter_set_props() } # Add ref to element if `ref` is None and `id` is not None. @@ -1201,6 +1202,39 @@ def get_props(cls) -> Iterable[str]: """ return cls.get_js_fields() + @classmethod + @functools.cache + def _get_defaulted_props(cls) -> frozenset[str]: + """Get the props whose field supplies a value when unset. + + Returns: + The props with a default other than ``None`` or a default factory. + """ + return frozenset( + prop + for prop, field_ in cls.get_js_fields().items() + if field_.default_factory is not None + or (field_.default is not MISSING and field_.default is not None) + ) + + def _iter_set_props(self) -> Iterator[tuple[str, Any]]: + """Walk the props that carry a value, in declaration order. + + An unset prop resolves to ``None`` through its field descriptor and + every consumer drops ``None``, so only props present on the instance + or backed by a class default are read. + + Yields: + Each prop name with its value. + """ + values = self.__dict__ + defaulted = self._get_defaulted_props() + for prop in self.get_props(): + if prop in values: + yield prop, values[prop] + elif prop in defaulted: + yield prop, getattr(self, prop) + @classmethod @functools.cache def get_initial_props(cls) -> set[str]: @@ -1215,9 +1249,8 @@ def get_initial_props(cls) -> set[str]: def _get_component_prop_property(self) -> Sequence[BaseComponent]: return [ component - for prop in self.get_props() - if (value := getattr(self, prop)) is not None - and isinstance(value, (BaseComponent, Var)) + for _, value in self._iter_set_props() + if isinstance(value, (BaseComponent, Var)) for component in _components_from(value) ] @@ -1438,11 +1471,15 @@ def render(self) -> dict: except AttributeError: pass tag = self._render() - rendered_dict = dict( - tag.set( - children=[child.render() for child in self.children], - ) - ) + children = [child.render() for child in self.children] + if type(tag) is Tag: + rendered_dict = {} + if (name := render_prop(tag.name)) is not None: + rendered_dict["name"] = name + rendered_dict["props"] = tag.format_props() + rendered_dict["children"] = children + else: + rendered_dict = dict(tag.set(children=children)) self._replace_prop_names(rendered_dict) self._cached_render_result = rendered_dict return rendered_dict @@ -1581,8 +1618,7 @@ def _get_vars( vars.extend(event_vars) # Get Vars associated with component props. - for prop in self.get_props(): - prop_var = getattr(self, prop) + for _, prop_var in self._iter_set_props(): if isinstance(prop_var, Var): vars.append(prop_var) diff --git a/packages/reflex-base/src/reflex_base/components/tags/tag.py b/packages/reflex-base/src/reflex_base/components/tags/tag.py index 6921121c4fa..cc607b90c82 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tag.py @@ -20,6 +20,9 @@ def render_prop(value: Any) -> Any: Returns: The rendered value. """ + if type(value) in (str, dict): + return value + from reflex_base.components.component import BaseComponent if isinstance(value, BaseComponent): diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 79ebe8bd283..ef3de2d9877 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -13,7 +13,6 @@ import logging import re import string -import uuid import warnings from abc import ABCMeta from collections.abc import Callable, Coroutine, Iterable, Mapping, Sequence @@ -115,6 +114,37 @@ class VarSubclassEntry: _var_subclasses: list[VarSubclassEntry] = [] _var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = [] +# Exact value type -> the literal class claiming it, or None when no literal +# class does. Reset whenever a literal subclass registers. +_literal_var_by_type: dict[type, type[LiteralVar] | None] = {} + + +def _literal_var_for(value: Any) -> type[LiteralVar] | None: + """Find the literal Var class claiming ``value``'s type. + + Args: + value: The python value to wrap. + + Returns: + The matching literal class, or None if no registered class claims it. + """ + value_type = type(value) + try: + return _literal_var_by_type[value_type] + except KeyError: + pass + literal_subclass = next( + ( + literal + for literal, var_subclass in reversed(_var_literal_subclasses) + if isinstance(value, var_subclass.python_types) + ), + None, + ) + # A class object's type is its metaclass, which other classes share. + if not isinstance(value, type): + _literal_var_by_type[value_type] = literal_subclass + return literal_subclass @functools.cache @@ -236,7 +266,7 @@ def insert_app_wraps( if seen is None: seen = target.get(key) if seen is not None: - if seen != wrapper: + if seen is not wrapper and seen != wrapper: msg = ( f"Conflicting app wraps for {key!r}: two different " "components claim the same (priority, tag) slot." @@ -1651,6 +1681,7 @@ def __init_subclass__(cls, **kwargs): _var_literal_subclasses.remove(var_literal_subclass) _var_literal_subclasses.append((cls, var_subclass)) + _literal_var_by_type.clear() @classmethod def _create_literal_var( @@ -1678,9 +1709,8 @@ def _create_literal_var( return value return value._replace(merge_var_data=_var_data) - for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: - if isinstance(value, var_subclass.python_types): - return literal_subclass.create(value, _var_data=_var_data) + if (literal_subclass := _literal_var_for(value)) is not None: + return literal_subclass.create(value, _var_data=_var_data) if ( (as_var_method := getattr(value, "_as_var", None)) is not None @@ -1760,9 +1790,8 @@ def _get_all_var_data_without_creating_var_dispatch( if isinstance(value, Var): return value._get_all_var_data() - for literal_subclass, var_subclass in _var_literal_subclasses[::-1]: - if isinstance(value, var_subclass.python_types): - return literal_subclass._get_all_var_data_without_creating_var(value) + if (literal_subclass := _literal_var_for(value)) is not None: + return literal_subclass._get_all_var_data_without_creating_var(value) if ( (as_var_method := getattr(value, "_as_var", None)) is not None @@ -2020,6 +2049,8 @@ def __set_name__(self, owner: Any, name: str): """ if self._attrname is None: self._attrname = name + self._cached_field_name = "_reflex_cache_" + name + cached_field_name = self._cached_field_name original_del = getattr(owner, "__del__", None) @@ -2029,7 +2060,6 @@ def delete_property(this: Any): Args: this: The object to delete the cached property from. """ - cached_field_name = "_reflex_cache_" + name try: unique_id = object.__getattribute__(this, cached_field_name) except AttributeError: @@ -2067,11 +2097,11 @@ def __get__(self, instance: Any, owner: type | None = None): if self._attrname is None: msg = "Cannot use cached_property on a class without __set_name__." raise TypeError(msg) - cached_field_name = "_reflex_cache_" + self._attrname + cached_field_name = self._cached_field_name try: unique_id = object.__getattribute__(instance, cached_field_name) except AttributeError: - unique_id = uuid.uuid4().int + unique_id = object() object.__setattr__(instance, cached_field_name, unique_id) if unique_id not in GLOBAL_CACHE: try: diff --git a/reflex/compiler/plugins/memoize.py b/reflex/compiler/plugins/memoize.py index a50e3e30569..3f71eb1409b 100644 --- a/reflex/compiler/plugins/memoize.py +++ b/reflex/compiler/plugins/memoize.py @@ -35,6 +35,9 @@ from reflex_base.constants.compiler import MemoizationDisposition from reflex_base.plugins import ComponentAndChildren, PageContext from reflex_base.plugins.base import Plugin +from reflex_components_core.base.bare import Bare +from reflex_components_core.core.cond import Cond +from reflex_components_core.core.match import Match from reflex.compiler.plugins.builtin import ( collect_var_app_wraps_for_component, @@ -146,10 +149,6 @@ def _should_memoize(component: Component) -> bool: Returns: True if the component should be wrapped in a memo definition. """ - from reflex_components_core.base.bare import Bare - from reflex_components_core.core.cond import Cond - from reflex_components_core.core.match import Match - strategy = get_memoization_strategy(component) if component._memoization_mode.disposition == MemoizationDisposition.NEVER: diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 687157d9725..592c5591433 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -5,6 +5,7 @@ import pytest from reflex_base.components.component import Component, field +from reflex_base.components.tags import Tag from reflex_base.constants import EventTriggers from reflex_base.constants.state import FIELD_MARKER from reflex_base.event import ( @@ -45,6 +46,33 @@ from reflex.utils import imports +@pytest.mark.parametrize("name", ["div", "", None]) +def test_plain_tag_render_matches_tag_protocol(name, monkeypatch): + """Direct rendering preserves names, props, children, and render caching.""" + tag = Tag(name=name).add_props(title="hello") + component = Component._create(children=[Bare.create("child")]) + monkeypatch.setattr(component, "_render", lambda: tag) + expected = dict(tag.set(children=[child.render() for child in component.children])) + assert component.render() == expected + assert component.render() is component.render() + assert not tag.children + + +def test_custom_tag_render_uses_subclass_protocol(monkeypatch): + """Custom tag iteration can depend on its supplied children.""" + + class ChildrenTag(Tag): + """A tag with custom child-dependent rendering.""" + + def __iter__(self): + """Yield a value derived from the child list.""" + yield "child_count", len(self.children) + + component = Component._create(children=[Bare.create("child")]) + monkeypatch.setattr(component, "_render", lambda: ChildrenTag()) + assert component.render() == {"child_count": 1} + + class TestState(BaseState): """A test state with various methods for event handling.""" @@ -2398,3 +2426,24 @@ def test_get_all_hooks_internal_does_not_mutate_hooks_cache(): assert dict(parent._get_hooks_internal()) == parent_own_hooks # And repeated collection yields the same result. assert parent._get_all_hooks_internal() == combined + + +def test_set_props_iteration_skips_unset_props_and_keeps_defaults(): + """Only set props and class defaults are visited, in declaration order.""" + + class DefaultedProps(Component): + first: Var[str] + second: Var[str] = LiteralVar.create("second-default") + third: Var[str] + + component = DefaultedProps._create(children=(), third="set") + assert [(prop, str(value)) for prop, value in component._iter_set_props()] == [ + ("second", '"second-default"'), + ("third", '"set"'), + ] + assert [str(var) for var in component._get_vars()] == ['"second-default"', '"set"'] + assert {prop: str(value) for prop, value in component._render().props.items()} == { + "second": '"second-default"', + "third": '"set"', + } + assert "first" not in vars(component) diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index f79065d5a02..47a117a8a35 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -1,5 +1,6 @@ import pytest from reflex_base.components.tags import CondTag, Tag, tagless +from reflex_base.components.tags.tag import render_prop from reflex_base.vars.base import LiteralVar, Var @@ -127,3 +128,28 @@ def test_tagless_string_representation(): tag = tagless.Tagless(contents="Hello world") expected_output = "Hello world" assert str(tag) == expected_output + + +def test_render_prop_preserves_plain_values_and_subclass_dispatch(): + """Already-rendered dictionaries pass through; callable subclasses do not.""" + + class CallableString(str): + """A string whose callability must still be inspected.""" + + def __call__(self): + """Return a marker value.""" + return "called" + + class CallableDict(dict): + """A mapping whose callability must still be inspected.""" + + def __call__(self): + """Return a marker value.""" + return "called" + + rendered = {"name": "div", "children": []} + assert render_prop(rendered) is rendered + assert render_prop("text") == "text" + assert render_prop(CallableString("text")) is None + assert render_prop(CallableDict(rendered)) is None + assert render_prop(("text", rendered)) == ["text", rendered] diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 4a2a72a4347..5575b1222c9 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -1,9 +1,12 @@ """Tests for reflex_base.vars.base state metaclass field handling.""" import dataclasses +import gc +import pickle import threading import traceback import typing +import weakref from typing import Any, Literal, TypeVar import pytest @@ -11,11 +14,13 @@ from reflex_base.utils.exceptions import ReflexRuntimeError from reflex_base.utils.types import get_field_type from reflex_base.vars.base import ( + GLOBAL_CACHE, CachedVarOperation, EvenMoreBasicBaseState, LiteralVar, Var, _linearize_bases, + cached_property, cached_property_no_lock, field, ) @@ -302,3 +307,114 @@ def _cached_get_all_var_data(self): BrokenVar(_js_expr="")._get_all_var_data() assert isinstance(exc_info.value.__cause__, AttributeError) assert str(exc_info.value.__cause__) == "the real error message" + + +class _CachedValue: + """A mutable input with an explicitly resettable derived value.""" + + _reflex_cache_result: object + + def __init__(self, value: str): + """Store the input. + + Args: + value: The value to cache. + """ + self.value = value + + @cached_property + def result(self) -> list[str]: + """Return the derived value. + + Returns: + A fresh list containing the input. + """ + return [self.value] + + +def test_cached_property_identity_and_reset(): + """Local keys isolate instances and survive explicit cache resets.""" + first = _CachedValue("first") + second = _CachedValue("second") + result = first.result + assert first.result is result + assert second.result == ["second"] + first.value = "changed" + assert first.result is result + GLOBAL_CACHE.clear() + assert first.result == ["changed"] + assert first.result is not result + + +def test_cached_property_pickle_does_not_reuse_another_instances_key(): + """Deserialized keys must not collide with live cache entries.""" + original = _CachedValue("original") + assert original.result == ["original"] + restored = pickle.loads(pickle.dumps(original)) + restored.value = "restored" + assert restored.result == ["restored"] + assert original.result == ["original"] + + +def test_cached_property_releases_entry_with_instance(): + """Destroying an instance removes its cached value.""" + value = _CachedValue("temporary") + assert value.result == ["temporary"] + key = value._reflex_cache_result + reference = weakref.ref(value) + del value + gc.collect() + assert reference() is None + assert key not in GLOBAL_CACHE + + +def test_literal_var_dispatch_follows_later_registrations(): + """A literal class registered after a lookup wins the next lookup for its type.""" + + class Coordinate: + """A value no literal Var claims yet.""" + + def __init__(self, x: int): + """Store the coordinate. + + Args: + x: The coordinate value. + """ + self.x = x + + from reflex_base.utils import serializers + + @serializers.serializer + def serialize_coordinate(value: Coordinate) -> str: + """Serialize a coordinate. + + Args: + value: The coordinate. + + Returns: + Its string form. + """ + return f"coordinate-{value.x}" + + assert str(LiteralVar.create(Coordinate(1))) == '"coordinate-1"' + + class CoordinateVar(Var[Coordinate], python_types=Coordinate): + """A Var holding a coordinate.""" + + class LiteralCoordinateVar(LiteralVar, CoordinateVar): + """A literal coordinate Var.""" + + @classmethod + def create(cls, value: Coordinate, _var_data=None): + """Create the literal. + + Args: + value: The coordinate. + _var_data: Unused metadata. + + Returns: + A Var with the coordinate's expression. + """ + return Var(_js_expr=f"[{value.x}]", _var_type=Coordinate) + + assert str(LiteralVar.create(Coordinate(2))) == "[2]" From 4715789340a746d5b5ef1f593dd2dae47b35776d Mon Sep 17 00:00:00 2001 From: Farhan Date: Tue, 15 Sep 2026 18:48:20 +0500 Subject: [PATCH 2/5] test(vars): restore registries after the literal dispatch test and add the root news fragment --- news/+compile-prop-hot-paths.performance.md | 1 + tests/units/reflex_base/vars/test_base.py | 64 +++++++++++++-------- 2 files changed, 40 insertions(+), 25 deletions(-) create mode 100644 news/+compile-prop-hot-paths.performance.md diff --git a/news/+compile-prop-hot-paths.performance.md b/news/+compile-prop-hot-paths.performance.md new file mode 100644 index 00000000000..8342a4a07b8 --- /dev/null +++ b/news/+compile-prop-hot-paths.performance.md @@ -0,0 +1 @@ +Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping. diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 5575b1222c9..f2186cd5f9e 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -383,38 +383,52 @@ def __init__(self, x: int): self.x = x from reflex_base.utils import serializers + from reflex_base.vars import base - @serializers.serializer - def serialize_coordinate(value: Coordinate) -> str: - """Serialize a coordinate. + var_subclasses = len(base._var_subclasses) + literal_subclasses = len(base._var_literal_subclasses) + try: - Args: - value: The coordinate. + @serializers.serializer + def serialize_coordinate(value: Coordinate) -> str: + """Serialize a coordinate. - Returns: - Its string form. - """ - return f"coordinate-{value.x}" + Args: + value: The coordinate. + + Returns: + Its string form. + """ + return f"coordinate-{value.x}" - assert str(LiteralVar.create(Coordinate(1))) == '"coordinate-1"' + assert str(LiteralVar.create(Coordinate(1))) == '"coordinate-1"' - class CoordinateVar(Var[Coordinate], python_types=Coordinate): - """A Var holding a coordinate.""" + class CoordinateVar(Var[Coordinate], python_types=Coordinate): + """A Var holding a coordinate.""" - class LiteralCoordinateVar(LiteralVar, CoordinateVar): - """A literal coordinate Var.""" + class LiteralCoordinateVar(LiteralVar, CoordinateVar): + """A literal coordinate Var.""" - @classmethod - def create(cls, value: Coordinate, _var_data=None): - """Create the literal. + @classmethod + def create(cls, value: Coordinate, _var_data=None): + """Create the literal. - Args: - value: The coordinate. - _var_data: Unused metadata. + Args: + value: The coordinate. + _var_data: Unused metadata. - Returns: - A Var with the coordinate's expression. - """ - return Var(_js_expr=f"[{value.x}]", _var_type=Coordinate) + Returns: + A Var with the coordinate's expression. + """ + return Var(_js_expr=f"[{value.x}]", _var_type=Coordinate) - assert str(LiteralVar.create(Coordinate(2))) == "[2]" + assert str(LiteralVar.create(Coordinate(2))) == "[2]" + finally: + serializers.SERIALIZERS.pop(Coordinate) + serializers.SERIALIZER_TYPES.pop(Coordinate) + serializers.get_serializer.cache_clear() + serializers.get_serializer_type.cache_clear() + del base._var_subclasses[var_subclasses:] + del base._var_literal_subclasses[literal_subclasses:] + base._clear_var_subclass_lookup_caches() + base._literal_var_by_type.clear() From 63413b6ad92a8c064b4d3ab77832a220f5b869a1 Mon Sep 17 00:00:00 2001 From: Farhan Date: Tue, 15 Sep 2026 23:46:34 +0500 Subject: [PATCH 3/5] refactor(tags): render tags through Tag.render instead of a type check in Component.render --- .../src/reflex_base/components/component.py | 10 +--------- .../src/reflex_base/components/tags/cond_tag.py | 13 ++++++++++++- .../src/reflex_base/components/tags/iter_tag.py | 15 +++++++++++++-- .../src/reflex_base/components/tags/match_tag.py | 11 +++++++++++ .../src/reflex_base/components/tags/tag.py | 16 ++++++++++++++++ .../src/reflex_base/components/tags/tagless.py | 13 +++++++++++++ tests/units/components/test_component.py | 8 ++++---- tests/units/components/test_tag.py | 15 ++++++++++++++- 8 files changed, 84 insertions(+), 17 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 8cf95482f9c..13223883c86 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -24,7 +24,6 @@ from reflex_base.components.dynamic import load_dynamic_serializer from reflex_base.components.field import BaseField, FieldBasedMeta from reflex_base.components.tags import Tag -from reflex_base.components.tags.tag import render_prop from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode from reflex_base.constants.compiler import SpecialAttributes from reflex_base.event import ( @@ -1472,14 +1471,7 @@ def render(self) -> dict: pass tag = self._render() children = [child.render() for child in self.children] - if type(tag) is Tag: - rendered_dict = {} - if (name := render_prop(tag.name)) is not None: - rendered_dict["name"] = name - rendered_dict["props"] = tag.format_props() - rendered_dict["children"] = children - else: - rendered_dict = dict(tag.set(children=children)) + rendered_dict = tag.render(children) self._replace_prop_names(rendered_dict) self._cached_render_result = rendered_dict return rendered_dict diff --git a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py index 2bdfefe555d..4dd195f2ae0 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py @@ -1,7 +1,7 @@ """Tag to conditionally render components.""" import dataclasses -from collections.abc import Iterator, Mapping +from collections.abc import Iterator, Mapping, Sequence from typing import Any from reflex_base.components.tags.tag import Tag @@ -29,3 +29,14 @@ def __iter__(self) -> Iterator[tuple[str, Any]]: yield ("cond_state", self.cond_state) yield ("true_value", self.true_value) yield ("false_value", self.false_value) + + def render(self, children: Sequence[Any]) -> dict[str, Any]: + """Render the tag into the dictionary consumed by the templates. + + Args: + children: The already rendered children. + + Returns: + The rendered tag dictionary. + """ + return dict(self) diff --git a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py index f5391905ea3..a76f6c240da 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py @@ -4,8 +4,8 @@ import dataclasses import inspect -from collections.abc import Callable, Iterable -from typing import TYPE_CHECKING +from collections.abc import Callable, Iterable, Sequence +from typing import TYPE_CHECKING, Any from reflex_base.components.tags.tag import Tag from reflex_base.utils.types import GenericType @@ -34,6 +34,17 @@ class IterTag(Tag): # The name of the index var. index_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) + def render(self, children: Sequence[Any]) -> dict[str, Any]: + """Render the tag into the dictionary consumed by the templates. + + Args: + children: The already rendered children. + + Returns: + The rendered tag dictionary. + """ + return dict(self.set(children=children)) + def get_iterable_var_type(self) -> GenericType: """Get the type of the iterable var. diff --git a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py index 636492da15c..4068786379f 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py @@ -29,3 +29,14 @@ def __iter__(self) -> Iterator[tuple[str, Any]]: yield ("cond", self.cond) yield ("match_cases", self.match_cases) yield ("default", self.default) + + def render(self, children: Sequence[Any]) -> dict[str, Any]: + """Render the tag into the dictionary consumed by the templates. + + Args: + children: The already rendered children. + + Returns: + The rendered tag dictionary. + """ + return dict(self) diff --git a/packages/reflex-base/src/reflex_base/components/tags/tag.py b/packages/reflex-base/src/reflex_base/components/tags/tag.py index cc607b90c82..0915cef2b94 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tag.py @@ -58,6 +58,22 @@ def format_props(self) -> list[str]: """ return format.format_props(*self.special_props, **self.props) + def render(self, children: Sequence[Any]) -> dict[str, Any]: + """Render the tag into the dictionary consumed by the templates. + + Args: + children: The already rendered children. + + Returns: + The rendered tag dictionary. + """ + rendered: dict[str, Any] = {} + if (name := render_prop(self.name)) is not None: + rendered["name"] = name + rendered["props"] = self.format_props() + rendered["children"] = children + return rendered + def set(self, **kwargs: Any): """Return a new tag with the given fields set. diff --git a/packages/reflex-base/src/reflex_base/components/tags/tagless.py b/packages/reflex-base/src/reflex_base/components/tags/tagless.py index e41ef33f6dc..7779ffad0e5 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tagless.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tagless.py @@ -1,6 +1,8 @@ """A tag with no tag.""" import dataclasses +from collections.abc import Sequence +from typing import Any from reflex_base.components.tags import Tag from reflex_base.utils import format @@ -34,3 +36,14 @@ def __iter__(self): tuple[str, Any]: The field name and value. """ yield "contents", self.contents + + def render(self, children: Sequence[Any]) -> dict[str, Any]: + """Render the tag into the dictionary consumed by the templates. + + Args: + children: The already rendered children. + + Returns: + The rendered tag dictionary. + """ + return dict(self) diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 592c5591433..8936f9da7d9 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -59,14 +59,14 @@ def test_plain_tag_render_matches_tag_protocol(name, monkeypatch): def test_custom_tag_render_uses_subclass_protocol(monkeypatch): - """Custom tag iteration can depend on its supplied children.""" + """A custom tag renders through its own render method.""" class ChildrenTag(Tag): """A tag with custom child-dependent rendering.""" - def __iter__(self): - """Yield a value derived from the child list.""" - yield "child_count", len(self.children) + def render(self, children): + """Return a value derived from the child list.""" + return {"child_count": len(children)} component = Component._create(children=[Bare.create("child")]) monkeypatch.setattr(component, "_render", lambda: ChildrenTag()) diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index 47a117a8a35..10ead8e7eef 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -1,5 +1,5 @@ import pytest -from reflex_base.components.tags import CondTag, Tag, tagless +from reflex_base.components.tags import CondTag, IterTag, MatchTag, Tag, tagless from reflex_base.components.tags.tag import render_prop from reflex_base.vars.base import LiteralVar, Var @@ -153,3 +153,16 @@ def __call__(self): assert render_prop(CallableString("text")) is None assert render_prop(CallableDict(rendered)) is None assert render_prop(("text", rendered)) == ["text", rendered] + + +def test_subclass_render_matches_iteration_protocol(): + """Tag subclasses keep rendering the fields their iteration protocol yields.""" + children = [{"name": "span", "props": [], "children": []}] + cond = CondTag(cond_state="state.flag", true_value={"a": 1}) + match = MatchTag(cond="state.value", match_cases=[], default={"b": 2}) + bare = tagless.Tagless(contents="hello") + for tag in (cond, match, bare): + assert tag.render(children) == dict(tag) + iter_tag = IterTag(name="Foreach", iterable=LiteralVar.create([1, 2])) + assert iter_tag.render(children) == dict(iter_tag.set(children=children)) + assert iter_tag.render(children)["children"] == children From 70a9821a69d2017ee309883fde9c784c8eb09a05 Mon Sep 17 00:00:00 2001 From: Farhan Date: Tue, 15 Sep 2026 23:51:27 +0500 Subject: [PATCH 4/5] refactor(tags): drop unreachable Tag subclass render overrides --- .../src/reflex_base/components/tags/cond_tag.py | 13 +------------ .../src/reflex_base/components/tags/iter_tag.py | 15 ++------------- .../src/reflex_base/components/tags/match_tag.py | 11 ----------- .../src/reflex_base/components/tags/tagless.py | 13 ------------- tests/units/components/test_tag.py | 15 +-------------- 5 files changed, 4 insertions(+), 63 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py index 4dd195f2ae0..2bdfefe555d 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py @@ -1,7 +1,7 @@ """Tag to conditionally render components.""" import dataclasses -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import Iterator, Mapping from typing import Any from reflex_base.components.tags.tag import Tag @@ -29,14 +29,3 @@ def __iter__(self) -> Iterator[tuple[str, Any]]: yield ("cond_state", self.cond_state) yield ("true_value", self.true_value) yield ("false_value", self.false_value) - - def render(self, children: Sequence[Any]) -> dict[str, Any]: - """Render the tag into the dictionary consumed by the templates. - - Args: - children: The already rendered children. - - Returns: - The rendered tag dictionary. - """ - return dict(self) diff --git a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py index a76f6c240da..f5391905ea3 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py @@ -4,8 +4,8 @@ import dataclasses import inspect -from collections.abc import Callable, Iterable, Sequence -from typing import TYPE_CHECKING, Any +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING from reflex_base.components.tags.tag import Tag from reflex_base.utils.types import GenericType @@ -34,17 +34,6 @@ class IterTag(Tag): # The name of the index var. index_var_name: str = dataclasses.field(default_factory=get_unique_variable_name) - def render(self, children: Sequence[Any]) -> dict[str, Any]: - """Render the tag into the dictionary consumed by the templates. - - Args: - children: The already rendered children. - - Returns: - The rendered tag dictionary. - """ - return dict(self.set(children=children)) - def get_iterable_var_type(self) -> GenericType: """Get the type of the iterable var. diff --git a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py index 4068786379f..636492da15c 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py @@ -29,14 +29,3 @@ def __iter__(self) -> Iterator[tuple[str, Any]]: yield ("cond", self.cond) yield ("match_cases", self.match_cases) yield ("default", self.default) - - def render(self, children: Sequence[Any]) -> dict[str, Any]: - """Render the tag into the dictionary consumed by the templates. - - Args: - children: The already rendered children. - - Returns: - The rendered tag dictionary. - """ - return dict(self) diff --git a/packages/reflex-base/src/reflex_base/components/tags/tagless.py b/packages/reflex-base/src/reflex_base/components/tags/tagless.py index 7779ffad0e5..e41ef33f6dc 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tagless.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tagless.py @@ -1,8 +1,6 @@ """A tag with no tag.""" import dataclasses -from collections.abc import Sequence -from typing import Any from reflex_base.components.tags import Tag from reflex_base.utils import format @@ -36,14 +34,3 @@ def __iter__(self): tuple[str, Any]: The field name and value. """ yield "contents", self.contents - - def render(self, children: Sequence[Any]) -> dict[str, Any]: - """Render the tag into the dictionary consumed by the templates. - - Args: - children: The already rendered children. - - Returns: - The rendered tag dictionary. - """ - return dict(self) diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index 10ead8e7eef..47a117a8a35 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -1,5 +1,5 @@ import pytest -from reflex_base.components.tags import CondTag, IterTag, MatchTag, Tag, tagless +from reflex_base.components.tags import CondTag, Tag, tagless from reflex_base.components.tags.tag import render_prop from reflex_base.vars.base import LiteralVar, Var @@ -153,16 +153,3 @@ def __call__(self): assert render_prop(CallableString("text")) is None assert render_prop(CallableDict(rendered)) is None assert render_prop(("text", rendered)) == ["text", rendered] - - -def test_subclass_render_matches_iteration_protocol(): - """Tag subclasses keep rendering the fields their iteration protocol yields.""" - children = [{"name": "span", "props": [], "children": []}] - cond = CondTag(cond_state="state.flag", true_value={"a": 1}) - match = MatchTag(cond="state.value", match_cases=[], default={"b": 2}) - bare = tagless.Tagless(contents="hello") - for tag in (cond, match, bare): - assert tag.render(children) == dict(tag) - iter_tag = IterTag(name="Foreach", iterable=LiteralVar.create([1, 2])) - assert iter_tag.render(children) == dict(iter_tag.set(children=children)) - assert iter_tag.render(children)["children"] == children From 9973b9af12a22c51b93859a415399c0cb3705919 Mon Sep 17 00:00:00 2001 From: Farhan Date: Wed, 16 Sep 2026 00:15:31 +0500 Subject: [PATCH 5/5] refactor(tags): split CommonTag from Tag so every tag renders through one protocol --- .../+compile-prop-hot-paths.performance.md | 2 +- .../src/reflex_base/components/component.py | 4 +- .../reflex_base/components/tags/__init__.py | 4 +- .../reflex_base/components/tags/cond_tag.py | 4 +- .../reflex_base/components/tags/iter_tag.py | 4 +- .../reflex_base/components/tags/match_tag.py | 4 +- .../src/reflex_base/components/tags/tag.py | 48 ++++++++++++++----- .../reflex_base/components/tags/tagless.py | 4 +- .../news/+compile-prop-hot-paths.misc.md | 1 + .../src/reflex_components_core/base/bare.py | 4 +- .../reflex_components_core/core/clipboard.py | 4 +- .../src/reflex_components_core/core/cond.py | 4 +- .../src/reflex_components_core/core/match.py | 4 +- .../el/elements/forms.py | 4 +- .../news/+compile-prop-hot-paths.misc.md | 1 + .../src/reflex_components_gridjs/datatable.py | 4 +- .../news/+compile-prop-hot-paths.misc.md | 1 + .../reflex_components_markdown/markdown.py | 4 +- .../news/+compile-prop-hot-paths.misc.md | 1 + .../primitives/base.py | 4 +- .../reflex_components_radix/themes/base.py | 4 +- tests/units/components/test_component.py | 14 +++--- tests/units/components/test_tag.py | 12 ++++- 23 files changed, 89 insertions(+), 51 deletions(-) create mode 100644 packages/reflex-components-core/news/+compile-prop-hot-paths.misc.md create mode 100644 packages/reflex-components-gridjs/news/+compile-prop-hot-paths.misc.md create mode 100644 packages/reflex-components-markdown/news/+compile-prop-hot-paths.misc.md create mode 100644 packages/reflex-components-radix/news/+compile-prop-hot-paths.misc.md diff --git a/packages/reflex-base/news/+compile-prop-hot-paths.performance.md b/packages/reflex-base/news/+compile-prop-hot-paths.performance.md index 8342a4a07b8..e00998ac907 100644 --- a/packages/reflex-base/news/+compile-prop-hot-paths.performance.md +++ b/packages/reflex-base/news/+compile-prop-hot-paths.performance.md @@ -1 +1 @@ -Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping. +Speed up compilation by reading only the props a component sets, caching literal Var dispatch by value type, and trimming render and app-wrap bookkeeping. Tags now render through `render(children)`: `CommonTag` holds the generic protocol shared by every tag class, and `Tag` overrides it with a direct fast path. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 13223883c86..ee5500ec9bc 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -23,7 +23,7 @@ from reflex_base.breakpoints import Breakpoints from reflex_base.components.dynamic import load_dynamic_serializer from reflex_base.components.field import BaseField, FieldBasedMeta -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag, Tag from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode from reflex_base.constants.compiler import SpecialAttributes from reflex_base.event import ( @@ -1143,7 +1143,7 @@ def _get_tag_name(self) -> str: name = '"' + name + '"' return name - def _render(self, props: dict[str, Any] | None = None) -> Tag: + def _render(self, props: dict[str, Any] | None = None) -> CommonTag: """Define how to render the component in React. Args: diff --git a/packages/reflex-base/src/reflex_base/components/tags/__init__.py b/packages/reflex-base/src/reflex_base/components/tags/__init__.py index c5003ff4aab..96efa62ad4f 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/__init__.py +++ b/packages/reflex-base/src/reflex_base/components/tags/__init__.py @@ -3,6 +3,6 @@ from .cond_tag import CondTag from .iter_tag import IterTag from .match_tag import MatchTag -from .tag import Tag +from .tag import CommonTag, Tag -__all__ = ["CondTag", "IterTag", "MatchTag", "Tag"] +__all__ = ["CommonTag", "CondTag", "IterTag", "MatchTag", "Tag"] diff --git a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py index 2bdfefe555d..8d9c950d5d0 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/cond_tag.py @@ -4,11 +4,11 @@ from collections.abc import Iterator, Mapping from typing import Any -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag @dataclasses.dataclass(frozen=True, kw_only=True) -class CondTag(Tag): +class CondTag(CommonTag): """A conditional tag.""" # The condition to determine which component to render. diff --git a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py index f5391905ea3..99f66ecb006 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/iter_tag.py @@ -7,7 +7,7 @@ from collections.abc import Callable, Iterable from typing import TYPE_CHECKING -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag from reflex_base.utils.types import GenericType from reflex_base.vars import LiteralArrayVar, Var, get_unique_variable_name from reflex_base.vars.sequence import _determine_value_of_array_index @@ -17,7 +17,7 @@ @dataclasses.dataclass(frozen=True) -class IterTag(Tag): +class IterTag(CommonTag): """An iterator tag.""" # The var to iterate over. diff --git a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py index 636492da15c..16a905c8c51 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/match_tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/match_tag.py @@ -4,11 +4,11 @@ from collections.abc import Iterator, Mapping, Sequence from typing import Any -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag @dataclasses.dataclass(frozen=True, kw_only=True) -class MatchTag(Tag): +class MatchTag(CommonTag): """A match tag.""" # The condition to determine which case to match. diff --git a/packages/reflex-base/src/reflex_base/components/tags/tag.py b/packages/reflex-base/src/reflex_base/components/tags/tag.py index 0915cef2b94..4c3748562f5 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tag.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tag.py @@ -6,6 +6,8 @@ from collections.abc import Iterator, Mapping, Sequence from typing import Any +from typing_extensions import Self + from reflex_base.event import EventChain from reflex_base.utils import format from reflex_base.vars.base import LiteralVar, Var @@ -35,8 +37,8 @@ def render_prop(value: Any) -> Any: @dataclasses.dataclass(frozen=True) -class Tag: - """A React tag.""" +class CommonTag: + """The fields and render protocol shared by every tag.""" # The name of the tag. name: str = "" @@ -58,21 +60,19 @@ def format_props(self) -> list[str]: """ return format.format_props(*self.special_props, **self.props) - def render(self, children: Sequence[Any]) -> dict[str, Any]: + def render(self, children: Sequence[Any] | None = None) -> dict[str, Any]: """Render the tag into the dictionary consumed by the templates. Args: - children: The already rendered children. + children: The already rendered children, or None to render the + tag's own children. Returns: The rendered tag dictionary. """ - rendered: dict[str, Any] = {} - if (name := render_prop(self.name)) is not None: - rendered["name"] = name - rendered["props"] = self.format_props() - rendered["children"] = children - return rendered + if children is not None: + return dict(self.set(children=children)) + return dict(self) def set(self, **kwargs: Any): """Return a new tag with the given fields set. @@ -99,7 +99,7 @@ def __iter__(self) -> Iterator[tuple[str, Any]]: if rendered_value is not None: yield field.name, rendered_value - def add_props(self, **kwargs: Any | None) -> Tag: + def add_props(self, **kwargs: Any | None) -> Self: """Return a new tag with the given props added. Args: @@ -124,7 +124,7 @@ def add_props(self, **kwargs: Any | None) -> Tag: }, ) - def remove_props(self, *args: str) -> Tag: + def remove_props(self, *args: str) -> Self: """Return a new tag with the given props removed. Args: @@ -154,3 +154,27 @@ def is_valid_prop(prop: Var | None) -> bool: Whether the prop is valid. """ return prop is not None and not (isinstance(prop, dict) and len(prop) == 0) + + +@dataclasses.dataclass(frozen=True) +class Tag(CommonTag): + """A React tag.""" + + def render(self, children: Sequence[Any] | None = None) -> dict[str, Any]: + """Render the tag without going through the generic field protocol. + + Args: + children: The already rendered children, or None to render the + tag's own children. + + Returns: + The rendered tag dictionary. + """ + rendered: dict[str, Any] = {} + if (name := render_prop(self.name)) is not None: + rendered["name"] = name + rendered["props"] = self.format_props() + rendered["children"] = ( + children if children is not None else render_prop(self.children) + ) + return rendered diff --git a/packages/reflex-base/src/reflex_base/components/tags/tagless.py b/packages/reflex-base/src/reflex_base/components/tags/tagless.py index e41ef33f6dc..8f35fa9c3f2 100644 --- a/packages/reflex-base/src/reflex_base/components/tags/tagless.py +++ b/packages/reflex-base/src/reflex_base/components/tags/tagless.py @@ -2,12 +2,12 @@ import dataclasses -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag from reflex_base.utils import format @dataclasses.dataclass(frozen=True, kw_only=True) -class Tagless(Tag): +class Tagless(CommonTag): """A tag with no tag.""" # The inner contents of the tag. diff --git a/packages/reflex-components-core/news/+compile-prop-hot-paths.misc.md b/packages/reflex-components-core/news/+compile-prop-hot-paths.misc.md new file mode 100644 index 00000000000..0be157843a1 --- /dev/null +++ b/packages/reflex-components-core/news/+compile-prop-hot-paths.misc.md @@ -0,0 +1 @@ +Annotate `_render` overrides as returning `CommonTag`, the new base of every tag class. diff --git a/packages/reflex-components-core/src/reflex_components_core/base/bare.py b/packages/reflex-components-core/src/reflex_components_core/base/bare.py index 191e37564da..058952c124a 100644 --- a/packages/reflex-components-core/src/reflex_components_core/base/bare.py +++ b/packages/reflex-components-core/src/reflex_components_core/base/bare.py @@ -7,7 +7,7 @@ from typing import Any from reflex_base.components.component import BaseComponent, Component, ComponentStyle -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag from reflex_base.components.tags.tagless import Tagless from reflex_base.environment import PerformanceMode, environment from reflex_base.utils.decorator import once @@ -180,7 +180,7 @@ def _get_all_refs(self) -> dict[str, None]: refs |= component._get_all_refs() return refs - def _render(self) -> Tag: + def _render(self) -> CommonTag: contents = ( Var.create(self.contents) if not isinstance(self.contents, Var) diff --git a/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py index b50d325eebb..069dc59202c 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/clipboard.py @@ -5,7 +5,7 @@ from collections.abc import Sequence from reflex_base.components.component import field -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag from reflex_base.constants.compiler import Hooks from reflex_base.event import EventChain, EventHandler, passthrough_event_spec from reflex_base.utils.format import format_prop, wrap @@ -59,7 +59,7 @@ def create(cls, *children, **props): def _exclude_props(self) -> list[str]: return [*super()._exclude_props(), "on_paste", "on_paste_event_actions"] - def _render(self) -> Tag: + def _render(self) -> CommonTag: tag = super()._render() # Ensure a different Fragment component is created whenever targets differ return tag.remove_props("targets").add_props(key=self.targets) diff --git a/packages/reflex-components-core/src/reflex_components_core/core/cond.py b/packages/reflex-components-core/src/reflex_components_core/core/cond.py index b6bed3f308c..c3301ad371a 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/cond.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/cond.py @@ -6,7 +6,7 @@ from reflex_base.components.component import BaseComponent, Component, field from reflex_base.components.memoize_helpers import passthrough_children_var -from reflex_base.components.tags import CondTag, Tag +from reflex_base.components.tags import CommonTag, CondTag from reflex_base.constants import Dirs from reflex_base.style import LIGHT_COLOR_MODE, resolved_color_mode from reflex_base.utils import types @@ -84,7 +84,7 @@ def create( ) ) - def _render(self) -> Tag: + def _render(self) -> CommonTag: true_child, false_child = self._get_cond_children() return CondTag( cond_state=str(self.cond), diff --git a/packages/reflex-components-core/src/reflex_components_core/core/match.py b/packages/reflex-components-core/src/reflex_components_core/core/match.py index 9216cfb767b..8890663b0e6 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/match.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/match.py @@ -5,7 +5,7 @@ from reflex_base.components.component import BaseComponent, Component, field from reflex_base.components.memoize_helpers import passthrough_children_var -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag from reflex_base.components.tags.match_tag import MatchTag from reflex_base.style import Style from reflex_base.utils import format @@ -266,7 +266,7 @@ def _create_match_cond_var_or_component( ), ) - def _render(self) -> Tag: + def _render(self) -> CommonTag: # Reconstruct match_cases and default from self.children, which may have # been updated by the compiler walker to include memoized wrappers. # self.children contains: [case_1_return, case_2_return, ..., default] diff --git a/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py index ccd6f4fef3b..e5ff438641c 100644 --- a/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py +++ b/packages/reflex-components-core/src/reflex_components_core/el/elements/forms.py @@ -9,7 +9,7 @@ from typing import Any, ClassVar, Literal, get_origin, get_type_hints from reflex_base.components.component import BaseComponent, Component, field -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag from reflex_base.constants import Dirs, EventTriggers from reflex_base.event import ( FORM_DATA, @@ -347,7 +347,7 @@ def add_hooks(self) -> list[str]: ) ] - def _render(self) -> Tag: + def _render(self) -> CommonTag: render_tag = super()._render() if EventTriggers.ON_SUBMIT in self.event_triggers: render_tag = render_tag.add_props(**{ diff --git a/packages/reflex-components-gridjs/news/+compile-prop-hot-paths.misc.md b/packages/reflex-components-gridjs/news/+compile-prop-hot-paths.misc.md new file mode 100644 index 00000000000..0be157843a1 --- /dev/null +++ b/packages/reflex-components-gridjs/news/+compile-prop-hot-paths.misc.md @@ -0,0 +1 @@ +Annotate `_render` overrides as returning `CommonTag`, the new base of every tag class. diff --git a/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py b/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py index 91b05cf358f..bd217132a38 100644 --- a/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py +++ b/packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.py @@ -6,7 +6,7 @@ from typing import Any from reflex_base.components.component import NoSSRComponent, field -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag from reflex_base.utils import types from reflex_base.utils.imports import ImportDict from reflex_base.utils.serializers import serialize @@ -105,7 +105,7 @@ def add_imports(self) -> ImportDict: """ return {"": "gridjs/dist/theme/mermaid.css"} - def _render(self) -> Tag: + def _render(self) -> CommonTag: if isinstance(self.data, Var) and types.is_dataframe(self.data._var_type): self.columns = self.data._replace( _js_expr=f"{self.data._js_expr}.columns", diff --git a/packages/reflex-components-markdown/news/+compile-prop-hot-paths.misc.md b/packages/reflex-components-markdown/news/+compile-prop-hot-paths.misc.md new file mode 100644 index 00000000000..0be157843a1 --- /dev/null +++ b/packages/reflex-components-markdown/news/+compile-prop-hot-paths.misc.md @@ -0,0 +1 @@ +Annotate `_render` overrides as returning `CommonTag`, the new base of every tag class. diff --git a/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py index f7ae600d5d6..7ec7360fda2 100644 --- a/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py +++ b/packages/reflex-components-markdown/src/reflex_components_markdown/markdown.py @@ -16,7 +16,7 @@ MemoizationLeaf, field, ) -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag from reflex_base.utils.imports import ImportDict, ImportTypes, ImportVar from reflex_base.vars.base import LiteralVar, Var, VarData from reflex_base.vars.number import ternary_operation @@ -454,7 +454,7 @@ def _get_custom_code(self) -> str | None: }} """ - def _render(self) -> Tag: + def _render(self) -> CommonTag: return ( super() ._render() diff --git a/packages/reflex-components-radix/news/+compile-prop-hot-paths.misc.md b/packages/reflex-components-radix/news/+compile-prop-hot-paths.misc.md new file mode 100644 index 00000000000..0be157843a1 --- /dev/null +++ b/packages/reflex-components-radix/news/+compile-prop-hot-paths.misc.md @@ -0,0 +1 @@ +Annotate `_render` overrides as returning `CommonTag`, the new base of every tag class. diff --git a/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py index 8ec5e8531e6..4c334ed2fdc 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/primitives/base.py @@ -3,7 +3,7 @@ from typing import Any from reflex_base.components.component import Component, field -from reflex_base.components.tags.tag import Tag +from reflex_base.components.tags.tag import CommonTag from reflex_base.utils import format from reflex_base.vars.base import Var @@ -19,7 +19,7 @@ class RadixPrimitiveComponent(Component): class RadixPrimitiveComponentWithClassName(RadixPrimitiveComponent): """Basic component for radix Primitives with a class name prop.""" - def _render(self) -> Tag: + def _render(self) -> CommonTag: return ( super() ._render() diff --git a/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py index 136f77721e8..d7e8c3776ea 100644 --- a/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py +++ b/packages/reflex-components-radix/src/reflex_components_radix/themes/base.py @@ -5,7 +5,7 @@ from typing import Any, ClassVar, Literal from reflex_base.components.component import Component, field -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag from reflex_base.utils.imports import ImportDict, ImportVar from reflex_base.vars.base import Var from reflex_components_core.core.breakpoints import Responsive @@ -238,7 +238,7 @@ def add_imports(self) -> ImportDict | list[ImportDict]: "$/utils/theme": [ImportVar(tag="theme", is_default=True)], } - def _render(self, props: dict[str, Any] | None = None) -> Tag: + def _render(self, props: dict[str, Any] | None = None) -> CommonTag: tag = super()._render(props) return tag.add_props( css=Var( diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index 8936f9da7d9..a54ca2b80c5 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -5,7 +5,7 @@ import pytest from reflex_base.components.component import Component, field -from reflex_base.components.tags import Tag +from reflex_base.components.tags import CommonTag, Tag from reflex_base.constants import EventTriggers from reflex_base.constants.state import FIELD_MARKER from reflex_base.event import ( @@ -59,14 +59,14 @@ def test_plain_tag_render_matches_tag_protocol(name, monkeypatch): def test_custom_tag_render_uses_subclass_protocol(monkeypatch): - """A custom tag renders through its own render method.""" + """A custom tag renders through the generic field protocol.""" - class ChildrenTag(Tag): - """A tag with custom child-dependent rendering.""" + class ChildrenTag(CommonTag): + """A tag whose iteration depends on its child list.""" - def render(self, children): - """Return a value derived from the child list.""" - return {"child_count": len(children)} + def __iter__(self): + """Yield a value derived from the child list.""" + yield "child_count", len(self.children) component = Component._create(children=[Bare.create("child")]) monkeypatch.setattr(component, "_render", lambda: ChildrenTag()) diff --git a/tests/units/components/test_tag.py b/tests/units/components/test_tag.py index 47a117a8a35..07a815848ed 100644 --- a/tests/units/components/test_tag.py +++ b/tests/units/components/test_tag.py @@ -1,5 +1,5 @@ import pytest -from reflex_base.components.tags import CondTag, Tag, tagless +from reflex_base.components.tags import CommonTag, CondTag, Tag, tagless from reflex_base.components.tags.tag import render_prop from reflex_base.vars.base import LiteralVar, Var @@ -153,3 +153,13 @@ def __call__(self): assert render_prop(CallableString("text")) is None assert render_prop(CallableDict(rendered)) is None assert render_prop(("text", rendered)) == ["text", rendered] + + +@pytest.mark.parametrize("name", ["div", "", None]) +def test_tag_render_matches_common_tag_protocol(name): + """The direct Tag render matches the generic CommonTag protocol.""" + tag = Tag(name=name).add_props(title="hello") + children = [{"name": "span", "props": [], "children": []}] + assert tag.render(children) == CommonTag.render(tag, children) + assert tag.render() == CommonTag.render(tag) == dict(tag) + assert not tag.children