Use initial_value for computed var dependencies when building the initial state dict

FA
4h ago
+172-12

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Problem

BaseState.dict(initial=True) (used by compile_state and by _diff_against_initial_state) substitutes initial_value only for the computed vars it lists itself. A computed var without an initial value is still evaluated, and when its getter reads self.other_var, ComputedVar.__get__ runs other_var's getter even though it has an initial_value. With the repro from #5767 (return annotations added, which are required now), compile_state still raises var1: This should never happen at compile time on main.

Fix

  • reflex_base.vars.base gets a private ContextVar, _initial_dict_cache. BaseState.dict(initial=True) sets it to an empty dict around the evaluation of its computed vars and resets it in a finally.
  • ComputedVar.__get__ checks it only where it is about to call the getter (uncached var, or cache miss), so a read that hits the cache costs nothing extra. While it is set, a var with an initial_value yields that value and its getter does not run.
  • A cached var without an initial_value is computed once per initial-dict build into that build-local cache, and never written to the instance cache, so repeated reads do not rerun the getter and nothing derived from initial values can be served later. _check_deprecated_return_type runs on the values computed there, as it does at runtime.
  • The context var is set and reset inside one synchronous call, and a ContextVar is local to the task, so a live session never sees it: at runtime self.var1 runs the getter or uses its cache as before.
  • reflex now imports a symbol that only exists in unreleased reflex-base, so its floor is raised to reflex-base >= 0.11.1.dev0 (uv.lock is unchanged by this).

Deliberately not changed

  • Async computed vars: their coroutines are awaited later by _resolve_delta, outside dict(), so a read of a computed var from inside an async getter is not covered here.
  • The top-level substitution in dict(initial=True) and is_computed_var are left as they are.
  • No integration test: there is nothing to observe in a browser beyond what the initial dict contains.

Tests

tests/units/test_state.py::test_initial_dict_uses_initial_value_of_computed_var_dependencies builds the initial dict of a state and its substate and compares it in full: dependents of a cached and of an uncached var with initial_value, a transitive dependent, and a substate reading inherited vars all see the initial values. No getter of a var with an initial value is called, and the cached var without one, which is read twice, runs its getter once. It then reads the vars normally and checks that the getters run, in the exact order expected, and that dict() returns the runtime values (nothing from the initial pass was cached on the state). It fails on main (101/104/3 instead of 110/140/30).

Run locally:

  • uv run pytest tests/units/test_state.py tests/units/vars tests/units/reflex_base/vars tests/units/compiler -q — 1216 passed
  • uv run ruff check . — clean; uv run ruff format . — no changes
  • uv run pyright reflex/state.py packages/reflex-base/src/reflex_base/vars/base.py tests/units/test_state.py — 0 errors
  • The issue's repro through compile_state: raises without the fix, returns {'var1': True, 'var2': None} with it

https://claude.ai/code/session_01LwJUCUfMV8TRZsr6ehRyuR

closes #5767