Vue-router: Fetch route data from the component

Created on 29 Jul 2015  路  12Comments  路  Source: vuejs/vue-router

Currently, we use the data property of a route to have it fetch the data before it's resolved. This is how angular 1.x (and ui-router) have done it.

However, this makes the routes file extremely bloated, and splits the component's code into two disparate locations, making it hard to manage it.

Both Angular 2.0 and Aurelia have learned from this mistake and have changed it so that data is handled by the component. Their router shares a common heritage, so they both use the activate hook.

It would be really nice if we could move the data and lifecycle hooks to the components.

Most helpful comment

@yyx990803 Shouldn't the context of the component be available in the activate hook?

Here's some pseudo-code of what I'm trying to do:

{
    route: {
        activate() {
            return this.getStuff();
        },
    },

    data() {
        stuff: {},
    },

    methods: {
        getStuff(page = 1) {
            return globalGetStuff({ page }).then(stuff => {
                this.stuff = stuff;
            });
        },
        onPageChange(page) {
            this.getStuff(page);
        },
    },
};

But the activate hook is not called with the component's context. It's null:

Uncaught TypeError: Cannot read property 'getStuff' of null

All 12 comments

In addition to data fetching, it probably makes sense to move the entire route activation lifecycle into the component as well. (which is also what Angular2 / Aurelia routers are doing)

Proposed API:

// inside a component
module.exports = {
  route: {

    // every hook function gets a transition object as the only argument.
    // - {Route} transition.from
    // - {Route} transition.to
    // - {Function} transition.next
    // - {Function} transition.reject

    // three options:
    // 1. return a boolean
    // 2. return a promise that resolves to a boolean
    // 3. explicitly call transition.next() or reject()
    beforeActivate(transition) {
      if (transition.from.path === '/about') {
        transition.reject('Not allowed!')
      } else {
        transition.next()
      }
    },

    // for async data loading.
    // sets the component's "$loading" meta property to true when called,
    // and sets it to false when resolved.
    // two options:
    // 1. return a promise
    // 2. explicitly call transition.next() or reject(reason)
    activate(transition) {
      var params = {
        id: transition.to.params.messageId
      }
      // "this" is available
      // callback based
      this.messages.get(params, function (err, message) {
        if (err) {
          transition.reject(err)
        } else {
          transition.next({
            message: message
          })
        }
      })

      // or promise based (with ES6 sugar)
      return this.messages
        .get(params)
        .then(message => ({ message }))
    },

    // same deal with beforeActicate
    beforeDeactivate(transition) {
      // ...
    },

    // for doing cleanups
    deactivate(transition) {
      // ...
    }
  }
}

Lifecycle of the hooks follow the diagram here (at bottom of the page): https://angular.github.io/router/lifecycle

Yep. That's more or less what I had in mind.

Also, should there be a way to set a page title for any given route? Would be used as both the value of the entry in the browser's history as well as the document.title value.

Implemented. See example/components/inbox/index.vue & example/components/inbox/message.vue.

@yyx990803 what happens with the data in the returned promise?

Vue.component('user', {

    template: '...',

    route: {
        activate(transition) {
            const id = transition.to.params.userId

            return this.$http.get('/users/' + id).then(({ data: user }) => user);
        },
    },

});

How can I now access the user object from within the component, either to set it as part of its data, or in some method?

You can do one of the following:

  1. Return an object containing the key/value pair to be $set on the component. So in the promise callback:

js return this.$http .get('/users/' + id) .then(({ data: user }) => ({ user }))

The resolved value is { user: user }, so the router will call this.$set('user', user) for you.

  1. You can also directly set the data yourself (and return nothing in the promise callback):

js return this.$http .get('/users/' + id) .then(({ data: user }) => { this.user = user })

So what's the lifecycle here?

Is data called before route.activate? When is compiled called? What about ready?

By default, data and compiled are called before activate, because the component is already created and available as this inside the hook. ready is called after activate and its exact call moment depends on the component transition mode.

You can also use waitForActivate: true in the component's route config - this will defer the creation and insertion of the component until the activate hook is resolved.

This is all nice and dandy if you only need a single thing.

If you need multiple sets of data, this becomes quite clumsy:

route: {
    activate(transition) {
        const id = transition.to.params.userId

        const user = userService.get(id).then(user => {
            this.user = user;
        });

        const posts = postsService.getForUser(id).then(posts => {
            this.posts = posts;
        });

        // Not sure what would happen if the promise returns an array, so returning null here
        return Promise.all([user, posts]).then(() => { return null; });
    },
}

What if we could add a data property to the route to resolve multiple promises, like Angular 1.x's resolve property?

route: {
    data: {
        user: transition => userService.get(transition.to.params.userId),
        posts: transition => postsService.getForUser(transition.to.params.userId),
    }
}

And for bonus points, if someone wants to use a function, let them use that as well:

route: {
    data(transition) {
        const id = transition.to.params.userId;

        return {
            user: userService.get(id),
            posts: postsService.getForUser(id),
        };
    }
}

In both cases, the router would then wait for all promises to complete, and then set the appropriate data properties and transition to the route.

Some syntax sugar could be nice, but you can use the current version like this:

route: {
  activate(transition) {
    const id = transition.to.params.userId
    return Promise.all([
      userService.get(id),
      postsService.getForUser(id)
    ]).then(([user, post]) => ({user, post}))
  }
}

or

route: {
  activate(transition) {
    const id = transition.to.params.userId
    return Promise.all([
      userService.get(id),
      postsService.getForUser(id)
    ]).then(([user, post]) => {
      this.user = user
      this.post = post
    })
  }
}

@yyx990803 Shouldn't the context of the component be available in the activate hook?

Here's some pseudo-code of what I'm trying to do:

{
    route: {
        activate() {
            return this.getStuff();
        },
    },

    data() {
        stuff: {},
    },

    methods: {
        getStuff(page = 1) {
            return globalGetStuff({ page }).then(stuff => {
                this.stuff = stuff;
            });
        },
        onPageChange(page) {
            this.getStuff(page);
        },
    },
};

But the activate hook is not called with the component's context. It's null:

Uncaught TypeError: Cannot read property 'getStuff' of null

this is now available in activate as of 0.5.0

:+1:

Was this page helpful?
0 / 5 - 0 ratings