Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.6+] Query Params behavior changes with RouterService when refreshModel is true #18683

Open
chriskrycho opened this issue Jan 14, 2020 · 12 comments

Comments

@chriskrycho
Copy link
Contributor

chriskrycho commented Jan 14, 2020

While upgrading our application from Ember 3.4 to 3.8, we discovered that the behavior of query params in the URL changes dramatically depending on whether any of the query params have refreshModel: true set on the route. I created a sample example reproduction in this repo.

When query params do not have refreshModel: true set, using RouterService#transitionTo ultimately results in query params being removed from the URL once the target route resolves. However, if refreshModel: true is set for any of the query params, the query params remain in the URL once the target route resolves. While in principle this should always still work in a well-behaved application, it is definitely not desirable behavior (because not all applications are well behaved here!).

The issue is surfaced because the presence of refreshModel leads Route#queryParamsDidChange to invoke Route#refresh:

queryParamsDidChange(this: Route, changed: {}, _totalPresent: unknown, removed: {}) {
let qpMap = get(this, '_qp').map;
let totalChanged = Object.keys(changed).concat(Object.keys(removed));
for (let i = 0; i < totalChanged.length; ++i) {
let qp = qpMap[totalChanged[i]];
if (
qp &&
get(this._optionsForQueryParam(qp), 'refreshModel') &&
this._router.currentState
) {
this.refresh();
break;
}
}
return true;
},

However, this is not the actual cause, just the surfacing symptom. The actual cause is that queryParamsDidChange is itself invoked with incorrect parameters. (It surfaces the issue clearly because invoking Route#refresh creates a new Transition, which carries along those query params.)

The call stack is:

  • queryParamsDidChange (router.js)
  • triggerEvent with event named "queryParamsDidChange" (router.js)
  • triggerEvent with event named "queryParamsDidChange" (router.js) – this is not a mistake, there are two layers of event triggering
  • fireQueryParamsDidChange (router_js.js)
  • queryParamsTransition (router_js.js)
  • getTransitionByIntent (router_js.js)

The getTransitionByIntent method calculates a queryParamChangelist which is then supplied to the rest of the chain, but has insufficient information to correctly generate that queryParamChangelist: it only knows the actual previous state of the query params for the route and the new query params… but (correctly) has no knowledge of Ember's Controller query params special handling.

This does not fail in the non-router-service case because when the transition comes from a route or a controller, _prepareQueryParams (again, correctly, per the RFC) prunes default query params from the list of query params—it only skips that operation when the transition comes from the router service:

if (!_fromRouterService) {
this._pruneDefaultQueryParamValues(state.routeInfos, queryParams);
}

Thus, the list of query params passed on down the chain still includes all values, whether or not they are defaults… so what ends up being checked by the the router microlib for different values includes those values.

At first blush, none of the changes we could make in this space are obvious winners, or even particularly good. 😬

@aaxelb
Copy link
Contributor

aaxelb commented Jan 15, 2020

Hey good news, this is more complicated! The same buggy behavior shows up when refreshModel is false for all QPs, but only when transitioning to another route, instead of changing QP values on the current route, as in the repro above. (This seems like the same issue to me, but let me know if it's actually different enough to report separately.)

A plain router.transitionTo('another-route') (again, without refreshModel: true) results in all QPs' default values serialized to the URL. (Repro here)

Best i can tell, this is caused by over-applying the "don't strip default values" rule established in the RFC -- Route#finalizeQueryParamChange explicitly keeps default values in the URL for transitions that were originally triggered by RouterService (by checking transition._keepDefaultQueryParamValues):

let thisQueryParamHasDefaultValue = qp.serializedDefaultValue === svalue;
if (!thisQueryParamHasDefaultValue || (transition as any)._keepDefaultQueryParamValues) {

While it is important not to instantiate controllers unnecessarily, all the relevant controllers are already there by that point in Route#finalizeQueryParamChange and, in fact, the comparison to the default value has already happened one line prior.

This prompts me to wonder if the RFC was mis-interpreted. There are tests that enforce default values are still serialized to the URL post-transition -- is that what we actually want?


edit: adding a gif of the repro above in action (and added a description to the repro readme):
router-service-qps

aaxelb added a commit to aaxelb/ember-osf-web that referenced this issue Jan 17, 2020
@acorncom
Copy link
Contributor

Looks quite similar to #18268

@RobbieTheWagner
Copy link
Member

Unsure if this is the same issue we are seeing as well, but we're trying to update from Ember 3.12 to 3.14 and we're seeing some weirdness where queryParams now seem to get reset when calling this.refresh() in the route.

@jakebixbyavalara
Copy link

jakebixbyavalara commented Jan 27, 2020

@rwwagner90 Was also seeing some issues with queryParams upgrading from 3.12 to 3.13.
Basically, params being removed when another one updates. For instance, visiting a page with queryParams in the url: http://my.url?param1=foo&param2=bar, (both params having null as their default values) then changing a third param param3 via an action in the controller, we were seeing param1 and param2 being removed (and listed in the removed params with queryParamsDidChange) and replaced with param3. I'm going to see if I have time to work on a repro case/app this week, I'd be interested to include the refresh behavior you're seeing in it as well.

@pzuraq
Copy link
Contributor

pzuraq commented Jan 27, 2020

@rwwagner90 @jakebixbyavalara can you open that up as a separate issue? It's likely related to the changes introduced in 3.13, which would be unrelated to this issue since it has been occuring since 3.6.

@jakebixbyavalara
Copy link

Oh, definitely was going to open another issue, I just noticed that @rwwagner90 was mentioning 3.12 to 3.14 which I would assume is related to my issue and not this one.

@RobbieTheWagner
Copy link
Member

@jakebixbyavalara can you link to the issue after you open it? Definitely think 3.13 was what broke some thing for us.

@richgt
Copy link
Contributor

richgt commented Jul 16, 2020

@aaxelb / @chriskrycho - I'm looking into this issue now. @aaxelb - I'm using your reproduction repository, but the tests don't seem to be failing where I expect. Is there a test in that repo (https://github.com/aaxelb/repro-ember-router-service-qp) that shows the failing issue? Or is there a specific step in the app I'm supposed to take? Pretty new to investigating Ember bugs (as in, this is my first), so any details are appreciated.

@aaxelb
Copy link
Contributor

aaxelb commented Jul 17, 2020

@richgt ah sorry, there aren't tests, it's an interactive repro showing the problem when refreshModel is false:

here's a video demonstrating:
router-service-qps

  • the application controller has a query param foo with default value 'defaultFoo' -- foo never changes value and has refreshModel: false by default
  • when transitioning via link-to, foo is not serialized to the URL (correctly, imo)
  • when transitioning via the router service (to a different route), ?foo=defaultFoo shows up in the URL (incorrectly, imo)

i didn't write a test because ember's tests currently enforce the "incorrect" behavior, and maybe i'm wrong about how it should work?

@richgt
Copy link
Contributor

richgt commented Jul 17, 2020

@aaxelb - perfect. thanks for the details. I think in general, it would be helpful for stuff like this to be in the README so that as folks come in to look at the issue, they know exactly how to repro :-D.

@sly7-7
Copy link
Contributor

sly7-7 commented Mar 6, 2021

@richgt @aaxelb I've tested this repo against ember 3.25.1, and this behavior is still here. There is definitely something inconsistent with the service, because using (the now deprecated transitionToRoute()) from the controller, it has the same behavior as link-to. And since it's a default value never updated, I have the same feeling that the qp should not be updated.

Reading through the code, I felt on https://github.com/emberjs/rfcs/blob/master/text/0095-router-service.md#query-parameter-semantics, so the behavior seems expected. Would that mean the other behaviors are wrong and should be fixed to make things consistent ? (the usual transitionTo are deprecated after all...)

cc/ @rwjblue @chancancode

@rwjblue
Copy link
Member

rwjblue commented Mar 9, 2021

Yes, I think the older behaviors should be deprecated if we can figure out how to do it reasonably.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants