vue-router: Data is not set with beforeRouteEnter () before created () method is called

Vue.js / vue-router versions

2.1.10 / 2.2.0

Steps to reproduce

  1. Fetch some data via the beforeRouteEnter () method
data () {
    return {
        post: null
    }
},
beforeRouteEnter (to, from, next) {
    getPost(to.params.id, (err, post) => {
        if (err) {
            // display some global error message
            next(false)
        } else {
            next(vm => {
                console.log('next function')
                vm.post = post
            })
        }
    })
},
  1. Log the created () and mounted () method
created () {
    console.log('created:', this.post);
}, 

mounted () {
    console.log('mounted:', this.post);
}, 

What is Expected?

The data fetched during the beforeRouteEnter () method should be set before the created () and mounted () methods occur.

What is actually happening?

The data is null in the created () and mounted () methods.

The ‘created’ and ‘mounted’ console input will occur before the ‘next function’.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 33
  • Comments: 24 (4 by maintainers)

Most upvoted comments

This is actually expected, the callback is triggered after a nextTick that’s why you don’t have access to post in both hooks.

Edit: this is something we might not be able to change on v3 as it is a breaking change but it’s something we plan on improving for v4

I found a workaround that works for me:

  1. Define a base component for all components that need lazy loading (I called it BaseLazyLoadingComponent):
export default (loadData) => {
  let loaderCallback = () => {}
  const loadRoute = (to, from, next) => {
    loadData(to, (callback) => {
      loaderCallback = callback
      next()
    })
  }
  return {
    beforeRouteEnter: loadRoute,
    beforeRouteUpdate: loadRoute,
    created: function() {
      loaderCallback.apply(this)
    },
    watch: {
      '$route': function() {
        loaderCallback.apply(this)
      }
    }
  }
}
  1. Extend from this component for all components that need the beforeRouteEnter/Update lazy loading:
import BaseLazyLoadingComponent from './BaseLazyLoadingComponent.js'

export default {
  data () {
    return {
      value: null // will always be loaded before component is first rendered
    }
  },
  extends: BaseLazyLoadingComponent((to, callback) => {
    getData(/* whatever you call to get the data for the component */, response => {
      callback(function() {
        this.value = response.value
        // and so on, this is mapped to the vue component instance so you can assign all data
      })
    })
  })
}

It makes sense to me on why this isn’t triggered before created, but why does it have to trigger after mounted as well ?

It’s not an issue, the created hook gets called as soon as the component can be used, and the callback passed to next is called after that (a tick after I believe) and has to because setting the local variable wouldn’t have an effect if it was called before. This cannot change.

If you want to run some tasks that depend on the data fetched you should put that logic in a method and invoke the method from the callback passed to next:

export default {
  data() {
    return {
      post: null,
    }
  },

  beforeRouteEnter(to, from, next) {
    try {
      const post = await getPost(to.params.id)
      next(vm => {
        vm.post = post
        this.manipulateData()
      })
    } catch (err) {
      next(false)
    }
  },

  methods: {
    manipulateData() {
      // this was the code initially put in created
    }
  }
}

you can also use a watcher to invoke that method if you have a beforeRouteUpdate hook:

export default {
  data() {
    return {
      post: null,
    }
  },

  beforeRouteUpdate(to, from, next) {
    this.$options.beforeRouteEnter(to, from, next)
  },

  beforeRouteEnter(to, from, next) {
    try {
      const post = await getPost(to.params.id)
      next(vm => {
        vm.post = post
      })
    } catch (err) {
      next(false)
    }
  },

  watch: {
    post:  'manipulateData'
  },

  methods: {
    manipulateData() {
      // this was the code initially put in created
    }
  }
}

I hope this gives some guidance and clears things 🙂

I just came across this as well:

I solved it with a bit of a cheat – the component CANNOT be used in two separate places – using the following solution (which I chose not to continue with) but it demonstrates that setting data before mounting makes life so much easier:

<template>
  <div>
    <h2>Site: {{ site.name }}</h2>
  </div>
</template>

<script>
import { getSite } from 'api/sites'

let site

export default {

  data () {
    return {
      site
    }
  },

  async beforeRouteEnter (to, from, next) {
    site = await getSite(to.params.siteId)
    next()
  }
}
</script>

Vue / Vue Router really does need an elegant solution to this.

I saw this post, which is a cool solution:

But for now, what about passing any data directly into the created method ?

import { getSite } from 'api/sites'

export default {

  data () {
    return {
      site
    }
  },

  async beforeRouteEnter (to, from, next) {
    site = await getSite(to.params.siteId)
    next(site)
  },

  created (site) {
    this.site = site
  }

}

Or:

  async beforeRouteEnter (to, from, next, done) {
    site = await getSite(to.params.siteId)
    done(site)
    next()
  },

  created (site) {
    this.site = site
  }

Though digging around Vue’s source, I see that Vue’s hooks don’t support passing payloads.

So maybe set an option on the instance the created hooks run:

  created () {
    this.site = this.$options.async.site
    // or
    this.site = this.$route.async.site
  }

There really should be a low-effort way to capitalise on the async nature of beforeRouteEnter so fetched data is simply available during and after created and we can avoid the additional acrobatics 😦

I am confused about why this behaviour would be desirable.

In my scenario, parts of my component that are determined by the fetched data (e.g. a slug for a route and props for child components).

This means that errors occur as the data is null when the component is rendered

I second that the documentation at fetching-before-navigation then seems incorrect (and led me to this page).

@BenRomberg BRILLIANT!!! Works beautifully!!!

You have access in the callback (first and only argument) of the next function

On Tue, 13 Jun 2017, 17:20 Darren Segal, notifications@github.com wrote:

@posva https://github.com/posva So if you dont have access to data set in beforeRouteEnter() on the vue instance in mounted() and created() when exactly am I supposed to access them? Purely in beforeRouteEnter() and the template?

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/vuejs/vue-router/issues/1144#issuecomment-308151921, or mute the thread https://github.com/notifications/unsubscribe-auth/AAoicZwjwdwF-lps635FMqTk9eHcvO-vks5sDqiegaJpZM4L6H0a .