Deep equality Var operation (lodash `isEqual` equivalent)
Describe the Features
Requested on Discord by Bert. There is currently no Var operation for structural (deep) equality. Var.__eq__ compiles to a JS identity comparison, so two structurally identical dicts never compare equal on the frontend:
a = rx.Var.create({"a": 1, "b": 2})
b = rx.Var.create({"b": 2, "a": 1})
str(a == b)
# (({ ["a"] : 1, ["b"] : 2 })?.valueOf?.() === ({ ["b"] : 2, ["a"] : 1 })?.valueOf?.())
# -> false at runtime: `===` on objects is reference equalityThe obvious workaround — comparing to_string(), i.e. JSON.stringify — is key-insertion-order dependent, so it is also false here:
str(a.to_string() == b.to_string())
# ((JSON.stringify(({ ["a"] : 1, ["b"] : 2 })))?.valueOf?.() === (JSON.stringify(({ ["b"] : 2, ["a"] : 1 })))?.valueOf?.())
# JSON.stringify({a:1,b:2}) === '{"a":1,"b":2}'
# JSON.stringify({b:2,a:1}) === '{"b":2,"a":1}' -> falseThe request is for a Var operation equivalent to lodash's isEqual: deep structural comparison of objects/arrays/primitives, returning a BooleanVar usable anywhere on the frontend.
Purpose: compare two state-derived structures on the frontend, without round-tripping to the backend and without hand-rolling a comparison per data shape.
Use case (from the reporter): a form that highlights each field that differs from what was previously submitted. Most fields are scalars, so
previous == currentis enough. One field is aPaintScheme— a mapping of house element to the color chosen for it, picked from a catalog. Changing the whole scheme is a cheap id/name comparison, but changing individual element colors within a scheme means comparing the entire nested mapping against the previously submitted one. The form is data-driven — the same code renders many different form configurations and item types, of whichPaintSchemeis one — so a hard-coded per-shape comparison isn't available.
Current workaround
Since reduce landed (#6701), this can be built by hand: iterate the keys, & together the per-key comparisons.
S.prev.keys().reduce(
lambda acc, k: acc & (S.prev[k] == S.curr[k]),
True,
) & (S.prev.length() == S.curr.length())The reporter notes this took about half an hour to work out and explicitly is not claiming there's no alternative — so this is an ergonomics request, not a blocker. The rough edges of the workaround:
- It handles exactly one level. Each additional level of nesting needs another hand-written pass — in the reporter's case the values were
ColorValueobjects, so the real comparison sits one level below the key. - The explicit
length()check is needed to catch keys present in only one of the two dicts; it's easy to leave off and get a false positive. - It has to be written differently again for arrays.
Additional context
- Related: #7084 deep-compares memoized prop objects with
.to_string()(JSON.stringify), which carries the same key-order caveat shown above. A shared deep-equality primitive could back both. - Naming:
Var.equals()is already taken by a compile-time method that compares_js_expr/_var_type/_var_data(i.e. "are these the same Var?"), not runtime values. A new runtime operation should not reuse that name. - Implementation: likely a
var_operationemitting a small recursive comparison helper in the JS runtime. Pulling in lodash for one function seems unwarranted; there are small single-purpose deep-equal packages if a dependency is preferred over a hand-written helper.
Source: reflex-dev/reflex