Skip to content

fix: guard binding updates against stale notifications after view unbind#7435

Merged
janechu merged 6 commits intomainfrom
users/janechu/assess-issue-7422
Apr 16, 2026
Merged

fix: guard binding updates against stale notifications after view unbind#7435
janechu merged 6 commits intomainfrom
users/janechu/assess-issue-7422

Conversation

@janechu
Copy link
Copy Markdown
Collaborator

@janechu janechu commented Apr 14, 2026

Pull Request

📖 Description

When a parent when directive tears down and recreates a child custom element with a property binding (e.g., :data), the child's template bindings could evaluate with a null source, causing a crash. This occurred because the coupled source lifetime optimization in ExpressionNotifierImplementation left expression observers subscribed to the child element's observable properties after the child's view was unbound.

Added isBound guards to HTMLBindingDirective.handleChange() and RenderBehavior.handleChange() to skip binding re-evaluation when the controller's view is no longer active. This prevents stale notifications from reaching expressions that would crash with a null source.

🎫 Issues

👩‍💻 Reviewer Notes

The root cause is that ExpressionNotifierImplementation.requiresUnbind() returns false for SourceLifetime.coupled bindings that only depend on the controller source. This means those observers are never disposed when the view is unbound, under the assumption that they will be garbage collected with the element. However, when a when directive reuses a cached view (same template), the element survives the unbind and the stale observers can fire.

The guards in handleChange() are a defensive fix at the subscriber level. A deeper fix would be to revisit the coupled lifetime optimization so that observers are always disposed on unbind for views that can be reused. That could be addressed as a follow-up.

The render test fixtures that used raw controller objects with isBound: false have been updated to set isBound = true after behavior.bind() to match real HTMLView lifecycle behavior.

📑 Test Plan

Two new Playwright tests added to binding.pw.spec.ts:

  1. Crash regression test — Reproduces the exact issue fix: when directive: child bindings may evaluate with no controller after teardown/recreate #7422 scenario: creates parent/child elements, toggles the when condition through multiple cycles (on → off → on → data change → off → on), and verifies no crash occurs.

  2. Rendering correctness test — Verifies that the child element's template correctly renders the pushed data label after toggling the when directive off and back on with new data.

All 1423 tests pass (1421 existing + 2 new).

✅ Checklist

General

  • I have included a change request file using $ npm run change
  • I have added tests for my changes.
  • I have tested my changes.
  • I have updated the project documentation to reflect my changes.
  • I have read the CONTRIBUTING documentation and followed the standards for this project.

⏭ Next Steps

  • Consider revisiting ExpressionNotifierImplementation.requiresUnbind() to properly dispose coupled-lifetime observers when the view is unbound, rather than relying on the subscriber-level guard. This would eliminate stale subscriptions entirely rather than silently skipping their notifications.

janechu and others added 2 commits April 14, 2026 13:34
When a parent `when` directive tears down and recreates a child custom
element with a property binding, the child's template bindings could
evaluate with a null source, causing a crash. This occurred because the
coupled source lifetime optimization left expression observers subscribed
to the child element's observable properties after the child's view was
unbound.

Added `isBound` guards to `HTMLBindingDirective.handleChange()` and
`RenderBehavior.handleChange()` to skip updates when the controller's
view is no longer active.

Fixes #7422

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as ready for review April 14, 2026 21:32
@janechu janechu marked this pull request as draft April 14, 2026 22:03
@janechu
Copy link
Copy Markdown
Collaborator Author

janechu commented Apr 14, 2026

Converting to draft to consider a more holistic approach on disposal of coupled lifetime observers. This could create some performance issues either way (from notifications that are silently ignore or from re-creating observables).

janechu added a commit that referenced this pull request Apr 14, 2026
Remove the coupled-lifetime optimization in ExpressionNotifierImplementation
that skipped unbind registration for observers with a single subscription on
the controller source. All expression observers now register for disposal
when their controller unbinds, ensuring stale subscriptions are eliminated
when views are torn down (e.g., by a parent when directive).

The isBound guards in HTMLBindingDirective.handleChange and
RenderBehavior.handleChange are retained as defence-in-depth against any
notification already queued before disposal completes.

Follows up on #7435 which added subscriber-level guards as a defensive fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
};

behavior.bind(controller);
controller.isBound = true;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

just commenting here to point out that this is the main change in the sea of comma linting adjustments.

janechu and others added 3 commits April 15, 2026 11:57
Rename custom element tags from issue-number-based names (e.g.,
test-child-7422a) to descriptive names (e.g., when-child-a).

Break the monolithic try/catch test into six focused tests, each with
its own page.evaluate and assertions verifying the expected DOM state
at each step of the when-directive toggle lifecycle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Link to #7444 above each
isBound guard to note this behavior will be reconsidered in the
next major version.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu marked this pull request as ready for review April 16, 2026 18:27
@janechu janechu requested a review from radium-v April 16, 2026 18:39
@janechu janechu merged commit c687b2e into main Apr 16, 2026
14 checks passed
@janechu janechu deleted the users/janechu/assess-issue-7422 branch April 16, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: when directive: child bindings may evaluate with no controller after teardown/recreate

3 participants