vuejs / vue-router

🚦 The official router for Vue 2
http://v3.router.vuejs.org/
MIT License
18.99k stars 5.06k forks source link

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

Closed jasongerbes closed 7 years ago

jasongerbes commented 7 years ago

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 commented 7 years ago

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

jasongerbes commented 7 years ago

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 commented 7 years ago

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

philipithomas commented 7 years ago

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

HauntedSmores commented 7 years ago

@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 commented 7 years ago

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 .

Enchufadoo commented 7 years ago

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 commented 7 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

mazavr commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 commented 5 years ago

l hacked some code , so can no need set init data , just use async set data set pull request https://github.com/vuejs/vue-router/pull/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 commented 5 years ago

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 commented 4 years ago

@BenRomberg BRILLIANT!!! Works beautifully!!!

dmtkpv commented 2 years ago

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 commented 2 years ago

@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: https://github.com/vuejs/router/issues/454

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

jasonbodily commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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

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

Looking forward to v4 :)