From 6ea5a31a9223142baba670afcac97748b5c53b63 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Tue, 23 Jun 2026 11:08:41 -0700 Subject: [PATCH] fix(match): bind useContext for state vars used in rx.match case conditions rx.match only counted its subject toward statefulness, so a state Var used only in a case condition with a literal subject -- e.g. rx.match(True, (State.x > 0, comp), ...) -- left Match un-memoized. The compiled switch then referenced the substate context variable without a useContext binding, raising "ReferenceError: Can't find variable" at render (the page route emits useContext only via memoized children). Surface the case-condition Vars in Match._get_vars (and merge their var-data in add_imports, mirroring the Var-return branch) so Match is memoized when a case condition is stateful and the binding is emitted. Keeps the existing passthrough strategy (branches still memoize independently). Adds a regression test. Fixes #6675. Co-Authored-By: Claude Opus 4.8 --- .../news/6675.bugfix.md | 1 + .../src/reflex_components_core/core/match.py | 36 ++++++++++++++++++- tests/units/compiler/test_memoize_plugin.py | 34 ++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 packages/reflex-components-core/news/6675.bugfix.md diff --git a/packages/reflex-components-core/news/6675.bugfix.md b/packages/reflex-components-core/news/6675.bugfix.md new file mode 100644 index 00000000000..240d4f41bf6 --- /dev/null +++ b/packages/reflex-components-core/news/6675.bugfix.md @@ -0,0 +1 @@ +Fix `rx.match` raising `ReferenceError: Can't find variable` at render when a state Var is used in a case *condition* with component branches. The match is now rendered inside a memo wrapper that binds the required state context, and the case-condition Vars are surfaced to the compiler so their `useContext` hooks/imports are emitted. 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..45e1f6c5e5e 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 @@ -1,6 +1,7 @@ """rx.match.""" import textwrap +from collections.abc import Iterator from typing import Any, cast from reflex_base.components.component import BaseComponent, Component, field @@ -310,13 +311,46 @@ def render(self) -> dict: """ return dict(self._render()) + def _get_vars( + self, include_children: bool = False, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: + """Walk all Vars used in this component, including the case conditions. + + The per-case condition Vars live in ``match_cases``, which is not a + JavaScript property, so the base implementation does not surface them. + Yield them here so the hooks they require are emitted -- in particular + the ``useContext`` binding for a state Var referenced only in a case + condition. Without this, the compiled ``switch`` references the + substate context variable without ever binding it, raising + ``ReferenceError: Can't find variable`` at render time. + + Args: + include_children: Whether to include Vars from children. + ignore_ids: The ids to ignore. + + Yields: + Each Var referenced by the component, plus the case conditions. + """ + yield from super()._get_vars( + include_children=include_children, ignore_ids=ignore_ids + ) + for conditions, _ in self.match_cases: + yield from conditions + def add_imports(self) -> ImportDict: """Add imports for the Match component. Returns: The import dict. """ - var_data = VarData.merge(self.cond._get_all_var_data()) + var_data = VarData.merge( + self.cond._get_all_var_data(), + *[ + condition._get_all_var_data() + for conditions, _ in self.match_cases + for condition in conditions + ], + ) return var_data.old_school_imports() if var_data else {} diff --git a/tests/units/compiler/test_memoize_plugin.py b/tests/units/compiler/test_memoize_plugin.py index b9fb1c5df51..d660c3b79aa 100644 --- a/tests/units/compiler/test_memoize_plugin.py +++ b/tests/units/compiler/test_memoize_plugin.py @@ -1087,6 +1087,40 @@ def page() -> Component: assert any("withprop" in tag.lower() for tag in wrapper_tags) +def test_match_literal_subject_stateful_condition_is_memoized() -> None: + """Match with a literal subject and a state Var in a case condition is memoized. + + The case-condition Vars (not just the subject) must count toward Match's + statefulness. Otherwise the compiled ``switch`` references the substate + context variable without a ``useContext`` binding, raising + ``ReferenceError: Can't find variable`` at render. Regression test. + """ + + def page() -> Component: + comp = rx.match( + True, + ( + SpecialFormMemoState.value == "a", + WithProp.create(label=LiteralVar.create("A")), + ), + ( + SpecialFormMemoState.value == "b", + WithProp.create(label=LiteralVar.create("B")), + ), + WithProp.create(label=LiteralVar.create("default")), + ) + assert isinstance(comp, Component) + return comp + + ctx, _page_ctx = _compile_single_page(page) + wrapper_tags = tuple(ctx.memoize_wrappers) + assert any("match" in tag.lower() for tag in wrapper_tags), ( + "Match with a state Var in a case condition (and a literal subject) " + "must be memoized so the condition's useContext binding is emitted; " + f"got wrappers: {list(wrapper_tags)}" + ) + + def test_cond_stateful_branch_component_renders_via_memoized_wrapper() -> None: """Components inside Cond branches must render via their memo wrappers.