VueJS: Can't update progress loader during rendering - javascript

I am having a Vue app which is loading up to 15 different pivot tables with a lot of data.
The entire rendering time takes about 15 seconds.
Within this time period I really would like to display a progress bar up 100 %.
But unfortunately i can't update the progress bar during the rendering.
It is somehow blocked.
Which recommendations do you have to solve this issue?
The code looks like:
<template
v-for="n in (numberOfPivotTables - 1)"
>
<!-- Pivot table #1: spot count / ad count -->
<mdb-col
sm="12"
md="12"
lg="6"
xl="6"
:key="n"
>
<div class="pt-5 mx-3 pb-5 pr-5">
<div style="height:350px">
<h5>{{customPivotOptions[n]['reducerKey']}}</h5>
<channel-filters
:hour-options="filters.hour[n]"
:adtype-options="filters.adType[n]"
:length-options="filters.spotLength[n]"
:creative-options="filters.motive[n]"
:weekday-options="filters.weekday[n]"
:flight-options="filters.flightId[n]"
:month-options="filters.month[n]"
:product-options="filters.product[n]"
:station-options="filters.station[n]"
:pivot-table-id="String(n)"
#getValue="getSelectValue"
>
</channel-filters>
<mdb-row>
<mdb-col
cols="12"
sm="2"
>
<mdb-select selectAll search
#getValue="setReducerKey"
:options="customPivotOptions[n].reducerOptions"
label="Value"
:arrayId="String(n)"
modelName="reducerKey"
:visibleOptions="10"
placeholder="Choose your Value" />
</mdb-col>
</mdb-row>
</div>
<pivot-table
:headline="customPivotOptions[n]['reducerKey']"
:data="pivotData[n].data"
:pivot-table-id="String(n)"
:fields="fields[n]"
:available-field-keys="customPivotOptions[n].availableFieldKeys"
:row-field-keys="customPivotOptions[n].rowFieldKeys"
:col-field-keys="customPivotOptions[n].colFieldKeys"
:default-show-settings="defaultShowSettings"
:reducer="getReducerKey"
#getValues="getPivotValues"
:sum-row="true"
:sum-column="true"
:sum-column-custom="{}"
>
</pivot-table>
</div>
</mdb-col>
<div v-if="n==7" v-bind:key="n" class="w-100"></div>
</template>

Vue rendering is JS execution. When JS is executing, browser is not rendering (updating screen) so app looks frozen.
The way around it is simply do not render everything at the same time. requestAnimationFrame API can be very useful here. The code below is based on the 9 Performance Secrets Revealed talk by with Guillaume Chau (Vue core team member) (GitHub repo with code)
1st we create a reusable mixin (factory function returning the mixin):
defer.js
export default function (count = 10) {
// #vue/component
return {
data () {
return {
displayPriority: 0,
}
},
mounted () {
this.runDisplayPriority()
},
methods: {
runDisplayPriority () {
const step = () => {
requestAnimationFrame(() => {
this.displayPriority++
if (this.displayPriority < count) {
step()
}
})
}
step()
},
defer (priority) {
return this.displayPriority >= priority
},
},
}
}
This mixin adds displayPriority counter to your component. It starts at 0 and is increased every time browser is ready to render new frame (repaint the screen) until it reaches count (passed as an argument to the factory function - default 10).
Important part is defer() function. It simply compares it's argument with displayPriority and returns true/false. For example defer(2) returns false when the browser repaints the screen for the 1st time (after component was rendered) and true for the rest of the page lifetime.
So the component with v-if="defer(2)" will be rendered (for the 1st time) before the component with v-if="defer(3)" etc.
Now it is easy to "split" the rendering of the parts of the component into multiple stages:
<template>
<div>
<template v-for="n in (numberOfPivotTables - 1)">
<heavyComponent v-if="defer(n)" :key="n" />
</template>
</div>
</template>
<script>
import Defer from 'defer'
export default {
mixins: [
Defer(), // pass number of steps as an argument. Default is 10
],
}
</script>

Related

React Router - How to prevent top level refresh when child component button is clicked, preventDefault does not seem to make a difference

Background:
I have a simple react router:
<BrowserRouter >
<Routes>
<Route path="test/:locationId" element={<TestComponent />}></Route>
Then I have the component really simple:
function Inner(props) {
let ele = JSON.stringify(props);
console.log(" this is inner , with " + ele);
const [value, setValue] = useState(parseInt(ele));
return (
<div onClick={(e) => e.stopPropagation()}>
<p> Inner component: {String(ele)}</p>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("inner btn clicked for " + value);
setValue(value + 1);
}}
>
{" "}
inner btn with clicked value {value}
</button>
</div>
);
}
export default function TestComponent(props) {
console.log("TestComponent top level component " + new Date());
console.log("I will do some heavy work like a big http request.....")
const [res, setRes] = useState([1, 2, 3]);
let params = useParams();
console.dir(params);
if (res.length > 0) {
console.log("res > 0 ");
return (
<div className="container">
{res.map((ele) => {
return <div key={String(ele)}>{Inner(ele)}</div>;
})}
</div>
);
}
Problem:
Anytime I click on any of the 3 buttons, I see TestComponent refreshes from the top level, like a new line of TestComponent top level component Mon Jul 18 2022 17:35:52 GMT-0400 (Eastern Daylight Time).
Now I plan to do some heavy http request and setState() inside TestComponent. However whenever I click on the buttons (in child components) this TestComponent always refreshes and thus will do the http request over and over again which I want to prevent. Tried e.stopPropagation() and e.preventDefault() but saw no difference.
enter image description here
Issues
It doesn't seem you understand the React component lifecycle well and are using the completely wrong tool to measure React renders.
All the console logs are occurring outside the render cycle as unintentional side-effects. What this means is that anytime React calls your app (the entire app code) to rerender (for any reason) that the component body is executed during the "render phase" in order to compute a diff for what changed and actually needs to be pushed to the DOM during the "commit phase". The commit phase is React rendering the app to the DOM and this is what we often refer colloquially as the React component rendering.
Note that the "render phase" is to be considered a pure function that can be called anytime React needs to call it. This is why it's pure function, i.e. without unintentional side-effects. The entire function body of a React function component is the "render" method.
Note the "commit phase" is where the UI has been updated to the DOM and effects can now run.
The issue of using the console.log outside the component lifecycle is exacerbated usually by the fact that we render our apps into the React.StrictMode component that does some double mounting of apps/components to ensure reusable state, and intentionally double invokes certain methods and functions as a way to detect unintentional side-effects.
Inner is defined like a React component but called manually like a regular function. This is a fairly obvious no-no. In React we pass a reference to our React components to React as JSX. We don't ever directly call our React functions directly.
Solution/Suggestion
Move all the console.log statements into useEffect hooks so they are called exactly once per render (to the DOM) cycle. All intentional side-effects should be done from the useEffect hook.
Render Inner as a React component and correctly pass props to it.
Example:
function Inner({ ele }) {
useEffect(() => {
console.log("this is inner , with " + ele);
});
const [value, setValue] = useState(ele);
return (
<div onClick={(e) => e.stopPropagation()}>
<p>Inner component: {String(ele)}</p>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log("inner btn clicked for " + value);
setValue(value + 1);
}}
>
inner btn with clicked value {value}
</button>
</div>
);
}
...
function TestComponent(props) {
const [res, setRes] = useState([1, 2, 3]);
let params = useParams();
useEffect(() => {
console.log("TestComponent top level component " + new Date());
console.log("I will do some heavy work like a big http request.....");
console.dir(params);
if (res.length > 0) {
console.log("res > 0 ");
}
});
if (res.length > 0) {
return (
<div className="container">
{res.map((ele) => (
<div key={ele}>
<Inner {...{ ele }} />
</div>
))}
</div>
);
}
}
...
<Routes>
<Route path="test/:locationId" element={<TestComponent />} />
</Routes>
Note that after the initial StrictMode mounting/remount that clicking the buttons only logs a single log from each Inner component.

Calling a Vue mixin directly with a constant?

I have a footer component with three links. When a user clicks a link, besides taking the user to a new page, I am trying to use a mixin to track the click event. When I set a breakpoint in chrome devtools, it appears that this implementation is not working. I imported my constants file, and the mixin.
footer, one link for brevity
<template>
<footer>
<div class="container">
<div class="row">
<div class="col align-center">
<a
href="/"
target="_blank"
class="btn"
name="item"
#click="logButtonClick(ANALYTICS.ITEM)">{{ $t('footer.item') }}</a>
</div>
</div>
</div>
</footer>
</template>
<script>
import analytics from '#/mixins/analytics'
import { ANALYTICS } from "#/constants"
export default {
name: 'PageFooter',
mixins: [analytics]
}
</script>
mixin
methods: {
logButtonClick (buttonType) { // breakpoint here, get nothing
this.$analytics.track({
identifier: `Consumer ${this.$options.name} - ${buttonType} Button`
})
}
}
Am I missing something? Should this implementation work or should I have a method such as:
methods: {
selectLink(str) {
if (str === item) {
this.logButtonClick(ANALYTICS.ITEM)
}
}
}
The original error I received was
"Property or method ANALYTICS not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class based components, by initializing the property."
and
"Uncaught TypeError: Cannot read property ITEM of undefined at click event...."
Essentially this means I did not define ANALYTICS either in PageFooter (because this is a dumb component, I did not want to add a data object to it, I wanted to keep it strictly presentational) or on the vue instance at a root level. Since ANALYTICS is undefined, ITEM then throws another error because it can not be a property of undefined.
This is my solution, I used a switch case in the and in the template tag added #click="selectLink('asd')"
methods: {
selectLink (str) {
switch (true) {
case str === 'asd':
this.logButtonClick(ANALYTICS.ITEM)
break
case str === 'efg':
this.logButtonClick(ANALYTICS.ITEM2)
break
case str === 'hij':
this.logButtonClick(ANALYTICS.ITEM3)
break
}
}
}
and the unit test:
it('[positive] should track analytics if `asd` is passed to selectLink()', () => {
const str = 'asd'
const mockFn = jest.fn()
jest.spyOn(wrapper.vm, 'logButtonClick')
wrapper.find('a').trigger('click')
mockFn(str)
expect(wrapper.vm.logButtonClick).toHaveBeenCalledWith(ANALYTICS.COOKIE_CONSENT)
})
https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties
Moral of the story, question things when senior engineers tell you to do something funky in a code review.

Create custom slider to control 'scrollLeft' of html element using Vue

I'm trying to create a custom slider to emulate horizontal scroll behavior of a specific element. the slider is implemented as a stand alone component and the selector for the element is passed to the slider component as a property:
<template>
<div class="slidercontainer">
<h1>{{contentWidth}}</h1>
<button class="right-scroll"></button>
<input
type="range"
min="1"
:max="contentWidth"
value="1"
class="slider"
v-on:input="handleScroll"
>
<button class="left-scroll"></button>
</div>
</template>
<script>
export default {
props: ["el"],
data() {
return {
scrollLeft: 0
};
},
methods: {
handleScroll($event) {
this.content.scrollLeft = this.contentWidth - $event.target.value;
}
},
computed: {
content() {
return document.querySelector(this.el);
},
contentWidth() {
return this.content.scrollWidth - this.content.clientWidth;
}
}
};
</script>
the problem though, is that content wont update and always return 0. when first loaded the app is waiting for some data from the server and thats why when mounted the contentWidth equals 0, but i thought returning content as a computed property should take care of that, but nothing happens after the content is injected with some new...well - content :)
any idea how to solve this?

Dynamically integrate Vuetify v-stepper with Vue router

I would like to integrate vuetify's v-stepper with vue router. My requirements are as follows:
Each step has its own route (e.g. /myform/step1, /myform/step2, /myform/step3, etc)
Each step is component on its own which is dynamically loaded (lazy-load).
Each step is dynamically created (e.g. via a loop).
This is more of a 'what is the best approach' kind-of-question. I've tried multiple solutions but none fit my requirements.
I've tried creating nested routes and placing a router-view in v-stepper-content. Example below. The issue I faced here was that it's impossible to synchroniously update position (see v-stepper element) and the route. So you'll always see the route updating before the step is updated.
<v-stepper v-model="position" vertical>
<template v-for="(item, index) in steps">
<v-stepper-step :complete="position > index + 1" :step="index + 1">
<h2>
{{item.title}}
</h2>
</v-stepper-step>
<v-stepper-content :step="index+1">
<router-view></router-view>
</v-stepper-content>
</template>
</v-stepper>
Another solution I tried is loading the components async/dynamically directly (so without router). However, then I lose the beautiful ability to navigate through my v-stepper using the browser's back and next buttons.
In my experience, the biggest pitfall is that (contrary to e.g. v-tab), is that every step has to have its own v-stepper-content. If I were to do this with tabs, I would just create one tab-item and update the view. I can't do that with v-stepper, because it wouldn't continue to the next 'step'.
Would anyone have a creative approach?
I was able to achieve this by doing the following:
<v-stepper-step #click="goToRoute('step1')">
with
goToRoute(name) {
this.$router.push({'name': name})
}
You should be able to do this:
<v-stepper-step #click="$router.push({'name': name})">
As an additional answer to #tmfmaynard, in order to align the correct highlighted stepper with your current route after a page refresh, here is the code.
<v-stepper v-model="e1" alt-labels non-linear>
<v-stepper-header class="elevation-0">
<v-stepper-step
step="1"
class="caption"
editable
#click="$router.push({name: 'name'}).catch(err => {})"
>
</v-stepper-step>
</v-stepper-header>
<v-stepper-items>
<v-stepper-content step="1">
<router-view />
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<script>
export default {
data () {
return {
e1: 1
}
},
created() {
this.getStepper()
},
methods: {
getStepper() {
const path = this.$route.path.split('/')
if(path[path.length-1].toLowerCase() === 'your-router-path') {
this.e1 = 1
// this.e1 = 1 = <v-stepper-step step="1" />
// this.e1 = 2 = <v-stepper-step step="2" />
// and so on.
}
}
}
}

How to control order of rendering in vue.js for sibling component

I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.

Categories