Dynamic / Async Component Render - javascript

I am quite new to VueJS and have been playing around with the framework for a couple of days.
I am building a sort of dashboard with a widget based look and feel and the problem I have is that when the user adds a lot of widgets to the dashboard, problems arise on the loading of the page since the widgets make simultaneous calls to the API's to retrieve subsets of data.
To give you a better understanding of what I am doing, the concept is the below. (This is a brief idea to keep the code clean and simple).
Home.vue
<template>
<div class="Home">
<h1>Homepage</h1>
<div v-for="w in widgets">
<component :is="widget"></component>
</div>
</div>
</template>
<script>
export default {
name: 'Home',
mounted() {
for (var i = 0; i < availableWidgets; i++) {
widgets.push(availableWidgets);
}
},
};
</script>
Widget 1
<template>
<div class="Widget1">
<span>Widget 1</span>
</div>
</template>
<script>
export default {
name: 'Widget1',
mounted() {
//Get data from API and render
},
};
</script>
Widget 2
<template>
<div class="Widget2">
<span>Widget 2</span>
</div>
</template>
<script>
export default {
name: 'Widget2',
mounted() {
//Get data from API and render
},
};
</script>
As you can see, I am sort of loading the widgets and adding them dynamically depending on what the user has in his dashboard.
The problem I have is that Widget 1 and Widget 2 (in my case there are like 20-30 widgets), will be making API calls and this works fine when 1 or 2 widgets are loaded. But once the page grows a lot and there will be like 10 widgets on the page, everything starts lagging.
What would you suggest to do to make this more performant? Is it possible to allow once component to load at a time before loading the second component and so on? I was thinking of adding async calls, but that would not stop the components from being loaded at the same time?
Looking forward to your feedback and help that you could provide.

A common pattern would be to have the first render be without data, then re-render whenever your data comes in. The browser will make sure that not too many network requests run at the same time, so you should not have lag perse from that. You just perceive lag, because your component does not render until the data loads.
I would suggest using something like Axios, which uses promises and makes it easy to create asynchronous http requests while still keeping your code readable.
<template>
<div class="widget graph">
<div v-if="loading">
<span>Loading...</span>
<img src="assets/loader.svg">
</div>
<div v-else>
<!-- Do whatever you need to do whenever data loads in -->
</div>
</div>
</template>
<script>
export default {
name: 'WidgetGraph',
data () {
return {
loading: true,
error: null,
graphData: {}
}
},
created () {
this.loadData();
},
methods: {
loadData () {
return axios.get(...).then((data) => {
this.loading = false;
}).catch(() => {
this.error = 'Something went wrong.... Panic!';
this.loading = false;
});
}
}
}
</script>
<style>
</style>

Related

How do you optimize high total blocking time vue

I have encountered problem that I am not capable of solving. I have high total blocking time on my page (2+ sec). I have tried loading every vue component asynchronously, but it does not matter, still 2+ sec tbt. I don't really understand what can cause such high tbt and what can I do about it, as it is just a simple page without much underlying logic (https://i.stack.imgur.com/o7LSk.png) (Just 21 simple cards).
I have removed everything I can, compressed code, and left only the most nessesary stuff. Still it does not solve the issue. Is there any way to make it go down to 100-200ms? What can cause such a problem in your experience?
I have high amount of components though (cards, buttons, lazy-load picture, rating), so in the end there will be around 100-300 components on page. But I don't see there any possibilities of removing it, as this will break neat structure
The only way I found to fix high total blocking time in this case, is to use intersection observer and load it only when it enters the screen. I think same job can do some virtual scroller plugin.
<template>
<div class="grid">
<div class="grid__item" v-for="item in items" :data-id="item.id">
<Item v-if="itemsInScreen[item.id]" :item="item" />
</div>
</div>
</template>
<script>
export default {
name: 'Grid',
props: {
items: {required: true}
},
data() {
return {
itemsInScreen: {}
}
},
methods: {
initObserver() {
const callback = entries => {
entries.forEach(entry => {
if(entry.isIntersecting) this.$set(this.itemsInScreen, entry.target.dataset.id, true);
});
};
const options = {
rootMargin: "20px 20px 20px 20px"
};
const observer = new IntersectionObserver(callback, options);
const itemEls = this.$el.querySelectorAll('.grid__item');
itemEls.forEach(itemEl => observer.observe(itemEl));
}
},
mounted() {
this.initObserver();
}
}
</script>

Why doesn't v-show displayed when I go back to the previous step?

I have an SAP in VUE that consists of a form in 4 steps.
When submit in the first step button and redirect to second step, if I return to first step the last component is stopped showing, in spite of being shown before changing the component.
this is my template
<input
type="text"
id="animalName"
ref="animalName"
v-model="animalFormInfo.name"
#keypress="onChangeName($event)"
#keyup="onChangeName($event)"
/>
<div class="loader-spinner" v-if="loading">
<app-loader>
</div>
</div>
</div>
</div>
<div v-show="isNameCompleted">
<div class="from-group">
<h1 class="title">
For
<span>{{this.formInfo.name}}</span>, you have:
</h1>
<div class="list-services">
<div
class="column-service"
v-for="estimation in filteredEstimation"
v-bind:key="estimation.name"
>
<div>{{estimation.name}}</div>
<div>{{estimation.description}}</div>
<div>{{estimation.price.toString().replace(".", ",")}}</div>
</div>
</div>
</div>
</div>
<div class="button-line" v-show="isLoaderFinished"></div>
<div class="forward" v-show="isLoaderFinished">
<div class="forward-button">
<button
ref="submit"
v-bind:disabled="!isFormCompleted"
type="submit"
#click="navigateToCreate"
>
SEND INFO
</button>
</div>
</div>
and below the data and the method in which I control the load of the components
data() {
return {
step: 1,
isFormCompleted: false,
isBreedSelected: false,
isNameCompleted: false,
loading: false,
isLoaderFinished: false,
myTimeout: 0
};
},
methods:{
onChangeName(event) {
if (this.myTimeout >= 0 && event) {
clearTimeout(this.myTimeout);
this.loading = true;
this.isNameCompleted = false;
}
this.myTimeout = window.setTimeout(() => {
this.isNameCompleted = true;
const typedAnimalName = event.target._value;
this.capitalizeAnimalName(typedAnimalName);
this.loading = false;
this.isLoaderFinished = true;
this.isFormCompleted = true;
setTimeout(() => (this.$refs.scrolled_3.focus()), 10);
}, 2200);
},
capitalizeAnimalName(typedAnimalName) {
var splitAnimalName = typedAnimalName.toLowerCase().split(' ');
for (var i = 0; i < splitAnimalName.length; i++) {
splitAnimalName[i] = splitAnimalName[i].charAt(0).toUpperCase() + splitAnimalName[i].substring(1);
}
return this.animalName = splitAnimalName.join(' ');
},
},
Once I reach step 2 and return to step 1 only the info is shown until the input with id "animalName".
What am I doing wrong?
Is there any reason why when loading the first component it follows the logic and shows them and once I go to the next one they disappear?
thank you all for your time and help in advance
The problem is that the state of the component is not persistent.
There are multiple solutions for this.
1. Manage state globally with Vuex
This is probably the default, or most common way of handling this kind of situation. Instead of storing the data in the component, you use this library to manage the state. Most likely you can just implement vuex in your project and not read on. It's a solid choice with too many upsides to list.
2. Encapsulate form state in the parent component.
Instead of managing the state within the step component, you can create the state for the relevant data inside the parent object.
example:
<Step2
v-if="step === 2"
:formData="formData"
:updateFormData="updateFormData"
:nextStep="() => gotoStep(3)"
></Step2>
The idea is that the Step2 component would have the data passed in through formData prop. And any time that data changes, updateFormData function is called with the new data. You could even simplify it by utilizing the v-model.
3. DIY store
If you're using Vue js 2 (>= 2.6, < 3), you can do this with observable
Vue 2.6+
create a store.js file
import Vue from "vue";
export const store = Vue.observable({
step: 1,
isFormCompleted: false,
isBreedSelected: false,
isNameCompleted: false,
loading: false,
isLoaderFinished: false,
});
Then you can call your store.js from any component, and if you assign it to this in the beforeCreate hook, you can use it just like data (this.store.isFormCompleted in script, store.isFormCompleted in template)
import { store} from 'store.js';
//...
beforeCreate(){
this.store = store;
}
if you want to skip the extra store., you could even destructure the store object.
Vue 3
Similar to the other one there, but use a reactive() (or multiple ref()). Let me know if you want me to expand on this.
If each step is its own component, you can use the keep-alive component to cache the components and their state, so that when you switch between them, their state is retained. I would personally prefer that over using Vuex in this case, since it's much simpler.
<keep-alive>
<step-1-component v-if="step === 1"></step-1-component>
<step-2-component v-if="step === 2"></step-2-component>
<step-3-component v-if="step === 3"></step-3-component>
</keep-alive>
See: https://v2.vuejs.org/v2/api/#keep-alive
It is so because the component is re-rendering when you go back to the first page, so all the default states will take effect. Try saving your states with Vuex so that all components will be controlled by the state from the store.

Lazy load Vue components once, retain data

i'm lazy loading some components, but these components do AJAX requests. I want the component's to retain their data if they are hidden again, as to not do multiple AJAX requests.
This is my main component which contains both lazy loaded components:
<template>
<div class="media-component">
<vehicle-image-slider :vehicle-id="vehicleId"
v-if="active === 'vehicle-image-slider'"
key="vehicle-image-slider"></vehicle-image-slider>
<vehicle-360-viewer :vehicle-id="vehicleId"
v-if="active === 'vehicle-360-viewer'"
key="vehicle-360-viewer"></vehicle-360-viewer>
Slider
360
</div>
</template>
<script>
const Vehicle360viewer = () => import('./Vehicle360Viewer.vue');
const VehicleImageSlider = () => import('./VehicleImageSlider.vue');
export default {
data: function() {
return {
active: 'vehicle-image-slider'
}
},
components: {
'vehicle-360-viewer': Vehicle360viewer,
'vehicle-image-slider': VehicleImageSlider
},
props: [ 'vehicleId' ]
}
</script>
I'd imagine v-if is the wrong thing to use on the components, however I don't want to load the component unless it is displayed.

Nuxt: destroy and recreate current page's component without refresh

I have a bunch of Vue components and I'm using Nuxt as the routing layer. My global layout is pretty standard:
<template>
<nav>
<nuxt-link to="/foo">foo</nuxt-link> | <nuxt-link to="/bar">bar</nuxt-link> | etc.
</nav>
<main>
<nuxt />
</main>
</template>
In each page, I update the query string when data in the vuex store changes. If the page is loaded server-side, I parse the query string to pre-load the necessary data into the store:
<template>
<h1>foo</h1>
<!-- forms and stuff -->
</template>
<script>
export default {
data() {
return {
// static data
}
},
computed: {
fooStoreParams() {
return this.$store.state.foo.params
}
},
watch: {
fooStoreParams: async function() {
await this.$nextTick()
let query = {}
if (this.fooStoreParams.page > 1) {
query.page = this.fooStoreParams.page
}
this.$router.push({ path: this.$route.path, query: query })
}
},
async asyncData({ store, query }) {
let params = {}
let data = {
form: {
page: 1
}
}
if (query.page) {
data.form.page = query.page
params.page = query.page
}
store.dispatch('foo/updateParams', params)
await store.dispatch('foo/getStuffFromAPI')
return data
}
}
</script>
This works well, but there's a feature that I'm missing.
If I'm on already on /foo?page=2&a=1&b=2 and I click on the /foo link in the main navigation, nothing happens. This makes sense considering how Nuxt/vue-router works, but what I want to happen is for the page component to be reloaded from scratch (as if you had navigated from /bar to /foo).
The only ways I can think to do this are to either 1) do a server-side request (e.g. <b-link href="/foo">) if I'm already on /foo?whatever or 2) write a resetPage() method for each individual page.
Is there a way to just tell Nuxt to destroy and recreate the current page component?
You need to use watchQuery in order to enable client-navigation for query-params:
watchQuery: ['page', 'a', 'b']
https://nuxtjs.org/api/pages-watchquery/
If you have a component e.g
<titlebar :key="somekey" />
and the value of somekey changes the component re-renders. You could maybe work around this to achieve what you want. Read more here: https://michaelnthiessen.com/force-re-render/

Changing a button to say "Loading" with a loading animation on click

I have created a Vue button that displays "Load More" and then "Loading..." when clicked and loading more content. But, I would now like to add another component being a loading animation next to the "Loading." The button works completely fine, but I just would like to add that animation alongside the word "loading."
I have tried using Vue's ref tag, but have not had much luck in successfully using that in my method.
Loader.vue:
<template>
<div
ref="sdcVueLoader"
class="sdc-vue-Loader"
>
Loading...
</div>
</template>
<script>
export default {
name: 'Loader'
</script>
App.vue:
<Button
:disabled="clicked"
#click="loadMore"
>
{{ loadMoreText }}
</Button>
<script>
import Button from './components/Button'
import Loader from './components/Loader'
export default {
name: 'ParentApp',
components: {
Button,
Loader
},
data () {
return {
loadMoreText: 'Load More',
clicked: false
}
},
methods: {
loadMore () {
if ... {
this.page += 1
this.loadMoreText = 'Loading...' + this.$refs.sdcVueLoader
this.clicked = true
this.somerequest().then(resp => {
this.clicked = false
this.loadMoreText = 'Load More'
})
return this.loadMoreText
}
}
</script>
I am hoping for the button to continue working as it is now, but now to also have the "Loader" component displaying next to "Loading..." when the button is clicked in the app.vue loadMore method.
If you want to do anything with any form of complexity in html, it is best to move it over to your template. In your case, you have two states: It is either loading, or it is not loading. So lets create a variable loading that is either true or false.
data () {
return {
loading: false,
page: 1,
}
},
methods: {
async loadMore () {
if (this.loading) {
return;
}
this.page += 1;
this.loading = true;
const response = await this.somerequest();
this.loading = false;
// Oddly enough, we do nothing with the response
}
}
Now, in the template use a v-if with a v-else:
<button
:disabled="loading"
#click="loadMore"
>
<template v-if="loading">
<icon name="loader" />
Loading...
</template>
<template v-else>
Load more
</template>
</button>
If you want to move the logic to a different component, you have two options:
Add loading as a prop to that different component, and move the template code to that component
Use a slot and pass the html directly into your loading button. This is especially useful if you have several different configurations, and don't want to deal with increasingly complex configuration options just to accommodate them all.

Categories

Resources