Flow Router doesn't render on a route change

Hey guys,
I’ve just moved for my new project from Iron:Router to FlowRouter and now having some trouble with one of my routes. I’ve defined a “catch all” route, because for some SEO reason I want to have the title of a question directly behind the url.

This is my defined route:

FlowRouter.route('/:id/', {
action: function(params, queryParams) {
    BlazeLayout.render('layout', { site:"question" });
}
});

So f.e. I open a question by “myapp.com/how-late-is-it”. There is a button where I can move to the next question, so the next route will be f.e. “myapp.com/whats-your-name”. But here nothings happens, the template isn’t destroyed and there is no onRendered() called again. I know that there are some differences how Iron:Router and FlowRouter handle things, but how can I tell FlowRouter that the route has changed? When the user clicks “next question” this function is called:

FlowRouter.go("/" + newQuestion + "/");

The url in the browser bar changes but the template and title remain the same.

If I understood the docs right, I’ve to call FlowRouter.reload();. But this is also not working.

 FlowRouter.go("/" + newQuestion + "/");
 FlowRouter.reload();

//Edit: Answered here. I’ve added the subscription into an autorun, works now fine. @sashko Is this issue a topic in the current Meteor guide?

Template.question.viewmodel({

subReady: false,

dbObject: {},

flowId() {
    return FlowRouter.getParam("id");
},

answers() {
    return Answers.find({questionId: this.flowId()}, {sort: {votesUp: -1, createdAt: 1}});
},


onRendered() {
  //Normally everything in the autorun function is here but FlowRouter doesn't do a rerender if you visit the same route with other parameters. 
},


autorun: [

    function () {

        this.templateInstance.subscribe("question", this.flowId(), () => {

            DocHead.removeDocHeadAddedTags();
            this.subReady(true);
            this.dbObject(Questions.findOne({id: this.flowId()}));
            DocHead.setTitle(this.dbObject().title + " - myapp");

             DocHead.addMeta({name: 'description', content: this.dbObject().desc.substr(0, 80)});
        });


    }

]


});

I really get that feeling that we do easy things more complex only to get “a little bit” better performance…

1 Like

FlowRouter.watchPathChange() is what you’re looking for, right? :sunny: It will reactively rerun any autorun it resides within on URL-change. [docs here] (https://github.com/kadirahq/flow-router)

I’ve already added an autorun, see the example above. It is about the design of FlowRouter, a change of a routing parameter will not destroy the template.

I’m not into viewmodel, so can’t say that I understand what’s going on above :smile: But if everything’s working - great!

Regarding the design of Flow-Router, I think the behaviour you’re describing makes perfect sense. with a route like:

FlowRouter.route('/:id/', {
    ...
})

you’re technically not hitting a new route when you change your params - you’re staying on the same one - which means no rerender :sunny: If you want to rerender, you have to explicitly make routes for individual templates, or reload like you’re doing now.

2 Likes

Yeah, thats a point of view discussion. Google’s spider wouldn’t think that “/hello” and “/about” are the sames routes, also the browser wouldn’t do. I’m coming from PHP, so it’s normally for me that a catch all route will end in a rerender of the sites and the behavior of FlowRouter here feels a little bit different for me. On the one side I’m calling the same route, because FlowRouter is always calling this “catch all” route and so it’s the same, on the other side it isn’t the same.

The root of the problem is single page applications. When you suddenly flip the script on how to render HTML by bundling up all of it into a single HTML-file with javascript and the works, you suddenly have to deal with issues that didn’t exist before if you want crawlers and browsers to behave nicely with your app.

With traditional server side rendered forms-navigation and multiple html pages, everything’s great, simply because that’s how it’s been done for decades. submit a form = new url = new server request = new html

Having an SPA imitate traditional browser navigation CAN come with the cost of weird interaction with crawlers and browsers. You’ll need to do some extra work to achieve the same interactions…

Mozilla has provided a nice article about the history API :sunny: pushState and replaceState() are the holy grails of SPA routing AFAIK.

3 Likes

Yes, but this issue is not existant with the iron router. I recently moved from iron router to flow router and run into a similar issue: https://github.com/kadirahq/blaze-layout/issues/65

As I pointed out in the discussion, I think it’s because here ( https://github.com/kadirahq/blaze-layout/blob/master/lib/client/layout.js#L70-L74 ) no attention is paid to the value of the query parameter. I’ve found it quite annoying and then mover to React and used another template renderer that works fine.

2 Likes

With Flow Router, params and queryParams are not reactive by default. It was designed this way to get rid of some annoying behaviours of Iron Router, but it creates other annoying behaviours this way.

I agree that the issue you have is typical to single page applications in general. I had to deal with it with Angular too, before I moved to Meteor.

Now that’s interesting.

How I deal with that is I keep the state at the app’s level (while showing proper params and queryParams for the user to be able to share the link with others). This way subscriptions and methods rerun when my app’s state changes, not my routing.

1 Like

Very interesting! Could you expand on that? Some example?

@avalanche1 here’s an example. It may be an overkill, but I wrote it as an experiment and doesn’t work bad so far. Also, it may be a bad practice to reset queryParam value when it’s equal to default, who knows? I prefer it that way.

External means that you use this function in your template. Internal means that this function is only used internally so the template doesn’t have to address it in any way.

Data template is a template responsible for keeping the data for the particular view component. Each such view component may have different default settings, that’s why we allow the template to override them.

Query field template is a template responsible for managing a particular form field which I use to change the value of query properties - it may be a select field, a checkbox or whatever. @param() is a string representation of the property’s name, set in the template, @value() is the value of form field.

ViewModel.share
  query:
    filter: ''
    sort: 'createdAt'
    order: '-1'
    limit: 10

ViewModel.mixin

  #injected into data templates
  prepareQuery:

  #external

    #overwrites defaults and sets initial property values
    #called in onCreated
    getQueryParams: (sort, order, limit) ->
      @defaultQueryParams().sort = sort
      @defaultQueryParams().order = order
      @defaultQueryParams().limit = limit

    #passes values of queryParams to properties
    #called in onCreated
    checkQueryParams: ->
      @filter @checkQueryParam 'filter'
      @sort @checkQueryParam 'sort'
      @order @checkQueryParam 'order'
      @limit @checkQueryParam 'limit'

  #internal

    #sets queryParam value or resets it when default
    setQueryParam: (param, value) ->
      unless value is @defaultQueryParams()[param]
        FlowRouter.setQueryParams 
          "#{param}": value
      else
        FlowRouter.setQueryParams 
          "#{param}": null

    #sets default values for properties
    defaultQueryParams: ->
      filter: ''
      sort: 'createdAt'
      order: '-1'
      limit: 10

    #returns queryParam value if set or default if not
    checkQueryParam: (param) ->
      if FlowRouter.getQueryParam param
        FlowRouter.getQueryParam param
      else
        @defaultQueryParams()[param]

  #injected into query field templates
  queryField:

  #internal

    #sets queryParam and property value every time the field's value changes
    autorun: (c) ->
      @setQueryParam @param(), @value()
      @parent()[@param()] @value()

I used this opportunity to do some tweaks that previously I was too lazy to do. But now it’s ready.

I simply can’t seem to find a solution that reliably works every time for a particular scenario. I’m running Meteor 1.6.0.1 with FlowRouter. Specifically, when the page reloads after the route change, I want to ‘update’ an iCheck checkbox using jQuery. I have that checkbox in it’s own template:

<template name="activeCheck">
    <div class="i-checks">
        <label>
            <input name="activeCheck" type="checkbox" value="1" checked="{{isChecked active true}}">
            Active (Test: {{active}})
        </label>
    </div>
</template>

I do get the underlying checkbox updated when the route changes, but I can’t find an event that consistently triggers every time on the route change. Specifically:

Tracker.afterFlush(function() {
    console.log('afterFlush')
    $('input[name="activeCheck"]').iCheck('update')
})

Does NOT fire on every route change.

this.autorun(function () {
    console.log('autorun')
    $('input[name="activeCheck"]').iCheck('update')}, 2000)
});

DOES fire on every route change, but a race condition appears to exist such that I am updating the iCheck checkbox before the DOM has had a chance to redraw?

After testing every combo of autorun and afterFlush, I have the following code which works, but is less than ideal by injecting a 2 second pause to give the DOM time to update before running the jQuey iCheck update routine.

Tracker.autorun(function () {
    /* Todo there appears to be a race condition. This is a less than ideal fix that appears to work */
    FlowRouter.watchPathChange()
    setTimeout(function() {$('input[name="activeCheck"]').iCheck('update')}, 2000)
});

Any ideas as to how to get around this? @dr.dimitru might you have some insight into whether flow-router-extra might fix or address this?

Hello,

Thank you for pinging me.

  1. Have you tried to use .onRendered() hook?
  2. Could you check if .onRendered() hook fires when path is changed?
  3. How do you update path?

Yes, I tried .onRendered() for both the main template as well as the checkbox template.

However, I just discovered a pattern that had eluded me before: if I navigate to a different route, .onRendered() does fire, but if I navigate back to a route I’d already visited, the DOM does update, but .onRendered() does NOT fire.

In other words:

FlowRouter.go('/doc/1') <= Initial load, .onRendered() fires

FlowRouter.go('/doc/2') <= DOM updates & .onRendered() fires

FlowRouter.go('/doc/1') <= DOM updates & .onRendered() DOES NOT fire

FlowRouter.go('/doc/3') <= DOM updates & .onRendered() fires

FlowRouter.go('/doc/2') <= DOM updates & .onRendered() DOES NOT fire

@cormip need more info from you:

  • Are you on original flow-router or on one of its forks?
  • Version of flow-router you’re experiencing this issue
  • Version of Meteor you’re experiencing this issue
  • Browser name and its version (Chrome, Firefox, Safari, etc.)?
  • Platform name and its version (Win, Mac, Linux)?

@dr.dimitru, original: kadira:flow-router@2.12.1

Meteor 1.6.0.1

Chrome: Version 64.0.3282.119 (Official Build) (64-bit)

Development: Win10

Deployed to Galaxy where the problem is also observed.

This issue might be fixed using flow-router-extra as drop-in replacement (except it should be imported).

I replaced kadira:flow-router with ostrio:flow-router-extra and it did not solve the problem. :disappointed: I verified that onRendered still does not fire when revisiting a previously visited route that uses the same template as I described earlier.

My guess it’s a feature. Did action hook fire on path change?

Yes, the action hook fires every time the path changes, the same as autorun. However, I cannot use that hook since it creates a race condition between my code to render the checkbox, and the DOM being updated by the data change.

It does not seem likely that this is a feature since the DOM does update on every path change. I even added the checkbox status to the checkbox label as shown in a previous comment, and I can see that the DOM is updated on every path change. There is no reason why Tracker.afterFlush shouldn’t run. That’s the event that is needed and which should fire after the DOM has been updated.

My guess it’s BlazeLayout feature to not rerender tempates, but insted update it’s data. As I understood /doc/1 and /doc/x are using the same template, right? So, only data is updated.

Try to use this.render() method of flow-router-extra.