Skip to content

fix: prevent stale re-renders of #each items during sequence update (#468) - #501

Merged
jankapunkt merged 8 commits into
meteor:release-3.1.0from
dupontbertrand:fix/each-stale-data-context
Sep 21, 2026
Merged

jankapunkt merged 8 commits into
meteor:release-3.1.0from
dupontbertrand:fix/each-stale-data-context

Conversation

@dupontbertrand

Copy link
Copy Markdown

Summary

Fixes #468

When the data source of a parent template changes, Tracker can re-run helpers inside #each item views before ObserveSequence has had a chance to diff and remove stale items. This causes items to briefly render with inconsistent data — e.g., an item's msg shows "foo" while the parent data context already says "bar".

Root cause

During Tracker.flush(), autoruns are re-run in invalidation order. When a parent data context changes:

  1. Helper autoruns inside existing #each item views are invalidated (via bindDataContext → Blaze.getData() → dataVar.get())
  2. The ObserveSequence autorun is also invalidated (it depends on the sequence function)
  3. The item helper autoruns can re-run BEFORE ObserveSequence diffs and removes stale items
  4. Result: stale item views re-render with old item data but new parent data

Fix

Freeze item views during sequence transitions using 3 hooks:

  • onInvalidate (new callback in ObserveSequence.observe): called immediately when the sequence source is invalidated, BEFORE Tracker.flush() re-runs other autoruns. Marks all current item views with _eachItemPendingUpdate = true.
  • afterDiff (new callback): clears the flag on surviving item views after the diff is applied.
  • doRender guard (in _materializeView): skips re-render if the view or any ancestor has _eachItemPendingUpdate set.

This ensures stale item views never re-render — they are either destroyed by removedAt or updated by changedAt, and the flag is cleared afterward.

Files changed

File Change
packages/observe-sequence/observe_sequence.js Add onInvalidate, beforeDiff, afterDiff callback hooks
packages/blaze/builtins.js Implement hooks in Blaze.Each to freeze/unfreeze item views
packages/blaze/view.js Guard in doRender to skip re-render when pending update

Test plan

  • Tested with 5 scenarios in a dedicated test app:
    1. Different IDs (removedAt + addedAt) — original bug report
    2. Same ID, changed content (changedAt)
    3. Multiple items reduced to one (3 removedAt)
    4. New-style #each item in items
    5. Nested #each (outer + inner)
  • All 5 scenarios produce zero stale renders
  • Full Tinytest suite: 416/416 passed, 0 failures

@jankapunkt jankapunkt mentioned this pull request Apr 3, 2026
15 of 17 tasks
@jankapunkt jankapunkt added this to the 3.1 milestone Apr 8, 2026
@jankapunkt jankapunkt added the ready2review feel free to add your review to this PR label Apr 9, 2026
@jankapunkt

Copy link
Copy Markdown
Collaborator

@dupontbertrand sorry for the delay I was occupied with a release the last two weeks. I will need a free timeslot this week to test with our projects then adding my review. We are close to 3.1.0 🚀

@jankapunkt jankapunkt linked an issue Apr 28, 2026 that may be closed by this pull request
@jankapunkt
jankapunkt requested a review from Copilot June 4, 2026 15:20

Copilot AI 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.

Pull request overview

This PR addresses #468 by preventing #each item subtrees from re-rendering with a “mixed” data context during Tracker.flush() (parent context already updated, but ObserveSequence hasn’t yet removed/updated stale items).

Changes:

  • Add onInvalidate, beforeDiff, and afterDiff hooks to ObserveSequence.observe so callers can coordinate sequence transitions.
  • In Blaze.Each, freeze existing item views on sequence invalidation and unfreeze surviving views after the diff is applied.
  • Add a guard in Blaze._materializeView’s doRender autorun to skip re-rendering views while an #each sequence update is pending.
  • Add Tinytests covering stale renders for “different IDs” and #each item in ... syntax.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/observe-sequence/observe_sequence.js Adds lifecycle hooks around invalidation and diffing for sequence observers.
packages/blaze/builtins.js Uses the new hooks in Blaze.Each to freeze/unfreeze item views during sequence transitions.
packages/blaze/view.js Adds a render guard to avoid stale re-renders while an #each update is pending.
packages/spacebars-tests/template_tests.js Adds regression tests validating no stale renders occur in key #each scenarios.
packages/spacebars-tests/template_tests.html Adds templates used by the new regression tests (plus an unused pair).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/blaze/view.js Outdated
Comment thread packages/observe-sequence/observe_sequence.js Outdated
Comment thread packages/spacebars-tests/template_tests.html
When the data source of a parent template changes, Tracker can re-run
helpers inside #each item views before ObserveSequence has had a chance
to diff and remove stale items. This causes items to briefly render with
inconsistent data (e.g., item shows "foo" while parent data says "bar").

Fix by freezing item views during sequence transitions:

- observe_sequence.js: add onInvalidate, beforeDiff, and afterDiff
  callbacks so callers can hook into the sequence update lifecycle
- builtins.js: use onInvalidate to mark item views with
  _eachItemPendingUpdate immediately when the sequence is invalidated
  (before Tracker flush), and afterDiff to clear the flag on survivors
- view.js: skip doRender re-runs when the view or an ancestor has
  _eachItemPendingUpdate set

Tested with 5 scenarios: different IDs, same ID with changed content,
multiple items removed, new-style #each (each-in), and nested #each.

Fixes meteor#468
Add two Tinytest cases that verify #each item views don't re-render
with stale data when the parent data context changes:
- Different IDs (removedAt + addedAt) — the original bug report
- New-style #each item in items syntax
…eteor#468)

The pending-update guard added for meteor#468 skipped an item view's re-render by
returning from `doRender` before `view._render()` ran. That left the
computation with zero reactive dependencies, so a surviving item view whose
helpers read the parent data context never re-rendered again (confirmed by a
regression test): it was frozen, not just delayed.

Instead of returning silently, the deferred render now depends on a per-each
revival `Tracker.Dependency`, which `afterDiff` fires once the pending flag is
cleared. The computation is therefore guaranteed to re-run and re-collect its
real dependencies with fresh data.

Also wrap the diff in try/finally in observe-sequence so `afterDiff` always
runs (clearing the flag and firing the revival dependency) even if a diff
callback throws; the `lastSeq`/`lastSeqArray` baseline is only advanced on the
success path, so a partial failure keeps the last consistent baseline.
meteor#468)

Add a regression test (using the previously unused parent2/child2 fixtures)
for a surviving #each item view (same _id, changed data) that must re-render
to its new data after a sequence update instead of freezing. Add an
observe-sequence test asserting `afterDiff` still runs when a diff callback
throws, so callers are never left permanently frozen.
@dupontbertrand
dupontbertrand force-pushed the fix/each-stale-data-context branch from 336cfd1 to 64c4b45 Compare June 6, 2026 09:10
@dupontbertrand

Copy link
Copy Markdown
Author

Hi @jankapunkt 👋 following up on your suggestion to build an app that reproduces #468 in every way I could think of, then swaps in the local Blaze to confirm the fix 👍

Repro app: https://github.com/dupontbertrand/blaze-468-repro

It's a Meteor 3 + Blaze app that reproduces the stale #each data context across 10 scenarios (different _ids · same-_id changedAt · partial removal · {{#each x in xs}} · nested · Mongo
cursor · movedTo · Template.parentData() · {{else}} · deep nesting). Each card shows a one-line description, the expected rule, and the actual item → ctx renders, with a per-scenario
OK/STALE badge and a "Run all" button.

  • On the released Blaze: 10/10 STALE (the bug).
  • After gh pr checkout 501 + ./scripts/use-local-blaze.sh (steps in the README): 10/10 OK.

Building it actually surfaced something worth flagging: the first version of the guard fixed the removed-item case from the report, but regressed surviving items (same _id, changedAt) : they froze instead of re-rendering, because the silent early-return left their computation with no reactive dependencies. I reworked the fix to defer the render via a revival Tracker.Dependency that afterDiff fires (rather than dropping it — the approach Copilot also suggested), and wrapped the diff in try/finally so a throwing diff callback can't leave items permanently frozen. That's force-pushed to the PR now; CI is green on both the native-DOM and jQuery backends

One honest scope note (also in the repro's README): this fixes staleness of the data context. A helper with a direct reactive dependency on the same source the sequence reads (e.g. a closure over a global ReactiveVar) is a separate race the deferral doesn't catch — happy to discuss whether that's worth chasing on its own

Whenever you have a slot to test against your projects, the repro should make it quick to confirm 😉

dupontbertrand and others added 2 commits September 14, 2026 16:43
# Conflicts:
#	packages/spacebars-tests/template_tests.html
#	packages/spacebars-tests/template_tests.js
@nachocodoner

Copy link
Copy Markdown
Member

I noticed that Blaze has official documentation at https://www.blazejs.org/, including the changelog at https://www.blazejs.org/changelog. These docs should also account for user-facing changes introduced in Blaze 3.1.0.

Could you please:

  • Add #501 to HISTORY.md, since that file feeds the official changelog.
  • Document the timing and behavior of the new onInvalidate and afterDiff callbacks in the callback list above ObserveSequence.observe.

Also, beforeDiff currently appears to have no consumer: Blaze.Each does not use it, and there is no test covering it. Unless there is a concrete intended use, I would remove it to avoid introducing an unnecessary internal API. If it is meant to remain, it should be documented and tested as well.

Since observe-sequence is currently described as an internal package, this does not necessarily require a new public API page. However, the source-level API documentation and official changelog should remain aligned with the new behavior.

@nachocodoner

nachocodoner commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

Repro app: dupontbertrand/blaze-468-repro

I've also added focused E2E regression coverage in Meteor core #14754, based on the reproduction, so these stale-render cases are covered in the existing Blaze test app.

The regression tests fail with the currently pinned Blaze version and pass with #501. I also checked the existing Blaze E2E coverage against #501 and found no new failures. The core PR intentionally keeps the current pin, leaving the regression tests failing until the fix is included.

Maybe at some point we can migrate some of the E2E test coverage setup here, so we can focus on Blaze-specific cases here rather than in Meteor core. But for the moment, I believe it's more suitable to keep them there, covering the full Meteor modern bundler lifecycle and ensuring new versions are already covered in core.

I know you've also covered the new behavior with package testing here, but I think it's good to at least add E2E coverage for common Blaze APIs, so we can also have signals on them in specific contexts, like Rspack, and not just against the Meteor bundler.

@nachocodoner

nachocodoner commented Sep 15, 2026 •

Copy link
Copy Markdown
Member

@dupontbertrand: Apart from my comment above, everything else looks good to me. I'm not that experienced with Blaze, though I understand your point and was able to see it in action in the E2E app. Also, please close the AI reviews on this PR that have already been addressed.

However, before accepting this, I believe @jankapunkt could provide some additional insight on its acceptance and merge, as he is mor experienced on Blaze. I believe he wanted to test it in his projects and provide feedback.

…date and afterDiff

Nothing consumes beforeDiff: Blaze.Each only uses onInvalidate and
afterDiff. Document when those two hooks run in the callback list above
ObserveSequence.observe.
@dupontbertrand

Copy link
Copy Markdown
Author

@nachocodoner done in feb003f and 6c7a32a:

  • HISTORY.md: fix: prevent stale re-renders of #each items during sequence update (#468) #501 added under a new v3.1.0-beta.0, 2026-XX-XX section, using the same date placeholder as the 3.0.3 entry.
  • onInvalidate and afterDiff are documented in the callback list above ObserveSequence.observe: when each one runs, that both only apply to re-evaluations of the sequence function (not to the incremental changes a cursor reports through its own observe), that onInvalidate also fires on stop(), and that afterDiff still runs if a diff callback throws.
  • beforeDiff removed: nothing consumes it.
  • The three Copilot threads were already addressed by db71e20 and 64c4b45; I replied in each and resolved them.

Full suite on Meteor 3.5.2: native 420 passed / 0 failed, jQuery 424 passed / 0 failed.

#513 isn't in HISTORY.md yet either. I can add it in a separate small PR if you want.

@nachocodoner

Copy link
Copy Markdown
Member

Hey @jankapunkt. What do you feel about this change? We already reviewed and tested within E2E, would you like to give it test on your projects as approached above? Or could we merge it and release Blaze 3.1.0 beta?

@jankapunkt

Copy link
Copy Markdown
Collaborator

@nachocodoner sorry for stalling, I already looked at it in the past and I would opt for merging and releasing with the beta. One question - should this retain history or can be squash-merged (cc @dupontbertrand )?

@dupontbertrand

Copy link
Copy Markdown
Author

@jankapunkt squash is fine here. Nothing in this branch is worth keeping: five incremental commits plus two merges of release-3.1.0 👍

Squash title: fix(blaze): prevent stale re-renders of #each items during sequence updates (#468)

(The opposite of #515 / #516, where the merge commit itself was the point.)

@jankapunkt
jankapunkt merged commit 70d33bb into meteor:release-3.1.0 Sep 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready2review feel free to add your review to this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

#each re-rendered with stale data context

4 participants