feat(vars): add SetVar for set and frozenset vars

FA
1d ago
+1537-32

Closes #7623

Type of change

  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Changes To Core Features

What changed

A set-typed state var or literal was an ArrayVar, so it exposed sequence operations a set does not have, and frozenset was rejected by LiteralVar.create. #7622 widened ArrayVar instead and was reverted in #7632.

This adds SetVar / LiteralSetVar in reflex_base/vars/set.py for set[T] and frozenset[T], following the DateTimeVar pattern: a typed var over an untyped wire value.

  • Wire format stays a JSON array. serialize_set is unchanged (and frozenset gains the same serializer, so it is now a valid state var type) and nothing in applyDelta changes. An array also round-trips through event arguments, which a real JS Set would not (JSON.stringify(new Set(["a"])) is "{}"). The issue's "rehydrate as Set on the frontend" prerequisite is dropped for that reason.
  • Operations compile to small pySet* helpers in state.js (next to pyFlatMap) that key elements by value, so a set[tuple[int, int]] gets Python answers rather than array-identity comparison: contains/has, length/size, union, intersection, difference, symmetric_difference, issubset, issuperset, isdisjoint, and to_array() which is the JS identity typed as Sequence[T] (optional sets stay optional). Set-returning operations keep frozenset-ness and carry the element type through.
  • Routing. set is removed from ArrayVar.python_types; figure_out_type, Var.to, var_operation, Field.__get__ and ComputedVar.__get__ route set/frozenset to SetVar, so State.tags.contains("x") is a BooleanVar statically with no cast. rx.foreach accepts a SetVar by calling to_array().
  • Deprecation path. Indexing, map, filter, join, reverse, pluck, reduce, flat_map, + (either side) and * still work on a set var for one release with a console.deprecate pointing at .to_array(), and are marked @deprecated for type checkers.
  • A set literal renders its elements sorted when they compare, so compiled output no longer depends on the process hash seed.
  • LiteralArrayVar's element var-data and JSON helpers are extracted so LiteralSetVar shares them.

Tests

  • tests/units/reflex_base/vars/test_set.py: literal creation and static types (typing.assert_type under pyright), the rendered JS and helper import of every operation, tuple-element sets, operand validation, to_array (including optional sets), the deprecated fallbacks and concatenation in both directions, state fields (set, frozenset, optional) and computed vars, rx.foreach, VarData propagation.
  • tests/integration/tests_playwright/test_set_operations.py: backend add/discard re-render every operation and a foreach; a frozenset field; a set of tuples compared by value; an unset optional set renders no children; a set is sent back through an event argument and received intact (as a list, which is existing behavior for event args).
  • The helper JS was checked in Node against the same cases before being wired in.
  • uv run ruff check ., uv run ruff format ., uv run pyright reflex tests packages/reflex-base/src/reflex_base/vars packages/reflex-components-core/src are clean. The unit suite passes apart from tests/units/reflex_cli/v2, which fails identically on main in this environment (the hosting CLI rejects the 0.0.1.dev version string).

Docs and example apps checked for breakage

  • docs/enterprise/mantine/pill.md uses tags: set[str] with rx.foreach only: still works through to_array().
  • docs/guides/ai_applications.md uses frozenset in a backend dataclass only: unaffected.
  • No docs page indexes or maps a set var, so nothing hits the deprecation.
  • docs/vars/var-operations.md gains a "Set Operations" section and docs/api-reference/var_system.md lists SetVar.

Out of scope, as the issue says

  • Whether a set var satisfies a Sequence[...]-typed prop (typehint_issubclass) is unchanged.

https://claude.ai/code/session_01FTaqSdpd95XTrwgxAHExRd