Skip to content

Data is not set with beforeRouteEnter () before created () method is called #1144

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

Closed
jasongerbes opened this issue Feb 7, 2017 · 24 comments
Closed

Comments

@jasongerbes
Copy link

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

@posva
Copy link
Member

posva commented Feb 7, 2017

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

@posva posva closed this as completed Feb 7, 2017
@jasongerbes
Copy link
Author

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

@entr
Copy link

entr commented May 13, 2017

I'm also confused! Does https://router.vuejs.org/en/advanced/data-fetching.html#fetching-before-navigation need updating then?

@philipithomas
Copy link

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

@HauntedSmores
Copy link

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

@posva
Copy link
Member

posva commented Jun 13, 2017 via email

@Enchufadoo
Copy link

Enchufadoo commented Jun 27, 2017

I'm lost here

Before, in Vue 1 you could do something like this

route: {
        waitForData: true,
        data(transition) {
                someAjaxRequest().done(function(){
                    transition.next();    
                })
            }
        }
    },

You could start your component with the data ready.

But now in Vue 2 is there a way to achieve this? what's the utility of beforeRouteEnter if you can't set the data in the component before the lifecycles go on?

I'm asking out of pure ignorance, this component architecture things go over my head.

@BenRomberg
Copy link

BenRomberg commented Aug 19, 2017

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
      })
    })
  })
}

@zawilliams
Copy link

Hey @posva - it does seem like there is a possible bug here unless I'm not understanding the callback in the next() method.

I've put together a Fiddle to reproduce the issue: https://jsfiddle.net/gc3xd1oL/18/

Pull up dev tools, watch the console and click on the "Home component" link. You'll see the created() method gets hit first with a null value for this.name, followed by the callback method being called within the beforeRouteEnter() method, followed by another console log showing the data assignment does take place with a value of "Zach" shown in the log.

Am I doing everything correctly, or is there a bug?

@zawilliams
Copy link

zawilliams commented Mar 19, 2018

@posva - would you mind taking a look at my reproduction of the issue? If this is an actual issue I can take a look and see if I can get a PR submitted. Was just having a hard time trying to track down where this is happening in the vue-router code.

@posva
Copy link
Member

posva commented Mar 22, 2018

@zawilliams That is working as expected, the created hook is triggered before next callback

@mazavr
Copy link

mazavr commented Apr 2, 2018

I assume as well to have:

  1. get data insile beforeRouteEnter and define next callback with vm parameter. Operate data in the callback and change component(vm) data as well
  2. have an access to the changed data inside component created hook

Will it be changed or it will stay the same as it is?

@VersBinarii
Copy link

What is the current status of this? It still seem to be an issue with Vue 2.5.2 and Vue-Router 3.0.1

@posva
Copy link
Member

posva commented Aug 8, 2018

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 🙂

@ghost
Copy link

ghost commented Aug 27, 2018

Sorry, but as I stated here about this issue, I still can't fetch data. They still result null. Tried with @BenRomberg solution, and by refactoring code like @posva example too, with no success.

In short, I'm trying to fetch data from DB, in order to initialize a third-party Vue framework component for a Laravel project. Ajax call goes good, but seems that the vm callback instance being ignored and property are not updated, still remains as initially declared (as null). Simply, I would that this technique works.
Actually I'm still stuck on this and I can't continue until this will be solved.

EDIT:

Problem solved. The issue was caused by a misconfiguration of the third-party component itself.

@CoolGoose
Copy link

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

@miewx
Copy link

miewx commented Dec 2, 2018

l hacked some code , so can no need set init data , just use async set data
set pull request
#2512

use like this


export default {
    beforeRouteEnter:(from, to, next)->
        li = (await $.get '/api/li.txt').split('\n').filter(Boolean)
        @data = ->
            {
                n:0
                now:1
                li
                src : [
                    li[0]
                    li[1]
                ]
            }
        next()

@davestewart
Copy link

davestewart commented Dec 20, 2018

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 :(

@kirkbushell
Copy link

@BenRomberg BRILLIANT!!! Works beautifully!!!

@dmtkpv
Copy link

dmtkpv commented Jan 7, 2022

The following works with vue 3 and vue-router 4

route.vue

export default {

    async preload (to, from) {
        const user = await axios.get('/users/' + to.params.user);
        const posts = await axios.get('/posts');
        return { user, posts }
    },

    created () {
        console.log(this.$route.preload) // { user: {...}, posts: [...] }
    }

}

router.js

router.beforeEach((to, from, next) => {

   // get all matched components
   const components = to.matched.map(route => Object.values(route.components)).flat(); 
   
   // execute "preload" (if exists)
   const requests = components.map(component => component.preload?.(to, from)); 
   
   // merge all results into a single object
   const onSuccess = data => to.preload = data.reduce((result, item) => ({ ...result, ...item }), {});
   
   // handle error
   const onError = error => {}

   // requests promise
   Promise.all(requests).then(onSuccess).catch(onError).finally(next);

})

@jasonbodily
Copy link

jasonbodily commented Jun 24, 2022

@BenRomberg How does your solution work given the latest Vue 3 ignores beforeRouteEnter and beforeRouteUpdate on both mixins and extends? I can't get your solution to work because the guards aren't triggered.

Adding to previous comments, I'm perplexed that the ability to mixin/extend for route-level guards has been dismissed out of hand in Vue 3. I have tried 3x to bring our moderately sized app up to Vue 3 but each time I'm stopped because we leverage this feature rather heavily. Two heavily commented-on threads address this now, including this one: vuejs/router#454

EDIT
Just realized this is Vue 2. Still a problem in Vue 3 except exacerbated as explained above.

@jasonbodily
Copy link

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

Looking forward to v4 :)

@boogiefromzk
Copy link

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

Yes, the best would be an analog of serverPrefetch for client: clientPrefetch, which will WAIT until awaits inside are completed like it's server analog does.

@Yuiyis
Copy link

Yuiyis commented Feb 9, 2023

这实际上是预期的,回调是在 a 之后触发的nextTick,这就是为什么您无法访问post两个挂钩的原因。

编辑:这是我们可能无法在 v3 上更改的内容,因为它是一个重大更改,但我们计划在 v4 中改进它

Looking forward to v4 :)

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

No branches or pull requests