From 89fcf7e5711b098143daad648f7a23479a83f12f Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 29 Nov 2023 12:27:06 -0800 Subject: [PATCH 1/3] Do not memoize children of InputGroup The Chakra InputGroup component depends on its children being specific types of components, so a memoized Input_* is not "seen" as the input component. --- reflex/components/component.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index a1f4bc6ee5..f083dea0d9 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -1643,10 +1643,11 @@ def compile_from(cls, component: BaseComponent) -> BaseComponent: Returns: The memoized component tree. """ + from reflex.components.forms.input import InputGroup from reflex.components.layout.foreach import Foreach # Foreach must be memoized as a single component to retain index Var context. - if not isinstance(component, Foreach): + if not isinstance(component, (Foreach, InputGroup)): component.children = [ cls.compile_from(child) for child in component.children ] From ee113120d6a288f357c966e86da819937b61e40e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 29 Nov 2023 15:35:58 -0800 Subject: [PATCH 2/3] test_form_submit: enrich test case with input_group and button_group --- integration/test_form_submit.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/integration/test_form_submit.py b/integration/test_form_submit.py index cc36b5f252..8d36cc01dd 100644 --- a/integration/test_form_submit.py +++ b/integration/test_form_submit.py @@ -71,6 +71,8 @@ def FormSubmitName(): class FormState(rx.State): form_data: dict = {} + val: str = "foo" + options: list[str] = ["option1", "option2"] def form_submit(self, form_data: dict): self.form_data = form_data @@ -96,15 +98,24 @@ def index(): rx.switch(name="bool_input4"), rx.slider(name="slider_input"), rx.range_slider(name="range_input"), - rx.radio_group(["option1", "option2"], name="radio_input"), - rx.select(["option1", "option2"], name="select_input"), + rx.radio_group(FormState.options, name="radio_input"), + rx.select(FormState.options, name="select_input"), rx.text_area(name="text_area_input"), - rx.input( - name="debounce_input", - debounce_timeout=0, - on_change=rx.console_log, + rx.input_group( + rx.input_left_element(rx.icon(tag="chevron_right")), + rx.input( + name="debounce_input", + debounce_timeout=0, + on_change=rx.console_log, + ), + rx.input_right_element(rx.icon(tag="chevron_left")), + ), + rx.button_group( + rx.button("Submit", type_="submit"), + rx.icon_button(FormState.val, icon=rx.icon(tag="add")), + variant="outline", + is_attached=True, ), - rx.button("Submit", type_="submit"), ), on_submit=FormState.form_submit, custom_attrs={"action": "/invalid"}, From 616253948c5c0092d0bc7fd215aa9a63db450e57 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 29 Nov 2023 16:01:41 -0800 Subject: [PATCH 3/3] MemoizationMode: an extensible field of Component To help handle memoization exceptions and special cases as they are discovered, use a field on Component which controls aspecs of the auto-memoization process. For now, only the "recursive" boolean is understood. There is certain to be future categories of exceptions to memoization that will be discovered, so instead of plumbing additional fields through, the MemoizationMode can be extended with defaults and used to handle these special cases in the components that need them, rather than centrally in the Component or StatefulComponent logic. Replace special case recursive logic for Foreach and InputGroup components with this new mechanism. --- reflex/components/component.py | 26 +++++++++++++++++--------- reflex/components/forms/input.py | 4 +++- reflex/components/forms/input.pyi | 2 +- reflex/components/layout/foreach.py | 3 +++ reflex/constants/__init__.py | 4 ++++ reflex/constants/compiler.py | 20 ++++++++++++++++++++ scripts/pyi_generator.py | 1 + 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index f083dea0d9..2af3ac5aa4 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -23,7 +23,14 @@ from reflex.base import Base from reflex.compiler.templates import STATEFUL_COMPONENT from reflex.components.tags import Tag -from reflex.constants import Dirs, EventTriggers, Hooks, Imports, PageNames +from reflex.constants import ( + Dirs, + EventTriggers, + Hooks, + Imports, + MemoizationMode, + PageNames, +) from reflex.event import ( EventChain, EventHandler, @@ -150,6 +157,9 @@ class Component(BaseComponent, ABC): # custom attribute custom_attrs: Dict[str, Union[Var, str]] = {} + # When to memoize this component and its children. + _memoization_mode: MemoizationMode = MemoizationMode() + @classmethod def __init_subclass__(cls, **kwargs): """Set default properties. @@ -1643,15 +1653,13 @@ def compile_from(cls, component: BaseComponent) -> BaseComponent: Returns: The memoized component tree. """ - from reflex.components.forms.input import InputGroup - from reflex.components.layout.foreach import Foreach - - # Foreach must be memoized as a single component to retain index Var context. - if not isinstance(component, (Foreach, InputGroup)): - component.children = [ - cls.compile_from(child) for child in component.children - ] if isinstance(component, Component): + if component._memoization_mode.recursive: + # Recursively memoize stateful children (default). + component.children = [ + cls.compile_from(child) for child in component.children + ] + # Memoize this component if it depends on state. stateful_component = cls.create(component) if stateful_component is not None: return stateful_component diff --git a/reflex/components/forms/input.py b/reflex/components/forms/input.py index 7a2f5e4c8b..17f194daa0 100644 --- a/reflex/components/forms/input.py +++ b/reflex/components/forms/input.py @@ -9,7 +9,7 @@ LiteralButtonSize, LiteralInputVariant, ) -from reflex.constants import EventTriggers +from reflex.constants import EventTriggers, MemoizationMode from reflex.utils import imports from reflex.vars import Var @@ -107,6 +107,8 @@ class InputGroup(ChakraComponent): tag = "InputGroup" + _memoization_mode = MemoizationMode(recursive=False) + class InputLeftAddon(ChakraComponent): """The InputLeftAddon component is a component that is used to add an addon to the left of an input.""" diff --git a/reflex/components/forms/input.pyi b/reflex/components/forms/input.pyi index e9e16e2954..c82deca50b 100644 --- a/reflex/components/forms/input.pyi +++ b/reflex/components/forms/input.pyi @@ -15,7 +15,7 @@ from reflex.components.libs.chakra import ( LiteralButtonSize, LiteralInputVariant, ) -from reflex.constants import EventTriggers +from reflex.constants import EventTriggers, MemoizationMode from reflex.utils import imports from reflex.vars import Var diff --git a/reflex/components/layout/foreach.py b/reflex/components/layout/foreach.py index 0dbd860eab..f469cb7c08 100644 --- a/reflex/components/layout/foreach.py +++ b/reflex/components/layout/foreach.py @@ -8,12 +8,15 @@ from reflex.components.component import Component from reflex.components.layout.fragment import Fragment from reflex.components.tags import IterTag +from reflex.constants import MemoizationMode from reflex.vars import Var class Foreach(Component): """A component that takes in an iterable and a render function and renders a list of components.""" + _memoization_mode = MemoizationMode(recursive=False) + # The iterable to create components from. iterable: Var[Iterable] diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index 45aaa72488..175bd91a67 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -25,6 +25,8 @@ Ext, Hooks, Imports, + MemoizationDisposition, + MemoizationMode, PageNames, ) from .config import ( @@ -75,6 +77,8 @@ IS_WINDOWS, LOCAL_STORAGE, LogLevel, + MemoizationDisposition, + MemoizationMode, Next, Node, NOCOMPILE_FILE, diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index b1155ad58b..91e663837d 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,7 +1,9 @@ """Compiler variables.""" +import enum from enum import Enum from types import SimpleNamespace +from reflex.base import Base from reflex.constants import Dirs from reflex.utils.imports import ImportVar @@ -104,3 +106,21 @@ class Hooks(SimpleNamespace): """Common sets of hook declarations.""" EVENTS = f"const [{CompileVars.ADD_EVENTS}, {CompileVars.CONNECT_ERROR}] = useContext(EventLoopContext);" + + +class MemoizationDisposition(enum.Enum): + """The conditions under which a component should be memoized.""" + + # If the component uses state or events, it should be memoized. + STATEFUL = "stateful" + # TODO: add more modes, like always and never + + +class MemoizationMode(Base): + """The mode for memoizing a Component.""" + + # The conditions under which the component should be memoized. + disposition: MemoizationDisposition = MemoizationDisposition.STATEFUL + + # Whether children of this component should be memoized first. + recursive: bool = True diff --git a/scripts/pyi_generator.py b/scripts/pyi_generator.py index 37a98988f0..751945da5f 100644 --- a/scripts/pyi_generator.py +++ b/scripts/pyi_generator.py @@ -44,6 +44,7 @@ "is_default", "special_props", "_invalid_children", + "_memoization_mode", "_valid_children", ]