How to delay 'not found' error until data has loaded in Vue.js

Viewed 2934

I'm hitting this route: http://site.dev/person/1

And my component looks something like this:

// PeopleComponent.vue

<template>

<div>
   <template v-if="person == null">
       <b>Error, person does not exist.</b>
   </template>

   <template v-else>
       <pre> {{ person }} </pre>
   </template>
</div>

</template>

<script>
export default {


    mounted() {
      this.$store.dispatch('getPeople');
    }

    computed: {

      // represents the person whose id potentially matches the value of `/:id` in route.
      person(){ 
          let id = parseInt(this.$route.params.id)
          let person = null

          if(id > 0)
              person = this.$store.state.people.find(p => p.id == id)

          return person;
      }

    }



}
</script>

// [styles]

What I'm doing in this component:

I get an ID in my URL. The ID represents a specific resource being shown on this page. I have a person() computed property to retrieve the object whose ID matches the parameter in the URL from my Vuex store.

The desired outcome:

At the top of the page, I would like to display an error message if the object cannot be found (if say, for example, someone types an ID that doesn't exist in the array of objects in the store). If the item is found, a simple dump of the object is fine. This currently works, but the delay between fetching the data from the API and finding the right object in the store seems to be just long enough to briefly show the error message as if the object could not be found when the page first loads. When tested with a slower network speed, this error message is visible for several seconds. I want to eliminate it such that it does not appear at all if the object exists.

What I've tried:

  • v-cloak - tried applying this to every parent div, even the div#app itself, to no avail. I must be doing something wrong.
  • v-if - as seen above in the example

Some pointers would be appreciated, thank you!


Update 31/03/2020

As per Phil's suggestion, I tried to incorporate a flag to indicate when the page is ready. I did it in two different ways.

Method #1 Made the mounted() 'async' and added an await on the action that retrieves people from my API. Setting flag to true after it:

async mounted() { 

  await this.$store.dispatch('getPeople');
  this.loaded = true;

}

But I still see the error message, briefly.

Method #2

Use a then callback on the action, and set the flag to true inside the callback

mounted() {

  let vm = this

  vm.$store.dispatch('getPeople').then(() => {

      vm.loaded = true;

  })

}

Both methods don't stop the message from appearing.

I suspect that this is what's happening:

Core rule

Error should ONLY show if loaded=true and person=null

  1. Page loads, loaded is false and person is null. Error won't show.
  2. Page does a call to get people from API. Error still isn't showing.
  3. After the call, loaded has been set to true [At this point, loaded is true and person hasn't been resolved by the computed property yet, THIS is where I believe I'm seeing the error.]
  4. Computed property finds the relevant record from the store on the next tick.
  5. Person is no longer null, so the error disappears.

Edit 01/04/2020

Answering Phil's question: What does your getPeople() action look like?


getPeople({ commit })  {   
        axios.get('/get/people')
        .then(response => {
            commit("getPeople", response.data);
        })
        .catch(error => {
            console.log(error);
        })
},

3 Answers

Sounds like you need one more piece of state, for example loading.

Change your getPeople action to be composable, ie return the Axios Promise so that it waits for the async task to complete...

getPeople ({ commit })  {
  // make sure you return the promise
  return axios.get('/get/people').then(response => {
    commit("getPeople", response.data)
  }).catch(error => {
    console.error(error)
    // also make sure you don't accidentally convert this to a successful response
    return Promise.reject(error)
  })
},

then try something like this

export default {
  data: () => ({ loading: true }),
  async created () {
    // "created" fires earlier than "mounted"
    await this.$store.dispatch('getPeople')
    this.loading = false
  },
  computed: {
    // etc
  }
}

Now you can use loading in your template

<div v-if="loading">Loading...</div>
<div v-else>
  <!-- your "person" template stuff goes here -->
</div>

Note

Although this was my initial solution - Phil's answer seems to explain the problem best, and give a more suitable solution. So I'm still using my loader, but instead of a configured timer, I'm now simply waiting for the Promise.

In the end I realised I'm asking the site to wait for an indefinite amount of time until information is retrieved. In that time, something has to happen. I had to ask myself "well, what do you want displayed there before the resource is retrieved?" as this can't happen instantaneously... so I added spinner in.

The problem is the "indefinite" part, though. I don't know how long to display the spinner for. so I configured a global timeout variable.

So

1) If timeout hasn't been reached and resource is empty, display loader

2) If resource has loaded, show resource

3) If resource is empty and timeout has been reached, display error message

I'm pleased with this as a solution.

Related