Skip to content

Minify state var names and allow installing a name resolver after states exist - #7337

Open
benedikt-bartscher wants to merge 189 commits into
reflex-dev:mainfrom
benedikt-bartscher:late-bound-wire-names
Open

benedikt-bartscher wants to merge 189 commits into
reflex-dev:mainfrom
benedikt-bartscher:late-bound-wire-names

Conversation

@benedikt-bartscher

@benedikt-bartscher benedikt-bartscher commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #6100 — the diff includes its commits until that merges. The changes specific to this PR are in fb6992b and the merges of explicit-event-id-minification that follow it.

What this adds

Var name minification. REFLEX_MINIFY_VARS=1 shortens state var keys on the wire using a new optional vars section in minify.json (still schema version 1; older files load with no vars). A minified key is just its id, so a delta goes from {"a.b": {"items_rx_state_": []}} to {"a.b": {"c": []}}. reflex minify init/sync/validate/list/lookup cover vars, and the scheme digest includes them. Var ids that every JS object already has (constructor, __proto__, …) or that end in _rx_state_ are rejected.

A name resolver can be installed after states exist. RegistrationContext.set_name_resolver rebuilds the Vars every registered state holds. Backend dependency tracking and VarData.field_dependencies now key on classes and the resolver-independent default name, so they never go stale. This lets in-process AppHarness tests run fully minified. A Var built from state Vars before the install (e.g. a module-level component imported before minify.json was loaded) is not fixed; compiling fails with a message naming each stale reference instead of shipping a broken frontend.

One place builds wire keys. format_var_key / format_state_var (which reads through the prefixed local format_state_local from #6100) replace the ad-hoc name + FIELD_MARKER concatenations; update_vars_internal maps keys back through a per-state lookup, and state.js reads the hydrated flag's key from the compiled context instead of hardcoding it.

Notes

  • setvar still sends the Python var name: it dispatches through the class hierarchy, where a per-state id would be ambiguous.
  • REFLEX_PERF_MODE's check for stringified vars keys on the field marker, so it doesn't see minified vars.
  • Router fields and is_hydrated are minified too; nothing in the frontend reads them by name anymore.
  • New public API: NameResolver.resolve_var_name, reflex_base.utils.format.format_var_key / format_state_var.

Testing

  • Unit tests for var ids, config validation, sync/prune, the stale-name check, rebuilding Vars on a late install, dependency tracking and always-dirty substates under renamed states, client-storage compile, update_vars_internal with minified keys, and the CLI.
  • tests/integration/test_minification.py now runs in-process with framework states and vars minified.
  • A downstream app driven by Playwright in prod with all three modes on: inherited, computed and backend vars, background tasks, ComponentState, cookie/localStorage persistence across tabs, dynamic routes; websocket frames carry no var or state names.

🤖 Generated with Claude Code

benedikt-bartscher and others added 30 commits January 24, 2026 20:43
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
When a parent and child state have the same minified name, substate
resolution can fail because the leading segment is stripped incorrectly. This
change adds a flag to skip stripping only on the initial recursive call, ensuring
correct resolution even in name collision scenarios.
…into late-bound-wire-names

# Conflicts:
#	reflex/minify.py
…into late-bound-wire-names

# Conflicts:
#	reflex/minify.py
#	reflex/reflex.py
#	reflex/state.py
#	tests/units/minify_helpers.py
#	tests/units/reflex_base/test_registry.py
#	tests/units/test_minify.py
@masenf masenf added the perf Performance-improving changes label Sep 29, 2026
…inification

- state.js: keep the main_state_name prefix check in isStateful, with main's
  non-string event name guard and nested event normalization.
- vars/base.py, tests: keep both sides' imports and new tests.
- docs/api-reference/cli.md: keep compile and minify, drop the deleted
  component command.
- test_context_js_template.py: context_template no longer takes state_name.
…into late-bound-wire-names

- tests/benchmarks/test_shared_state.py: _always_dirty_substates holds state
  classes, so the shared-state harness adds SharedStateBaseInternal itself.
@masenf masenf added the documentation Improvements or additions to documentation label Oct 6, 2026
Ported upstream changes into the resolver-aware code:

- Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in
  InternalNames (replacing hydrate), alongside upstream's state schema and
  initial state hashes. state.js reads app.hydrate_and_load and
  app.update_vars_internal instead of hardcoded default names, and the
  handshake query (token + scheme) is built for the warm-up socket too.
- State reads use upstream's useStateContext (reflex-dev#7441), keyed by the
  state's resolved full name, into the branch's prefixed local.
- Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped
  backend_state_mismatch; the fatal minification scheme mismatch keeps its
  own flag (backend_scheme_mismatch) gating events, updates and reconnects.
- The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names;
  the scheme digest already covers the wire names.
- The processor's hydrate/hydrate_and_load/on_load_internal lookups cache
  handlers, not names, so they follow a resolver installed later.
- BaseStateToken._state_key and client storage application
  (_apply_client_storage_vars) use default names and wire var keys.
- get_substate keeps upstream's loop but only lets the first segment name
  the state itself; get_name/get_full_name stay function-wide caches so a
  resolver install clears every class, and also clears _get_substate_path.
- on_connect sets up per-SID state before upstream's new first await.
- pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
Comment thread reflex/minify.py
Comment thread packages/reflex-base/src/reflex_base/registry.py Outdated
…inification

Ported upstream changes into the resolver-aware code:

- Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in
  InternalEventNames (replacing hydrate), alongside upstream's state schema
  and initial state hashes. state.js reads app.hydrate_and_load and
  app.update_vars_internal instead of hardcoded default names, and the
  handshake query (token + scheme) is built for the warm-up socket too.
- State reads use upstream's useStateContext (reflex-dev#7441), keyed by the
  state's resolved full name, into the prefixed local.
- Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped
  backend_state_mismatch; the fatal minification scheme mismatch keeps its
  own flag (backend_scheme_mismatch) gating events, updates and reconnects.
- The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names;
  the scheme digest already covers the wire names.
- The processor's hydrate/hydrate_and_load/on_load_internal lookups cache
  handlers, not names, so they follow a resolver installed later.
- BaseStateToken._state_key keys by the resolver-independent name.
- get_substate keeps upstream's loop but only lets the first segment name
  the state itself; get_name/get_full_name stay function-wide caches so a
  resolver install clears every class, and also clears _get_substate_path.
- on_connect sets up per-SID state before upstream's new first await.
- pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
…into late-bound-wire-names

Brings in reflex-dev#6100's merge of upstream/main. Its conflicts here are the
reflex-dev#6100 spellings of code this branch already resolved (InternalEventNames
vs InternalNames, the hook's state name, tests of state/event-only
minification), so this branch's versions are kept and the tree is
unchanged.
A handler with no minify.json entry keeps its Python name, which can be
the id another handler of its state was given, e.g. when it is added
before 'reflex minify sync' runs. Registering it overwrote the other,
so an event for one ran the other. Registration now raises when two
handlers of a state resolve to the same name.

refresh_keys rebuilt the substate sets from base_states, which still
holds a state a module reload removed, so installing a resolver after
such a reload put the removed state back into the tree. It now re-keys
the substate sets as they are.

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation perf Performance-improving changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants