From c26118fdd477493a16f77a7aad13115a80cff72a Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 20 May 2026 13:04:10 -0700 Subject: [PATCH 1/2] fix: use python truthiness for compiled `and`/`or` Var operations JS `&&`/`||` use JS truthiness, where `[]` and `{}` are truthy. Python treats them as falsy, so `Var([]) | true` was compiling to `[] || true` and returning `[]` instead of `true`. Same issue for `Var({}) & x`, `Var("") | y`, etc. Compile `a | b` to `pyOr(a, () => b)` and `a & b` to `pyAnd(a, () => b)`, backed by helpers that use `isTrue` for the truthiness check. RHS is wrapped in a thunk so short-circuit semantics are preserved. --- .../src/reflex_base/.templates/web/utils/state.js | 12 ++++++++++++ packages/reflex-base/src/reflex_base/vars/base.py | 15 +++++++++++++-- tests/units/test_var.py | 6 +++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index ce4a19a4747..225bbe448e2 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -1142,6 +1142,18 @@ export const isNotNullOrUndefined = (val) => { return (val ?? undefined) !== undefined; }; +/*** + * Python-semantics OR: returns `a` if python-truthy, else evaluates and returns `b`. + * `b` is a thunk so it is only evaluated when needed, preserving short-circuit. + */ +export const pyOr = (a, b) => (isTrue(a) ? a : b()); + +/*** + * Python-semantics AND: returns `a` if python-falsy, else evaluates and returns `b`. + * `b` is a thunk so it is only evaluated when needed, preserving short-circuit. + */ +export const pyAnd = (a, b) => (isTrue(a) ? b() : a); + /** * Get the value from a ref. * @param ref The ref to get the value from. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 4034b69aa3c..0181b2774ea 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -1955,6 +1955,15 @@ def __hash__(self: DataclassInstance) -> int: )) +_PY_AND_IMPORT: ImportDict = { + f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="pyAnd")], +} + +_PY_OR_IMPORT: ImportDict = { + f"$/{constants.Dirs.STATE_PATH}": [ImportVar(tag="pyOr")], +} + + def and_operation( a: Var[VAR_TYPE] | Any, b: Var[OTHER_VAR_TYPE] | Any ) -> Var[VAR_TYPE | OTHER_VAR_TYPE]: @@ -1982,8 +1991,9 @@ def _and_operation(a: Var, b: Var): The result of the logical AND operation. """ return var_operation_return( - js_expression=f"({a} && {b})", + js_expression=f"pyAnd({a}, () => ({b}))", var_type=unionize(a._var_type, b._var_type), + var_data=VarData(imports=_PY_AND_IMPORT), ) @@ -2014,8 +2024,9 @@ def _or_operation(a: Var, b: Var): The result of the logical OR operation. """ return var_operation_return( - js_expression=f"({a} || {b})", + js_expression=f"pyOr({a}, () => ({b}))", var_type=unionize(a._var_type, b._var_type), + var_data=VarData(imports=_PY_OR_IMPORT), ) diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 72aeb7ff78f..6971cdffcd4 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -313,8 +313,8 @@ def test_basic_operations(TestObj): assert str(LiteralNumberVar.create(1) // 2) == "Math.floor(1 / 2)" assert str(LiteralNumberVar.create(1) % 2) == "(1 % 2)" assert str(LiteralNumberVar.create(1) ** 2) == "(1 ** 2)" - assert str(LiteralNumberVar.create(1) & v(2)) == "(1 && 2)" - assert str(LiteralNumberVar.create(1) | v(2)) == "(1 || 2)" + assert str(LiteralNumberVar.create(1) & v(2)) == "pyAnd(1, () => (2))" + assert str(LiteralNumberVar.create(1) | v(2)) == "pyOr(1, () => (2))" assert str(LiteralArrayVar.create([1, 2, 3])[0]) == "[1, 2, 3]?.at?.(0)" assert ( str(LiteralObjectVar.create({"a": 1, "b": 2})["a"]) @@ -1025,7 +1025,7 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(isTrue((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))" + == "!(isTrue(pyOr(Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))), () => (pyAnd(2, () => (Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))))" ) assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" From 5544019c975d21e5e1b45662c5146db6a3f822fa Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 20 May 2026 13:09:07 -0700 Subject: [PATCH 2/2] docs: add JSDoc types to pyOr/pyAnd helpers --- .../src/reflex_base/.templates/web/utils/state.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js index 225bbe448e2..c07b4dfcbac 100644 --- a/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js +++ b/packages/reflex-base/src/reflex_base/.templates/web/utils/state.js @@ -1145,12 +1145,22 @@ export const isNotNullOrUndefined = (val) => { /*** * Python-semantics OR: returns `a` if python-truthy, else evaluates and returns `b`. * `b` is a thunk so it is only evaluated when needed, preserving short-circuit. + * @template A + * @template B + * @param {A} a The left-hand value. + * @param {() => B} b Thunk producing the right-hand value. + * @returns {A | B} `a` if python-truthy, otherwise the result of `b()`. */ export const pyOr = (a, b) => (isTrue(a) ? a : b()); /*** * Python-semantics AND: returns `a` if python-falsy, else evaluates and returns `b`. * `b` is a thunk so it is only evaluated when needed, preserving short-circuit. + * @template A + * @template B + * @param {A} a The left-hand value. + * @param {() => B} b Thunk producing the right-hand value. + * @returns {A | B} `a` if python-falsy, otherwise the result of `b()`. */ export const pyAnd = (a, b) => (isTrue(a) ? b() : a);