Skip to content

fix: sync shared-name storage vars in the writing tab (#7567) - #7637

Merged
masenf merged 9 commits into
reflex-dev:mainfrom
anishgurjar:fix-7567-same-tab-storage-sync
Oct 27, 2026
Merged

masenf merged 9 commits into
reflex-dev:mainfrom
anishgurjar:fix-7567-same-tab-storage-sync

Conversation

@anishgurjar

@anishgurjar anishgurjar commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #7567

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)

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

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?

What

Several rx.LocalStorage(..., sync=True) vars can share one storage name. A typical case is a named synced field on an rx.ComponentState that a page uses more than once. When the backend writes one of them, the frontend updates localStorage. The browser raises the storage event in other tabs only. So the sibling vars in the writing tab stay stale until a reload.

This fix sets the siblings on the backend. In BaseState._get_resolved_delta, _sync_shared_storage finds each localStorage var that this event made dirty. It then sets every other synced var with the same storage name to the same value. A var that the event wrote itself keeps its own value. This happens in the same locked event, before the delta is resolved. So the writing tab gets all siblings in one delta, and it sends no extra sync event. _shared_storage_groups groups the vars by storage name once per state tree, and the groups are cached. Hydration replay (_replay_client_storage) skips the fan-out. The frontend does not change.

Tests

  • tests/units/test_state.py::test_shared_storage_name_syncs_siblings: a write to one var sets the synced siblings with that name.
  • tests/units/test_state.py::test_shared_storage_name_keeps_event_writes: when one event writes two vars with the same name (synced or not), each var keeps its own value.
  • tests/units/test_state.py::test_shared_storage_name_conflicting_writes_pick_by_name: when one event writes different values to one name, the other synced vars take the first written value in state and var name order, in every process.
  • test_synced_storage_shared_name_syncs_siblings_in_writing_tab (Playwright): the siblings update in the writing tab, and the tab sends no update_vars_internal.
  • test_synced_storage_shared_name_override_keeps_syncing (Playwright): a get_delta override that rewrites the value does not stop later syncs.

The first unit test and both Playwright tests fail on main and pass with the fix. All 42 tests in test_hydration_storage.py pass, including the race tests that hold socket replies.

@anishgurjar
anishgurjar requested a review from a team as a code owner October 22, 2026 18:32

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread tests/units/compiler/state_js.test.mjs Outdated
@anishgurjar
anishgurjar force-pushed the fix-7567-same-tab-storage-sync branch from d0cba9f to d931987 Compare October 22, 2026 18:36
@anishgurjar
anishgurjar force-pushed the fix-7567-same-tab-storage-sync branch from d931987 to 344ae42 Compare October 22, 2026 18:37
@greptile-apps

greptile-apps Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issue was found.

Summary

Updates synced LocalStorage vars sharing a storage name in the writing tab, without sending another browser event.

  • A shared local-storage write updates synced siblings in the writing tab.

The earlier browser-side findings no longer apply to the replacement approach. The backend also collects explicit writes before setting siblings, addressing the earlier overwrite finding.

Reviews (8) · Last reviewed commit: "Sort shared storage groups by state and ..." · Reviewed by Greptile

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
One delta writing two same-name vars with different values would queue a
sync that excluded the earlier writer, leaving it on the old value while
storage held the last write. Build the sync set from every synced var
with the name, like handleStorage, so the last write converges all of
them.
@anishgurjar

Copy link
Copy Markdown
Contributor Author

Hi @masenf, this is ready for a look when you have a moment. The workflows are waiting on first-time-contributor approval; the new state.js tests pass locally (37/37, and the new ones fail against the template on main).

@FarhanAliRaza FarhanAliRaza added the on deck PRs lined up to review / merge next label Oct 22, 2026
@codspeed

codspeed Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 22 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 148 untouched benchmarks


Comparing anishgurjar:fix-7567-same-tab-storage-sync (52afe72) with main (9c64f1e)

Open in CodSpeed

@masenf

masenf commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

The required app-harness workflow now fails test_synced_storage_echo_after_own_write_is_written[..., shared_name] in both dev/prod modes across all ten memory/Redis Python matrix legs. The assertion expects sent but receives old (representative job). This is reproducible, not a transient CI failure.

I reduced it against the actual state.js module with Node 24: the existing 37 tests pass, while this ordering case fails:

  1. Another tab's sent value is sent through update_vars_internal, while an older handler reply is delayed.
  2. That older reply sets the unsynced sibling to old; the new same-tab path emits an additional synced-sibling update carrying old.
  3. The original sent echo arrives and restores storage to sent.
  4. The additional old echo arrives last and overwrites it again.

The same-tab sync generated from an older response is being treated as a fresh write. This needs an ordering fix that preserves same-tab propagation and the existing cross-tab echo protection; simply suppressing every pending echo can also suppress legitimate updates whose backend delta was transformed. I have left the synchronization design to the author/reviewers and have not rerun the deterministic failure.

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread tests/units/compiler/state_js.test.mjs Outdated
Skip the sibling sync when any synced var sharing the storage name has
an unechoed entry in sentStorageValues, so a stale delta cannot resend
an older value while a newer update_vars_internal is in flight. Adds
the Playwright regression test requested in review.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Replace the frontend same-tab sync with a backend fan-out. When an event
writes a localStorage var, set the other synced vars with the same storage
name in the same locked event, before the delta is resolved. The writing
tab gets every sibling in one delta and sends no extra sync event.

The frontend sync had to track sent values that the backend did not echo.
A get_delta override that rewrites the echo left such a value behind, and
the guard then suppressed the sibling sync for that name until reload.
The backend fan-out keeps no per-tab state, so that cannot happen.
@FarhanAliRaza

Copy link
Copy Markdown
Contributor

I changed the solution in this PR. The fix now sets the sibling vars on the backend, not from the frontend.

Why: the frontend sync had to track sent values that the backend did not echo yet. A get_delta override that rewrites the echo left such a value behind. The guard then stopped the sibling sync for that storage name until a reload (the open greptile and cubic threads). Clearing the stale value instead caused an endless sync loop.

Now _sync_shared_storage sets the other synced vars with the same storage name in the same locked event, before the delta is resolved. The writing tab gets all values in one delta and sends no extra event. There is no per-tab state, so the override case keeps syncing, and no reply can race with a later write. The state.js changes and their unit tests are removed. The frontend is the same as on main.

Tests: a new unit test and two Playwright tests (sibling sync and the override case) fail on main and pass now. All 42 tests in test_hydration_storage.py pass. The PR description is updated.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread reflex/state.py Outdated
Comment thread reflex/state.py Outdated
When one event writes two synced vars with the same storage name, the
fan-out set each to the other's value and lost one write. Collect the
event's writes first and do not set a var the event wrote.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread reflex/state.py Outdated
The fan-out found the vars an event wrote from a per-var map that left
out vars with no synced sibling, so an unsynced write could overwrite a
synced var written in the same event. Group the vars by storage name and
find the written vars in each group, so a written var always keeps its
value.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread reflex/state.py
The groups came from a walk over a set of state classes, so their order,
and the value that conflicting writes give the other synced vars, could
differ between processes. Sort each group once when it is built.
@masenf masenf added the bug Something isn't working label Oct 27, 2026
@masenf
masenf merged commit bac5d98 into reflex-dev:main Oct 27, 2026
172 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Synced storage vars sharing one name stay stale in the tab that writes

3 participants