Skip to content

Deep equality Var operation (lodash isEqual equivalent) #7160

Description

@masenf

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 equality

The 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}'   -> false

The 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 == current is enough. One field is a PaintScheme — 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 which PaintScheme is 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 ColorValue objects, 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: Extend rx.memo configuration and function memoization #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_operation emitting 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestA feature you wanted added to reflex

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions