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"}, diff --git a/reflex/components/component.py b/reflex/components/component.py index a1f4bc6ee5..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,14 +1653,13 @@ def compile_from(cls, component: BaseComponent) -> BaseComponent: Returns: The memoized component tree. """ - 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): - 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", ]