Skip to content

Hydration drops cached dependent updates from get_delta storage assignments #7571

Open

Description

@masenf

Regression from #7568, reproduced on current main at 7ccaa4e213fa1f67374002cb28709272866c3d53.

When a get_delta() override assigns a corrected storage value before calling super().get_delta(), the backend value and cached dependents update, but hydration never sends the corrected cached dependent to the client.

Reproduction

Add this test to tests/units/reflex_base/event/processor/test_base_state_processor.py (it uses that module's existing imports and fixtures), then run:

uv run pytest tests/units/reflex_base/event/processor/test_base_state_processor.py -k hydrate_sends_override_mutation_dependents -q
async def test_hydrate_sends_override_mutation_dependents(
    wired_app, real_base_state_processor, emitted_deltas, token
):
    class StorageState(State):
        value: str = rx.LocalStorage("")

        @_override_base_method
        def get_delta(self) -> Delta:
            if "value" in self.dirty_vars and self.value == "stale":
                self.value = "fresh"
            return super().get_delta()

    class DependentState(State):
        @rx.var(cache=True, auto_deps=False)
        async def copy(self) -> str:
            return (await self.get_state(StorageState)).value

    DependentState.computed_vars["copy"].add_dependency(
        DependentState, StorageState.value
    )
    wired_app.add_page(
        lambda: rx.text(StorageState.value, DependentState.copy), route="/"
    )
    wired_app._compile_page("index")
    name = StorageState.get_full_name()
    key = "value" + FIELD_MARKER
    boot_name = Event.from_event_type(State.hydrate_and_load())[0].name
    payload = {"vars": {f"{name}.{key}": "stale"}}

    async with real_base_state_processor as processor:
        await (
            await processor.enqueue(token, _boot_event(boot_name, payload))
        ).wait_all()

    async with _read_back(real_base_state_processor, token) as root:
        assert (await root.get_state(StorageState)).value == "fresh"
        assert await (await root.get_state(DependentState)).copy == "fresh"

    storage_values = [
        delta[name][key]
        for _, delta in emitted_deltas
        if key in delta.get(name, {})
    ]
    dependent_values = [
        delta[DependentState.get_full_name()]["copy" + FIELD_MARKER]
        for _, delta in emitted_deltas
        if "copy" + FIELD_MARKER in delta.get(DependentState.get_full_name(), {})
    ]
    assert storage_values[-1] == "fresh"
    assert dependent_values[-1] == "fresh"  # Actual: "stale"

Expected / actual

Expected: the correction delta delivers both the changed storage field and its changed cached dependents.

Actual: emitted storage values are ["stale", "fresh"] (snapshot, then correction); the dependent is emitted only as ["stale"]. Reading the backend storage and dependent after hydration returns "fresh".

The fields-only correction pass suppresses computed vars even when a real assignment invalidates them. The following self._clean() then drops their dirty marks.

Verification

The parameterized regression passes 48/48 on the pre-merge base 2fc4a9caa5ce125acd9ce1f2e13cf5910ca5f8d8. On merged main, all 24 stale-input cases fail and all 24 fresh-input controls pass.

Coverage: LocalStorage / SessionStorage / Cookie × hashes on/off × on-load on/off × in-process / Redis state managers. Redis uses the repository's mocked transport. These are event-processor tests inspecting emitted deltas, not a browser reproduction. Release adoption has not been checked.

This is a direct state assignment, not an override that only rewrites its returned delta. The fix should retain #7568's single cached evaluation for unchanged storage and preserve output-only override behavior.

Activity

  1. self-assigned this
    on Oct 14, 2026
  2. linear-code commented on Oct 14, 2026

    @linear-code
  3. added a commit that references this issue on Oct 15, 2026
    2301a72
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions