[BUGFIX] @model becomes undefined or changes to the wrong route's model during Glimmer component willDestroy - #21203
Conversation
When transitioning between routes, the @model argument on a Glimmer component becomes unstable during willDestroy - the model value changes before the component is properly destroyed. Requires @glimmer/component Vite alias to resolve in tests. Based on PR emberjs#20959 by @Windvis.
When transitioning between routes, @model becomes undefined in Glimmer component willDestroy hooks. The existing guard (lastState === state) only detects outlet changes at the same level. When a parent outlet tears down first, the dynamic scope refs silently redirect to the new route's outlet state while the guard still passes. Add a controller identity check so the model ref detects when its outletRef has been redirected to a different route's data. Fixes emberjs#18987
…nternal @glimmer/component import isn't necessary in vite.config
| named['controller'] = createConstRef(state.controller, '@controller'); | ||
|
|
||
| // Create a ref for the model | ||
| let modelRef = childRefFromParts(outletRef, ['render', 'model']); |
There was a problem hiding this comment.
I think something along this vein is the real fix:
| let modelRef = childRefFromParts(named['controller'], ['render', 'model']); |
The modelRef should only depend on the constant controller, not the variable reference to the controller.
There was a problem hiding this comment.
@ef4 Thanks, tried now (if I understood correctly): I get two test failures with something like that: https://github.com/johanrd/ember.js/actions/runs/22914352893/job/66495351782?pr=11
I guess we can be okay with @model tracking controller.model mutations? If yes, we may update the tests (bugfix, or RFC)?
There was a problem hiding this comment.
I got 33 failures https://github.com/emberjs/ember.js/actions/runs/22912061404/job/66486942647?pr=21215 🙈 ope
Concerns over @model updating during same-route transitions
@NullVoxPopuli tests are added for this now, but they could maybe be more specific? |
|
We are also running into issues with So this PR would be greatly appreciated :) |
|
In a 2.7 million line app, we have 27818 tests... that passed boring exploration, fumbling my package managerAs a point of (in)confidence, I tried this out on the 3.5 million line project at work, and got a ton of test failures 🤔 (I don't have the app at work on the latest canary tho ... I should try that out as a baseline) update: happens on main, so I need to figure out what's going on here |
|
@NullVoxPopuli ok, thanks a lot for running such a thorough test on the pr! Just to not fully leave the the @ef4-inspired solution: - let modelRef = childRefFromParts(outletRef, ['render', 'model']);
+ let modelRef = childRefFromParts(named['controller'], ['model']);This only fails on the tests that check that @model should not rerender external mutations to the model via the controller:
The current premise is from the RFC:
I am not sure what is the real fix in the long term – changing it may require RFC, though |
|
I'd like @ef4 to re-review before any merge |
|
So just to let you know we are about to open a draft PR with the Route Manager RFC implementation. I would recommend adding this test to that PR to double check if the problem still persists 🤔 I suspect that @ef4 's suggestion would be much clearer once we see the clear boundary between the classic route manager and the actual router |
The problem persists in itself (but is fixed because the fix has been applied to #21460) Tested #21460 now, but with restoring the logic to the current ember-7.0.0 bevavior: wrapperArgs['model'] = createComputeRef(() => {
if (lastState === state) {
- let currentOutlet = valueForRef(outletRef);
- if (currentOutlet?.render?.controller === controller) {
- model = valueForRef(modelRef);
- }
+ model = valueForRef(modelRef);
}
return model;
});
|
evoactivity
left a comment
There was a problem hiding this comment.
I would merge this.
I already pulled it's tests into the route manager work, bart has done a bunch of work there to change how outlets work, so if those tests are still passing on that branch this fix will be superseded by that work, but no reason to wait for the route manager stuff to land to get this fix in place now.

Summary
@modelbecomesundefined(or changes to the wrong route's model) during Glimmer componentwillDestroywhen transitioning between routeslastState === stateguard in the outlet helper's model compute ref@modelis stable between route transitions #20959 by @Windvis)Details
The
@modelreference in route templates is built from a chain of compute refs that read from the shared outlet state tree via dynamic scope. When transitioning,_setOutlets()rebuilds this tree, and all refs in the chain immediately see the new state.The existing guard (
lastState === state) freezes the model value when the outlet changes at the same level. But when a parent outlet is torn down first, the child outlet's outer compute ref never re-evaluates, solastStateis never updated. The guard passes, andmodelRefreads from the wrong outlet state.The fix compares the current outlet's controller identity against the expected one. Since each route has a unique controller singleton, this detects when the
outletRefhas been redirected — even whenlastStatehasn't been updated yet.From git history, the bug may have existed since
@modelwas introduced in v3.14.0 (16b74a5). The original implementation read directly from theoutletRefchain with no guard at all. ThelastState === stateguard added in v4.0.0 (commit 7d334cf) partially mitigated same-level transitions but did not catch parent-level teardown.Fixes #18987
Supersedes #20959
Test plan
smoke-tests/scenarios/basic-test.ts: transitions through sibling, parent, cousin, and unrelated routes, verifyingthis.args.modelinwillDestroyBased on Add a test that verifies@modelis stable between route transitions #20959 but moved to smoke-test to avoid the vite.config import of glimmer-componentBefore the fix, the added smoke tests fail with:
workspace:*(with fix)~6..11.0~5.12.0~4.0.0~3.28.0Cowritten by claude