Nuxt async fetch() creating multiple instances? Repeated fetch() calls - vue.js

I have a simple BasePreviewImage component that needs to fetch an Array.Buffer() asynchronously from an internal API. However, it appears that async fetch() is called for every instance ever created despite the components themselves being destroyed.
Example:
<template>
<div class="image-container">
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
#Component({})
export default class BasePreviewImage extends Vue {
#Prop({ type: String }) id: string
#Prop({ type: String, default: 'small' }) size: string
image: string = ''
async fetch() {
console.log('async fetch', this)
}
created() {
console.log('created')
}
mounted() {
console.log('mounted')
}
beforeDestroy() {
console.log('beforeDestroy')
}
}
</script>
Output when I load a page with 1 BasePreviewImage component then back, then re-open the page. This continues calling fetch n times the page has been opened.
How do I avoid making the API call multiple times as a user navigates pages and is there some other memory leak going on here?
I'm not really sure if the problem is code, config, vue, nuxt, nuxt-property-decorator, vue-class-component, or somewhere else.
Related but not helpful: https://github.com/vuejs/vue-class-component/issues/418

Related

Vue JS best approach to wait for value in mounted hook

In my Vue component I want to fetch work orders via axios. To do that, I need a user id, which is loaded via a computed property.
Just calling my method 'loadWorkorders' in the mounted lifecycle hook, results in the computed property not being available yet. So I want to check if this computed property is available, if not, just wait a few milliseconds and try again. Repeat the same cycle until it is available.
What is the best approach to handle this? Now I'm just waiting for a second and this seems to work, but I'm looking for a more robust approach.
My code:
export default {
name: "MyWorkorders",
mounted() {
if (this.user) {
this.loadWorkorders();
} else {
setTimeout(this.loadWorkorders(), 1000);
}
},
data() {
return {
workOrders: null,
};
},
methods: {
loadWorkorders() {
axios
.get(`/workorders/load-user-workorders/${this.user.id}`)
.then((res) => {
this.workOrders = res.data.workorders;
})
.catch((err) => {
console.log(err);
});
},
},
computed: {
user() {
return this.$store.getters.getUser;
},
},
};
If you are using Vue 3, consider using Async Components, which are also related to the Suspense built-in component. (at the time of this post, Suspense is an experimental feature, however Async Components are stable)
If you are using Vue 2, take a look at this part of the documentation about handling loading state in Async Components.
If you do not want to invest in these solutions because your use case is very, very simple -- you can try the following simple solution:
<template>
<template v-if="loading">
loading...
</template>
<template v-else-if="loaded">
<!-- your content here -->
</template>
<template v-else-if="error">
error
</template>
</template>
I have made this runnable example for demonstration. Of course, the templates for loading and error states can be made more complex if required.

Paginated async Component doesn't trigger setup() on route change

I have a paginated component. The async setup() method is pulling data from an API to populate the page. It works fine when the route is directly loaded, but when I change the route to a different page slug (eg. clicking a router-link), the component is not reloaded and setup is not executed again to fetch the new data.
I guess I somehow want to force reloading the component?
This is my MainApp component it has the router view and fallback.
<router-view v-slot="{ Component }">
<Suspense>
<template #default>
<component :is="Component" />
</template>
<template #fallback>
loading...
</template>
</Suspense>
</router-view>
The router looks kinda like that. You see the page component takes a page_slug:
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "",
component: MainApp,
children: [
{
name: "page",
path: "page/:page_slug",
component: Page,
props: true,
},
// [...]
]
}
And this is how my Page component looks like. It uses the page_slug to load data from an API which is then used in the template:
<template>
<div> {{ pageData }} </div>
</template>
export default defineComponent({
name: "Page",
props: {
page_slug: {
type: String,
required: true,
},
},
async setup(props) {
const pageData = await store.dispatch("getPageData", {
page_slug: props.page_slug
});
return { pageData }
}
}
When I directly open the route, the fallback "loading..." is nicely shown until the data is returned and the component is rendered.
But when I do a route change to another page, then async setup() is not executed again. In that case the url in the browser updates, but the data just remains the same.
How can I solve this case? Do I have to force reload the component somehow? Or have an entirely different architecture to the data loading?
The answer is simple, when trying to create Vue 3 Single File Components (SFCs) in Composition API way as shown below:
<template>
<!-- Your HTML code-->
</template>
<script>
export default {
name: 'ComponentName',
async setup():{
// Your code
}
};
</script>
<style>
/*Your Style Code*/
</style>
<script>, will only executes once when the component is first imported. So, when the data have changed by other component, the component above will not updated or in other words not re-created.
To make your component re-created whenever it about to mount, you have to use <script setup> which will make sure the code inside will execute every time an instance of the component is created, but you need to re-write your script code with few changes in comparison when using setup() method, and also you are able to use both of scripts like this:
<script>
// normal <script>, executed in module scope (only once)
runSideEffectOnce()
// declare additional options
export default {
name: "ComponentName",
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// executed in setup() scope (for each instance)
</script>
Read this documentation carefully to have full idea.

Vue trigger mounted before async created completed

VueJS fires mounted before async created done.
The base html is rendered by SSR (Laravel/PHP) for SEO and users. So, it is desirable to complete this.fetchCakeShops() and variable this.cakeShops is filled before mount id="cake-shop-list" to virtual DOM. (Otherwise, the screen become blank until this.cakeShops is set) Therefore I wrote code like bellow.
<!-- Base html -->
<div id="cake-shop-list">
<!-- Already server side rendered cake shops list -->
<ul><li>shopA</li><li>shopB</li></lu>
</div>
<template>
<div>
<ul>
<li v-for="shop in cakeShops" :key="shop.id" :click="onClickShop(shop.id)">{{shop.name}}</li>
</ul>
</div>
</template>
<script>
export default {
...
async created() {
console.log("created start!")
this.cakeShops = await this.fetchCakeShops({page: 1})
console.log("created ends!", this.cakeShops)
},
mounted() {
console.log("mounted!")
},
methods: {
...
}
}
</script>
I wish mounted hook starts after created. But the log console shows like bellow, and the DOM is override with blank virtual DOM in a few seconds.
created start!
mounted! <- Oops
created ends!
Array(10)[{id: 1, name: 'shopA'},{id: 2, 'shopB'}...]
Is there any solution to wait mount until async is complete?
note: I can not pass shops array to props.
The mounted method will not wait untill the created method is done (event if its async) instead what you could do is create a different function and call this at the end of your create function
this will look something like this:
async created() {
this.cakeShops = await this.fetchCakeShops({page: 1})
this.loaded();
},
methods: {
loaded: {
//code here
},
}

Nuxt fetch() called repeatedly, multiple times

I am creating a base component that receives in image from an Array.Buffer, asynchronously.
Following Nuxt documentation, I thought fetch() would be the right call but when it's implemented it is called +20 times.
Why does this happen? I wanted to take advantage of async/await but looks like for now created() promises will have to do?
<template>
<div class="image-container">
<!-- <img :class="imgClasses" :src="`data:image/png;base64, ${image}`" alt="Preview Image" /> -->
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from 'nuxt-property-decorator'
#Component
export default class BasePreviewImage extends Vue {
#Prop({ type: String }) id: string
#Prop({ type: String, default: 'small' }) size: string
image: string = ''
get imgClasses() {
return {
[`preview-image-${this.size}`]: true
}
}
async fetch() {
console.log('fetching')
}
mounted() {
console.log('mounted')
}
created() {
console.log('Created BasePreviewImage')
}
}
fetch is called on server-side when rendering the route, and on client-side when navigating. so there is a chance to call/revoke the fetch() multiple times. and may be you have used this component at multiple places too. but the important things fetch() is deprecated where Nuxt >= 2.12
but you can control this way fetchOnServer: false then it will call on client side only.
<script>
export default {
data () {
return {
posts: []
}
},
async fetch () {
this.posts = await this.$http.$get('https://jsonplaceholder.typicode.com/posts')
},
fetchOnServer: false
}
</script>

Undefined variable - While api fetch | Axios | Props

My main component - Home
A really simple component, I pass the fetch variable to another component.
<template>
<Page actionBarHidden="true">
<ComponentA :api="api.somevariable"></ComponentA>
</Page>
</template>
<script>
import axios from "axios";
import ComponentA from "./ComponentA.vue";
export default {
data() {
return {
isLoading: false,
result: []
};
},
components: {
ComponentA,
},
created() {
this.loadData();
},
methods: {
async loadData() {
let self = this;
console.log("fetch");
self.isLoading = true;
const { data } = await Endpoints.get();
self.isLoading = false;
self.api = data;
console.log(data); // returns the data as intended
}
}
</script>
The componentA is also simple
<template>
<Label :text="somevariable"></Label>
</template>
<script>
export default {
data() {
return {
somevariable: 0
};
},
props: {
api: {
type: Number,
required: true
}
},
mounted() {
this.somevariable = this.api;
}
};
</script>
The error I am getting is [Vue warn]: Invalid prop: type check failed for prop "api". Expected Number with value NaN, got Undefined in the componentA, after some quoting and requoting of console.logs it actually picks up the value. I am not sure why is that, is my approach wrong? This frustrates me, can't figure it out for some hours already.
api isn't defined in the data for the first component, so it won't be reactive. That should be giving you a warning message in the console.
data () {
return {
api: null,
isLoading: false,
result: []
};
}
The second problem is that when the component first renders it won't yet have loaded api from the server. Using await won't help with this, rendering the template will happen before the asynchronous request has completed.
Given the way componentA is currently written it won't be able to cope with api being missing when it is first created. So you'll need to use a v-if to defer creation until that data is available:
<ComponentA v-if="api" :api="api.somevariable"></ComponentA>
Without the v-if check it'll just be passing the initial value of api, which in your original code is undefined. That is what caused the warning mentioned in the question.
When you talk about 'quoting and requoting of console.logs', I would assume that those changes are just triggering hot reloading, which could easily cause components to re-render with the new data. That wouldn't happen otherwise because of the lack of reactivity caused by api not being included in the original data.