Skip to content

[BUGFIX] @model becomes undefined or changes to the wrong route's model during Glimmer component willDestroy#21203

Open
johanrd wants to merge 6 commits intoemberjs:mainfrom
johanrd:test/20959
Open

[BUGFIX] @model becomes undefined or changes to the wrong route's model during Glimmer component willDestroy#21203
johanrd wants to merge 6 commits intoemberjs:mainfrom
johanrd:test/20959

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Mar 8, 2026

Summary

Details

The @model reference 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, so lastState is never updated. The guard passes, andmodelRef reads 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 outletRef has been redirected — even when lastState hasn't been updated yet.

From git history, the bug may have existed since @model was introduced in v3.14.0 (16b74a5). The original implementation read directly from the outletRef chain with no guard at all. The lastState === state guard 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 test in smoke-tests/scenarios/basic-test.ts: transitions through sibling, parent, cousin, and unrelated routes, verifying this.args.model in willDestroy Based on Add a test that verifies @model is stable between route transitions #20959 but moved to smoke-test to avoid the vite.config import of glimmer-component
  • Full internal test suite passes (5907/5907)
  • Scenario tests pass for both v1 (classic/embroider-webpack) and v2 (embroider-vite) app builds

Before the fix, the added smoke tests fail with:

not ok 1 - Acceptance | @model stability during route transitions: @model should be stable when transitioning out of the route                                                                                                                                                                                                            
      actual:   b,b,d,e,
      expected: b,b,b,b,b
      message:  The @model value should remain stable in willDestroy for all transition types
ember-source Result Notes
workspace:* (with fix) PASS Local build includes the bugfix
~6..11.0 FAIL Last version
~5.12.0 FAIL Last 5.x
~4.0.0 FAIL First 4.x
~3.28.0 FAIL Last 3.x

Cowritten by claude

Windvis and others added 4 commits March 8, 2026 01:08
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
NullVoxPopuli
NullVoxPopuli previously approved these changes Mar 8, 2026
named['controller'] = createConstRef(state.controller, '@controller');

// Create a ref for the model
let modelRef = childRefFromParts(outletRef, ['render', 'model']);
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.

I think something along this vein is the real fix:

Suggested change
let modelRef = childRefFromParts(named['controller'], ['render', 'model']);

The modelRef should only depend on the constant controller, not the variable reference to the controller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@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)?

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.

@NullVoxPopuli NullVoxPopuli dismissed their stale review March 10, 2026 17:31

Concerns over @model updating during same-route transitions

@johanrd
Copy link
Copy Markdown
Contributor Author

johanrd commented Mar 14, 2026

Concerns over @model updating during same-route transitions

@NullVoxPopuli tests are added for this now, but they could maybe be more specific?

@NullVoxPopuli NullVoxPopuli requested a review from ef4 March 14, 2026 15:42
@BoussonKarel
Copy link
Copy Markdown

We are also running into issues with @model being undefined during transitions.
We use template tags for all our routes, and thus we use @model for all our routes.

So this PR would be greatly appreciated :)

@NullVoxPopuli
Copy link
Copy Markdown
Contributor

NullVoxPopuli commented Mar 19, 2026

In a 2.7 million line app, we have 27818 tests... that passed

boring exploration, fumbling my package manager

As 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 🤔
lots of

Cannot read properties of undefined (reading 'lookup')

(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
update2: looks like ember-simple-auth problem -- perhaps they don't test against canary. time for a PR over there!
update3: pnpm bug is creating duplicate ember-source's so I have multiple get/set owner pairs :(
update4: got big app against ember-source@main passing -- will try this branch tomorrow

Copy link
Copy Markdown
Contributor

@NullVoxPopuli NullVoxPopuli left a comment

Choose a reason for hiding this comment

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

Unless someone can (with code) prove the fix is wrong, I think I'm in favor of this moving forward. The biggest-app-I-have-access-to's test suite passed on this PR, so at the least there is no regression.

Image

@johanrd
Copy link
Copy Markdown
Contributor Author

johanrd commented Mar 20, 2026

@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:

runTask(() => {
this.controllerFor('color').model = 'blue';
});
assert.equal(this.currentURL, '/yellow');
this.assertInnerHTML(strip`
[@model: yellow]
[this.model: blue]
[model: blue]
`);
}

@model becomes more in sync with this.model (both change to blue, even if the route url still is still yellow):

        actual: >
            [@model: blue][this.model: blue][model: blue]
        expected: >
            [@model: yellow][this.model: blue][model: blue]

The current premise is from the RFC:

Just like the "reflected" named arguments in classic components, mutating this.model on the controller instance would not change the value of @model. In practice, this seems unlikely to be relied upon and probably considered a bad practice (it does not change the URL, does not affect what is returned by route.modelFor, etc). In any case, this is consistent with the general behavior for named arguments, in that they are immutable and should always reflect what was "passed in" from the caller.

I am not sure what is the real fix in the long term – changing it may require RFC, though

@kategengler
Copy link
Copy Markdown
Member

I'd like @ef4 to re-review before any merge

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.

Route @model argument value timing issue

6 participants