From 4b4463216ff493dbd8b2c3aa34e7f66d832bc905 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 17 Oct 2026 00:38:43 +0500 Subject: [PATCH 1/3] fix: render hooks in declaration order instead of by HookPosition Hooks are kept in an ordered tuple inside VarData, and every hook's dependencies are merged before it. The INTERNAL / PRE_TRIGGER / POST_TRIGGER buckets reordered that sequence. VarData.merge also could not keep two positions, and _get_added_hooks overwrote a hook's position with None, so a hook could render before a state declaration it reads. _render_hooks now joins the hooks in the order they were declared and merged. VarData drops the position field; the position argument is deprecated and ignored. The framework's own position uses (ref, mount, var and event-loop hooks, window_event_listener, clipboard) are removed. Closes #7584 --- .../news/+hook-declaration-order.bugfix.md | 1 + .../news/+hook-position.deprecation.md | 1 + .../src/reflex_base/compiler/templates.py | 49 ++--------- .../src/reflex_base/components/component.py | 16 +--- .../reflex_base/components/memoize_helpers.py | 1 - .../reflex_base/components/state_context.py | 6 +- .../src/reflex_base/constants/compiler.py | 2 +- .../reflex-base/src/reflex_base/vars/base.py | 36 +++----- .../news/+hook-declaration-order.misc.md | 1 + .../reflex_components_core/core/clipboard.py | 9 +- .../core/window_events.py | 10 +-- tests/units/components/test_component.py | 84 +++++++++++++++++++ tests/units/reflex_base/vars/test_base.py | 15 ++++ 13 files changed, 129 insertions(+), 102 deletions(-) create mode 100644 packages/reflex-base/news/+hook-declaration-order.bugfix.md create mode 100644 packages/reflex-base/news/+hook-position.deprecation.md create mode 100644 packages/reflex-components-core/news/+hook-declaration-order.misc.md diff --git a/packages/reflex-base/news/+hook-declaration-order.bugfix.md b/packages/reflex-base/news/+hook-declaration-order.bugfix.md new file mode 100644 index 00000000000..2191c0efb66 --- /dev/null +++ b/packages/reflex-base/news/+hook-declaration-order.bugfix.md @@ -0,0 +1 @@ +Component hooks render in the order they are declared and merged, so a hook always comes after the hooks it reads. Before, a component whose prop and `add_hooks` Var read the same state could render a hook before the state declaration it reads and fail with a `ReferenceError`. diff --git a/packages/reflex-base/news/+hook-position.deprecation.md b/packages/reflex-base/news/+hook-position.deprecation.md new file mode 100644 index 00000000000..11a4d269c41 --- /dev/null +++ b/packages/reflex-base/news/+hook-position.deprecation.md @@ -0,0 +1 @@ +`VarData(position=...)` and `Hooks.HookPosition` are deprecated and ignored: hooks render in declaration order. Remove the `position` argument; to place a hook after another one, put the one it reads in its `VarData(hooks=...)`. diff --git a/packages/reflex-base/src/reflex_base/compiler/templates.py b/packages/reflex-base/src/reflex_base/compiler/templates.py index 0c65b8b73c8..38072d97b53 100644 --- a/packages/reflex-base/src/reflex_base/compiler/templates.py +++ b/packages/reflex-base/src/reflex_base/compiler/templates.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, Literal from reflex_base import constants -from reflex_base.constants import Hooks from reflex_base.utils import memo_paths from reflex_base.utils.format import json_dumps from reflex_base.vars.base import VarData @@ -18,39 +17,6 @@ from reflex_base.components.component import Component -def _sort_hooks( - hooks: dict[str, VarData | None], -) -> tuple[list[str], list[str], list[str]]: - """Sort the hooks by their position. - - Args: - hooks: The hooks to sort. - - Returns: - The sorted hooks. - """ - internal_hooks = [] - pre_trigger_hooks = [] - post_trigger_hooks = [] - - for hook, data in hooks.items(): - if data and data.position and data.position == Hooks.HookPosition.INTERNAL: - internal_hooks.append(hook) - elif not data or ( - not data.position - or data.position == constants.Hooks.HookPosition.PRE_TRIGGER - ): - pre_trigger_hooks.append(hook) - elif ( - data - and data.position - and data.position == constants.Hooks.HookPosition.POST_TRIGGER - ): - post_trigger_hooks.append(hook) - - return internal_hooks, pre_trigger_hooks, post_trigger_hooks - - class _RenderUtils: @staticmethod def render(component: Mapping[str, Any] | str) -> str: @@ -1191,19 +1157,16 @@ def styles_template(stylesheets: list[str]) -> str: ]) -def _render_hooks(hooks: dict[str, VarData | None], memo: list | None = None) -> str: - """Render hooks for macros. +def _render_hooks(hooks: dict[str, VarData | None]) -> str: + """Render hooks in the order they were declared and merged. + + Every hook's dependencies are declared before it, so declaration order is + the dependency order. Args: hooks: Dictionary of hooks to render. - memo: Optional list of memo hooks. Returns: Rendered hooks code as string. """ - internal, pre_trigger, post_trigger = _sort_hooks(hooks) - internal_str = "\n".join(internal) - pre_trigger_str = "\n".join(pre_trigger) - post_trigger_str = "\n".join(post_trigger) - memo_str = "\n".join(memo) if memo is not None else "" - return f"{internal_str}\n{pre_trigger_str}\n{memo_str}\n{post_trigger_str}" + return "\n".join(hooks) diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index e35a870e57a..c9097c51a4d 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -24,7 +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 CommonTag, Tag -from reflex_base.constants import Dirs, EventTriggers, Hooks, Imports, MemoizationMode +from reflex_base.constants import Dirs, EventTriggers, Imports, MemoizationMode from reflex_base.constants.compiler import SpecialAttributes from reflex_base.event import ( EventCallback, @@ -1959,8 +1959,7 @@ def _get_ref_hook(self) -> Var | None: ref = self.get_ref() if ref is not None: return Var( - f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};", - _var_data=VarData(position=Hooks.HookPosition.INTERNAL), + f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};" ) return None @@ -1974,14 +1973,7 @@ def _get_vars_hooks(self) -> dict[str, VarData | None]: for var in self._get_vars(): var_data = var._get_all_var_data() if var_data is not None: - vars_hooks.update( - var_data.hooks - if isinstance(var_data.hooks, dict) - else { - k: VarData(position=Hooks.HookPosition.INTERNAL) - for k in var_data.hooks - } - ) + vars_hooks.update(dict.fromkeys(var_data.hooks)) for component in var_data.components: vars_hooks.update(component._get_all_hooks()) return vars_hooks @@ -2016,7 +2008,7 @@ def _get_hooks_internal(self) -> dict[str, VarData | None]: result = { **self._get_events_hooks(), **{ - str(hook): VarData(position=Hooks.HookPosition.INTERNAL) + str(hook): None for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()] if hook is not None }, diff --git a/packages/reflex-base/src/reflex_base/components/memoize_helpers.py b/packages/reflex-base/src/reflex_base/components/memoize_helpers.py index ba629a36cce..9d40990a63d 100644 --- a/packages/reflex-base/src/reflex_base/components/memoize_helpers.py +++ b/packages/reflex-base/src/reflex_base/components/memoize_helpers.py @@ -175,7 +175,6 @@ def _var_data_key(data: VarData | None) -> tuple | None: data.imports, data.hooks, tuple(id(dep) for dep in data.deps), - data.position, tuple(id(component) for component in data.components), tuple((priority, id(component)) for priority, component in data.app_wraps), ) diff --git a/packages/reflex-base/src/reflex_base/components/state_context.py b/packages/reflex-base/src/reflex_base/components/state_context.py index 04d75fb7d29..ccdeae47e2d 100644 --- a/packages/reflex-base/src/reflex_base/components/state_context.py +++ b/packages/reflex-base/src/reflex_base/components/state_context.py @@ -11,7 +11,6 @@ from reflex_base.components.component import Component from reflex_base.constants import Dirs -from reflex_base.constants.compiler import Hooks from reflex_base.vars.base import VarData @@ -62,7 +61,4 @@ def get_events_hooks_var_data() -> VarData: Returns: A new VarData carrying both providers as app_wraps. """ - return VarData( - position=Hooks.HookPosition.INTERNAL, - app_wraps=get_event_app_wraps(), - ) + return VarData(app_wraps=get_event_app_wraps()) diff --git a/packages/reflex-base/src/reflex_base/constants/compiler.py b/packages/reflex-base/src/reflex_base/constants/compiler.py index aab6a6e6553..48a21df1ed2 100644 --- a/packages/reflex-base/src/reflex_base/constants/compiler.py +++ b/packages/reflex-base/src/reflex_base/constants/compiler.py @@ -189,7 +189,7 @@ class Hooks(SimpleNamespace): ) class HookPosition(enum.Enum): - """The position of the hook in the component.""" + """Deprecated: hooks render in the order they are declared and merged.""" INTERNAL = "internal" PRE_TRIGGER = "pre_trigger" diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 0671d80c917..55b02ef66dc 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -46,7 +46,7 @@ from reflex_base.constants.compiler import Hooks from reflex_base.constants.state import FIELD_MARKER from reflex_base.environment import _on_env_var_set, environment -from reflex_base.utils import exceptions, imports, serializers, types +from reflex_base.utils import console, exceptions, imports, serializers, types from reflex_base.utils.compat import MISSING_TYPE, annotations_from_namespace from reflex_base.utils.decorator import once from reflex_base.utils.exceptions import ( @@ -344,9 +344,6 @@ class VarData: # Dependencies of the var deps: tuple[Var, ...] = dataclasses.field(default_factory=tuple) - # Position of the hook in the component - position: Hooks.HookPosition | None = None - # Components that are part of this var components: tuple[BaseComponent, ...] = dataclasses.field(default_factory=tuple) @@ -378,7 +375,8 @@ def __init__( imports: Imports needed to render this var. hooks: Hooks that need to be present in the component to render this var. deps: Dependencies of the var for useCallback. - position: Position of the hook in the component. + position: Deprecated and ignored: hooks render in the order they + are declared and merged. components: Components that are part of this var. app_wraps: App-level wrapper components this var requires when used. field_dependencies: Every state field this var is built from, @@ -387,6 +385,14 @@ def __init__( single field. Keyword-only in practice: it trails the older parameters so positional callers of those are unaffected. """ + if position is not None: + console.deprecate( + feature_name="VarData(position=...)", + reason="Hooks render in the order they are declared and merged. " + "Remove the position argument.", + deprecation_version="0.10.1", + removal_version="1.0", + ) if isinstance(hooks, str): hooks = [hooks] if not isinstance(hooks, dict): @@ -402,7 +408,6 @@ def __init__( object.__setattr__(self, "imports", immutable_imports) object.__setattr__(self, "hooks", tuple(hooks or {})) object.__setattr__(self, "deps", tuple(deps or [])) - object.__setattr__(self, "position", position or None) object.__setattr__(self, "components", tuple(components or [])) object.__setattr__(self, "app_wraps", tuple(app_wraps or [])) @@ -418,7 +423,6 @@ def __init__( object.__setattr__(self, "imports", merged_var_data.imports) object.__setattr__(self, "hooks", merged_var_data.hooks) object.__setattr__(self, "deps", merged_var_data.deps) - object.__setattr__(self, "position", merged_var_data.position) object.__setattr__(self, "components", merged_var_data.components) object.__setattr__(self, "app_wraps", merged_var_data.app_wraps) @@ -508,21 +512,6 @@ def merge(*all: VarData | None) -> VarData | None: }.values() ) - positions = list( - dict.fromkeys( - var_data.position - for var_data in all_var_datas - if var_data.position is not None - ) - ) - if positions: - if len(positions) > 1: - msg = f"Cannot merge var data with different positions: {positions}" - raise exceptions.ReflexError(msg) - position = positions[0] - else: - position = None - components = tuple( component for var_data in all_var_datas for component in var_data.components ) @@ -536,7 +525,6 @@ def merge(*all: VarData | None) -> VarData | None: imports=imports_, hooks=hooks, deps=deps, - position=position, components=components, app_wraps=tuple( (priority, wrapper) for (priority, _tag), wrapper in app_wraps.items() @@ -554,7 +542,6 @@ def __bool__(self) -> bool: or self.imports or self.hooks or self.deps - or self.position or self.components or self.app_wraps ) @@ -582,7 +569,6 @@ def _identity_key(self) -> tuple: self.imports, self.hooks, tuple(dep._hash_key() for dep in self.deps), - self.position, tuple(id(component) for component in self.components), frozenset( (priority, component.tag or type(component).__name__) diff --git a/packages/reflex-components-core/news/+hook-declaration-order.misc.md b/packages/reflex-components-core/news/+hook-declaration-order.misc.md new file mode 100644 index 00000000000..e105a3e5128 --- /dev/null +++ b/packages/reflex-components-core/news/+hook-declaration-order.misc.md @@ -0,0 +1 @@ +`window_event_listener` and `clipboard` no longer set a hook position. 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 069dc59202c..0d1859d9052 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 @@ -6,12 +6,11 @@ from reflex_base.components.component import field 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 from reflex_base.utils.imports import ImportVar from reflex_base.vars import get_unique_variable_name -from reflex_base.vars.base import Var, VarData +from reflex_base.vars.base import Var from reflex_components_core.base.fragment import Fragment @@ -90,11 +89,7 @@ def add_hooks(self) -> list[str | Var[str]]: hook_expr = f"usePasteHandler({self.targets!s}, {self.on_paste_event_actions!s}, {on_paste!s})" return [ - Var( - hook_expr, - _var_type="str", - _var_data=VarData(position=Hooks.HookPosition.POST_TRIGGER), - ), + Var(hook_expr, _var_type="str"), ] diff --git a/packages/reflex-components-core/src/reflex_components_core/core/window_events.py b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py index ee57bb770c0..20880622e66 100644 --- a/packages/reflex-components-core/src/reflex_components_core/core/window_events.py +++ b/packages/reflex-components-core/src/reflex_components_core/core/window_events.py @@ -5,9 +5,8 @@ from typing import Any, cast from reflex_base.components.component import field -from reflex_base.constants.compiler import Hooks from reflex_base.event import EventHandler, key_event, no_args_event_spec -from reflex_base.vars.base import Var, VarData +from reflex_base.vars.base import Var from reflex_base.vars.object import ObjectVar from reflex_components_core.base.fragment import Fragment @@ -135,12 +134,7 @@ def add_hooks(self) -> list[str | Var[Any]]: }}, []); """ - hooks.append( - Var( - hook_expr, - _var_data=VarData(position=Hooks.HookPosition.POST_TRIGGER), - ) - ) + hooks.append(Var(hook_expr)) return hooks diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index dcba084aa39..c141365e6ed 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -1,9 +1,12 @@ import copy +import random +import re from contextlib import nullcontext from dataclasses import dataclass from typing import Any, ClassVar, TypedDict import pytest +from reflex_base.compiler.templates import _render_hooks from reflex_base.components.component import Component, field from reflex_base.components.tags import CommonTag, Tag from reflex_base.constants import EventTriggers @@ -2137,6 +2140,87 @@ def add_hooks(self): assert ImportVar(tag="useEffect") in imports["react"] +class _HookReader(Component): + """Reads ``value`` through a prop and ``added`` through an ``add_hooks`` Var.""" + + tag = "HookReader" + + value: Var[int] + + added: Var[int] = field(default=Var(_js_expr="0"), is_javascript_property=False) + + def add_hooks(self): + return [Var(f"const read_{self.added!s} = {self.added}")] + + +def _assert_declared_before_use(rendered: str, names: str): + """Assert that every name a hook line reads is declared on an earlier line. + + Args: + rendered: The rendered hooks. + names: A regex matching the declared names to check. + """ + declared = set() + for line in filter(None, (line.strip() for line in rendered.splitlines())): + target = re.match(r"const (\w+) =", line) + # A string literal names the state; only an identifier reads it. + rhs = re.sub(r'"[^"]*"', "", line[target.end() :] if target else line) + assert set(re.findall(names, rhs)) <= declared, line + if target: + declared.add(target.group(1)) + + +def test_hooks_render_after_the_state_hook_they_read(test_state: type[TestState]): + """A prop hook and an added hook that read one state share its declaration.""" + num = Var.create(test_state.num) + derived = Var( + _js_expr="derived", + _var_data=VarData( + hooks={f"const derived = {num!s} + 1": num._get_all_var_data()} + ), + ) + + reader = _HookReader.create(value=derived, added=num) + _assert_declared_before_use( + _render_hooks(reader._get_all_hooks()), + rf"\b{test_state.get_name()}\b|\bderived\b", + ) + + +@pytest.mark.parametrize("seed", range(25)) +def test_hooks_render_after_their_dependencies(seed: int): + """Random hook graphs, read across a random tree, render dependencies first. + + Args: + seed: The seed for the random graph and tree. + """ + rng = random.Random(seed) + hook_vars: list[Var] = [] + for i in range(8): + deps = rng.sample(hook_vars, min(len(hook_vars), rng.randint(0, 3))) + expr = " + ".join(str(dep) for dep in deps) or "0" + hook_vars.append( + Var( + _js_expr=f"h{i}", + _var_data=VarData( + hooks={ + f"const h{i} = {expr}": VarData.merge( + *(dep._get_all_var_data() for dep in deps) + ) + } + ), + ) + ) + + def tree(depth: int) -> Component: + children = [tree(depth - 1) for _ in range(rng.randint(0, 2) if depth else 0)] + return _HookReader.create( + *children, value=rng.choice(hook_vars), added=rng.choice(hook_vars) + ) + + _assert_declared_before_use(_render_hooks(tree(3)._get_all_hooks()), r"\bh\d+\b") + + def test_add_style_embedded_vars(test_state: type[TestState]): """Test that add_style works with embedded vars when returning a plain dict. diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 58517ddaa52..a6e7a6511de 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -354,6 +354,21 @@ def test_var_data_field_name_reports_the_first_field(): assert VarData().field_name == "" +def test_var_data_position_is_deprecated_and_ignored(): + """Hooks of different positions merge, in order, instead of raising.""" + with mock.patch("reflex_base.utils.console.deprecate") as deprecate: + internal = VarData( + hooks=["const a = 1"], position=constants.Hooks.HookPosition.INTERNAL + ) + post = VarData( + hooks=["const b = a"], position=constants.Hooks.HookPosition.POST_TRIGGER + ) + assert deprecate.call_count == 2 + merged = VarData.merge(internal, post) + assert merged is not None + assert merged.hooks == ("const a = 1", "const b = a") + + def test_serializer_attribute_error_is_not_masked() -> None: """An AttributeError raised inside a serializer surfaces chained, with its own frame.""" From 3e63d1f5f4276b43bb25bd367de042c15109dd46 Mon Sep 17 00:00:00 2001 From: Farhan Date: Sat, 17 Oct 2026 00:51:12 +0500 Subject: [PATCH 2/3] test: expect hooks without empty position buckets in AppWrap Document that a hook reading state must be a Var that carries it. --- packages/reflex-base/news/+hook-declaration-order.bugfix.md | 2 +- tests/units/test_app.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/reflex-base/news/+hook-declaration-order.bugfix.md b/packages/reflex-base/news/+hook-declaration-order.bugfix.md index 2191c0efb66..92a15c5cea8 100644 --- a/packages/reflex-base/news/+hook-declaration-order.bugfix.md +++ b/packages/reflex-base/news/+hook-declaration-order.bugfix.md @@ -1 +1 @@ -Component hooks render in the order they are declared and merged, so a hook always comes after the hooks it reads. Before, a component whose prop and `add_hooks` Var read the same state could render a hook before the state declaration it reads and fail with a `ReferenceError`. +Component hooks render in the order they are declared and merged, so a hook always comes after the hooks it reads. Before, a component whose prop and `add_hooks` Var read the same state could render a hook before the state declaration it reads and fail with a `ReferenceError`. A hook that reads state must carry it: return `rx.Var(f"const x = {State.value}")` from `add_hooks`, not a plain string built with `str(State.value)`. diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 6ef3ceef5cf..849e662ad8a 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -2430,7 +2430,7 @@ def test_app_wrap_compile_theme( function_app_definition, "DefaultOverlayComponents" ) expected = ( - "function AppWrap({children}) {\n\n\n\n\n" + "function AppWrap({children}) {\n\n" "return (" + ("jsx(StrictMode,{}," if react_strict_mode else "") + "jsx(StateProvider,{}," @@ -3344,7 +3344,7 @@ def page(): function_app_definition, "DefaultOverlayComponents" ) expected = ( - "function AppWrap({children}) {\n\n\n\n\n" + "function AppWrap({children}) {\n\n" "return (" + ("jsx(StrictMode,{}," if react_strict_mode else "") + "jsx(StateProvider,{}," From fdce21fd09f2fafb5f9ae4d9a3f8958638cd3e8c Mon Sep 17 00:00:00 2001 From: Farhan Date: Fri, 16 Oct 2026 20:46:16 +0000 Subject: [PATCH 3/3] docs: describe declaration-order hooks in the custom hooks guide The guide told wrapper authors to set VarData.position, which is now deprecated and ignored. Document declaration order and VarData(hooks=...) for a hook that reads another hook's declaration instead, and mark the components-core fragment as a bugfix with its user-visible result. Claude-Session: https://claude.ai/code/session_01SyBSxfLKyRDRQrKJqcg3Wh --- docs/wrapping-react/custom-code-and-hooks.md | 83 ++++++++++++------- .../news/+hook-declaration-order.bugfix.md | 1 + .../news/+hook-declaration-order.misc.md | 1 - 3 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 packages/reflex-components-core/news/+hook-declaration-order.bugfix.md delete mode 100644 packages/reflex-components-core/news/+hook-declaration-order.misc.md diff --git a/docs/wrapping-react/custom-code-and-hooks.md b/docs/wrapping-react/custom-code-and-hooks.md index b75e000ea69..a8ac35edb03 100644 --- a/docs/wrapping-react/custom-code-and-hooks.md +++ b/docs/wrapping-react/custom-code-and-hooks.md @@ -34,20 +34,12 @@ console.log(customVariable); Custom hooks are any hooks that need to be included in your component. This can include things like `useEffect`, `useState`, or any other hooks from the library you are wrapping. - Simple hooks can be added as strings. -- More complex hooks that need to have special import or be written in a specific order can be added as `rx.Var` with a `VarData` object to specify the position of the hook. - - The `imports` attribute of the `VarData` object can be used to specify any imports that need to be included in the component. - - The `position` attribute of the `VarData` object can be set to `Hooks.HookPosition.PRE_TRIGGER` or `Hooks.HookPosition.POST_TRIGGER` to specify the position of the hook in the component. +- Hooks that need an import can be added as `rx.Var` with a `VarData` object. The `imports` attribute of the `VarData` object specifies any imports that need to be included in the component. -```md alert info -# The `position` attribute is only used for hooks that need to be written in a specific order. - -- If an event handler need to refer to a variable defined in a hook, the hook should be written before the event handler. -- If a hook need to refer to the memoized event handler by name, the hook should be written after the event handler. -``` +Hooks render in the order they are declared. The hooks the framework manages (refs, `on_mount`, and the state hooks that props read) come first, then the hooks returned by `add_hooks` in the order of the list, with a base class's hooks before a subclass's. ```python from reflex.vars.base import Var, VarData -from reflex_base.constants import Hooks from reflex.components.el.elements import Div @@ -60,34 +52,19 @@ class ComponentWithHooks(Div, MyBaseComponent): hooks1 = """const customHookVariable = "some value";""" hooks.append(hooks1) - # A hook that need to be written before the memoized event handlers. + # A hook that reads the variable declared by the hook before it. hooks2 = Var( """useEffect(() => { - console.log("PreTrigger: " + customHookVariable); + console.log(customHookVariable); }, []); """, _var_data=VarData( imports={ "react": ["useEffect"], }, - position=Hooks.HookPosition.PRE_TRIGGER, ), ) hooks.append(hooks2) - - hooks3 = Var( - """useEffect(() => { - console.log("PostTrigger: " + customHookVariable); - }, []); - """, - _var_data=VarData( - imports={ - "react": ["useEffect"], - }, - position=Hooks.HookPosition.POST_TRIGGER, - ), - ) - hooks.append(hooks3) return hooks ``` @@ -98,19 +75,63 @@ export function Div_7178f430b7b371af8a12d8265d65ab9b() { const customHookVariable = "some value"; useEffect(() => { - console.log("PreTrigger: " + customHookVariable); + console.log(customHookVariable); }, []); - /* memoized triggers such as on_click, on_change, etc will render here */ + return jsx("div", {}); +} +``` + +### Hooks that read other hooks + +When a hook reads a value that another hook declares, list that declaration in the `hooks` attribute of its `VarData`. Each entry maps a hook's code to its own `VarData` (or `None`). A hook's dependencies always render before it, in whichever component the hook ends up in, so a hook `Var` can be reused across components or passed in through a prop without caring about where its dependency is declared. + +```python +from reflex.vars.base import Var, VarData +from reflex.components.el.elements import Div + + +class CountLogger(Div, MyBaseComponent): + """MyComponent.""" + + def add_hooks(self) -> list[str | Var]: + """Add hooks to the component.""" + use_count = VarData( + imports={"react": ["useState"]}, + ) + log_count = Var( + """useEffect(() => { + console.log(count); + }, [count]); + """, + _var_data=VarData( + imports={"react": ["useEffect"]}, + hooks={"const [count, setCount] = useState(0);": use_count}, + ), + ) + return [log_count] +``` + +The `useState` declaration renders before the `useEffect` that reads it, and both imports are included: + +```javascript +export function Div_2b1c5f3e9a7d4c6e8f0a1b2c3d4e5f60() { + const [count, setCount] = useState(0); useEffect(() => { - console.log("PostTrigger: "+ customHookVariable); - }, []); + console.log(count); + }, [count]); return jsx("div", {}); } ``` +```md alert warning +# The `position` attribute of `VarData` and `Hooks.HookPosition` are deprecated. + +Earlier versions sorted hooks into `PRE_TRIGGER` and `POST_TRIGGER` groups. Passing `position` now warns and has no effect on the order. To place a hook after another one, list the hook it reads in its `VarData(hooks=...)` as shown above. +``` + ```md alert info # You can mix custom code and hooks in the same component. Hooks can access a variable defined in the custom code, but custom code cannot access a variable defined in a hook. ``` diff --git a/packages/reflex-components-core/news/+hook-declaration-order.bugfix.md b/packages/reflex-components-core/news/+hook-declaration-order.bugfix.md new file mode 100644 index 00000000000..d05d5cf4cef --- /dev/null +++ b/packages/reflex-components-core/news/+hook-declaration-order.bugfix.md @@ -0,0 +1 @@ +`window_event_listener` and `clipboard` hooks now render in declaration order with the rest of the component's hooks instead of being moved after them. diff --git a/packages/reflex-components-core/news/+hook-declaration-order.misc.md b/packages/reflex-components-core/news/+hook-declaration-order.misc.md deleted file mode 100644 index e105a3e5128..00000000000 --- a/packages/reflex-components-core/news/+hook-declaration-order.misc.md +++ /dev/null @@ -1 +0,0 @@ -`window_event_listener` and `clipboard` no longer set a hook position.