Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/reflex-components-core/news/6675.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {}


Expand Down
34 changes: 34 additions & 0 deletions tests/units/compiler/test_memoize_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading