ember.js icon indicating copy to clipboard operation
ember.js copied to clipboard

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

Open chriskrycho opened this issue 5 years ago • 12 comments

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:

https://github.com/emberjs/ember.js/blob/d96d9aad52e010da977d813f479494397ee53ec6/packages/%40ember/-internals/routing/lib/system/route.ts#L2495-L2512

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:

https://github.com/emberjs/ember.js/blob/d96d9aad52e010da977d813f479494397ee53ec6/packages/%40ember/-internals/routing/lib/system/router.ts#L902-L904

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. :grimacing:

chriskrycho avatar Jan 14 '20 16:01 chriskrycho

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): https://github.com/emberjs/ember.js/blob/c25092965d266a28b72ddb1b6a6b96839a9c5758/packages/%40ember/-internals/routing/lib/system/route.ts#L2584-L2585

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 avatar Jan 15 '20 17:01 aaxelb

Looks quite similar to https://github.com/emberjs/ember.js/issues/18268

acorncom avatar Jan 18 '20 19:01 acorncom

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.

RobbieTheWagner avatar Jan 22 '20 17:01 RobbieTheWagner

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

jakebixbyavalara avatar Jan 27 '20 19:01 jakebixbyavalara

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

pzuraq avatar Jan 27 '20 19:01 pzuraq

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.

jakebixbyavalara avatar Jan 27 '20 19:01 jakebixbyavalara

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

RobbieTheWagner avatar Jan 27 '20 20:01 RobbieTheWagner

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

richgt avatar Jul 16 '20 16:07 richgt

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

aaxelb avatar Jul 17 '20 13:07 aaxelb

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

richgt avatar Jul 17 '20 14:07 richgt

@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

sly7-7 avatar Mar 06 '21 12:03 sly7-7

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

rwjblue avatar Mar 09 '21 02:03 rwjblue