Why vue need to forceUpdate components when they include static slot - vue.js

Why vue needs to forceUpdate child component that has a static slot when it update self
It will trigger too much update calculate when a component has lots of child components that has a static slot
// my-button.vue
<template>
<div>
<slot></slot>
</div>
</template>
// my-com.vue
<template>
<div>
<span>{{ foo }}</span>
<template v-for="(item, index) in arr">
<my-button>test</my-button>
</template>
</div>
</template>
<script>
export default {
data() {
return {
foo: 1,
arr: (new Array(10000)).fill(1)
}
}
}
</scirpt>
If run this.foo = 2 will lead update queue include 10000 watcher. When I read source code I found the following code
function updateChildComponent (
...
// Any static slot children from the parent may have changed during parent's
// update. Dynamic scoped slots may also have changed. In such cases, a forced
// update is necessary to ensure correctness.
const needsForceUpdate = !!(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
)
...
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context)
vm.$forceUpdate()
}
...
}

I have found this issue on GitHub.
Unfortunately, any child components with static slot content still
need to be forced updated. This means the common use case of
<parent><child></child></parent> doesn't benefit from this change,
unless the default slot is explicitly forced into a scoped slot by
using <parent v-slot:default><child></child></parent>. (We cannot
directly force all slots into scoped slots as that would break
existing render function code that expects the slot to be present on
this.$slots instead of this.$scopedSlots)
Seems like it's fixed in 2.6.
In 2.6, we have introduced an optimization that further ensures parent
scope dependency mutations only affect the parent and would no longer
force the child component to update if it uses only scoped slots.
To solve your problem just update your Vue version to 2.6. Since it's just a minor update nothing will break down. What about the reason to call forceUpdate - only Evan You knows that :)

Hello i solved this problem with this:
export function proxySlots(scopedSlots: any): any {
return Object.keys(scopedSlots).reduce<any>(
(acc, key) => {
const fn = scopedSlots[key];
fn.proxy = true;
return { ...acc, [key]: fn };
},
{ $stable: true },
);
}
const ctx = {
// ...
scopedSlots: proxySlots({ someSlot: () => <span>Hello</span>})
// or from provided slots : this.$scopedSlots or context.slots if using composition api
}
It's a bit hackish but no more forced update.

Related

How to make provide/inject reactive in Vue 3 to avoid props drilling?

I have a root component that has a lot of descendants. In order to avoid props drilling, I want to use provide/inject.
In the root component in the setup function, I use provide.
In the child component in the setup function, I get the value via inject.
Then the child component might emit an event, that forces the root component to reload data that it provides to the child components.
However, the data in the child component is not changed.
Previous answers that I found usually were related to Vue 2, and I'm struggling with Vue 3 composition API.
I tried to use watch/watchEffect, and "re-provide" the data, but it didn't work (and I'm not sure if it's a good solution).
Sample code: https://codesandbox.io/s/mystifying-diffie-e3eqyq
I don't want to be that guy, but read the docs!
Anyway:
App.vue
setup() {
let randomNumber = ref(Math.random());
function updateRandomNumber() {
randomNumber.value = Math.random()
}
// This should be an AJAX call to re-configurate all the children
// components. All of them needs some kind of config.
// How to "re-provide" the data after a child component asked this?
provide("randomNumber", {
randomNumber,
updateRandomNumber
});
},
ChildComponent.vue
<template>
<div>Child component</div>
<button #click="updateRandomNumber">Ask root component for re-init</button>
<div>Injected data: {{ randomNumber }}</div>
</template>
<script>
import { inject } from "vue";
export default {
setup() {
// How to "re-inject" the data from parent?
const {randomNumber, updateRandomNumber} = inject("randomNumber");
return {
randomNumber,
updateRandomNumber
};
},
};
</script>

How to avoid rerendering all child components which are created by v-for directive

There is a list of child component
<question-list-item
v-for="(item, index) in questionListParsed"
:key="item.id"
:both-question="item"
:class-id="classId"
:subject-id="subjectId"
:index="index+1"
/>
and the questionListParsed is a getter in vuex.
/**************************************************************************
* getters
**************************************************************************/
get questionListParsed(): QuestionListItemRes[] {
const { questionList, showingOriginalQuestion } = this
const questionListParsed = questionList.map((e) => {
const recommendQuestion = e.recommendedQuestions[0]
const recommendQuestionIds = showingOriginalQuestion[e.questionNumber]
let arr = []
if (recommendQuestionIds) {
arr = recommendQuestionIds.filter((item) => {
return !this.removedRecommendQuestionIds.includes(item)
})
}
return {
recommendQuestion: {
...recommendQuestion,
stem: recommendQuestion.question,
knowledges: splitMultiKnowledge(recommendQuestion.knowledge),
questionSourceList: recommendQuestion.sources,
categoryId: recommendQuestion.categoryId,
},
originalQuestion: {
...e,
id: e.questionNumber,
stem: e.question,
difficulty: e.complexity,
knowledges: splitMultiKnowledge(e.knowledge),
},
id: recommendQuestion.id,
questionSimilarId: e.questionNumber,
mistakeAnswerId: e.id,
targetExerciseId: e.targetExerciseId,
status: recommendQuestion.status,
}
})
return questionListParsed
}
and the questionListParsed is mainly depends on the state questionList whitch is the originnal data from server side. Now i change questionList by the following way
#Mutation
updateQuestionListByIndex(data: UpdateParams): void {
if (data.value) {
const temp = [...this.questionList]
temp[data.index] = data.value
this.questionList = temp
}
}
and commit the mutation inside an Action like these
this.context.commit('updateQuestionListByIndex', {
index: targetIndex,
value: originQuestion[0],
})
I just want to change one item in the array questionList and then questionListParsed changed.
The expectation is that only one component updated but all of the child component updated(use console.log('updated') in its updated hocks).
How to do that?
The reason why all components are updated is because you use computed property (Vuex getters are Vue computed properties).
Whenever anything in questionList is changed, questionListParsed is recomputed and because you are using map and generating new objects, the result is a new array with completely new objects --> every child in list is updated
I would not consider it a problem because in reality only the DOM elements of the changed item are updated (that is the beauty of virtual DOM). If you do see some performance problem, the way around it is to stop using computed/getters and instead do the transformation only once when data is loaded and continue to work only with questionListParsed
You don't need to prevent the child components from rerendering, Vue does that for you. By providing a unique key to each list element :key="item.id" you give Vue a hint about the item, so Vue can identify and reuse the already rendered parts.
See https://v2.vuejs.org/v2/api/#key for more information.

Find nearest parent Vue component of template ref (Vue 3)

When a Vue template ref is mounted, I want to get the nearest parent Vue component. This should be generic and work for any template ref so I've put it in a composition function (but that's just an implementation detail).
I had this working but my implementation used elem.__vueParentComponent while iteratively searching an element's ancestors. While reading the Vue source code I saw __vueParentComponent was only enabled for dev mode or if dev tools is enabled in production. Thus, I don't want to rely on that flag being enabled.
I thought this might be possible using vnodes but this isn't easily google-able. Here's an example of what I'm trying to do:
function useNearestParentInstance(templateRef) {
function getNearestParentInstance(el) {
// code here
}
onMounted(() => {
const el = templateRef.value;
const instance = getNearestParentInstance(el);
// do something with instance
});
}
<template>
<div>
<SomeComponent>
<div>
<div ref="myElem"></div>
</div>
</SomeComponent>
</div>
</template>
<script>
export default {
setup() {
const myElem = ref();
// nearest would be SomeComponent's instance in this case
useNearestParentInstance(myElem);
...
}
}
</script>
If you want the nearest vue parent you can simply use
ref().$parent // Not sure if syntax is same in vue3
ref().$parent will get the first vuecomponent that is the parent of the ref that you placed.

How to force Vue to update modified HTML

I use a custom directive to render LaTeX-code with KaTeX' renderMathInElement function. This, obviously, changes the component's innerHTML. I would like to re-run KaTeX once the content changes, but: The content never does!
A simple reproduction of the problem does not need KaTeX or directives and still shows, that reactivity works, but stops to work for the parts of a component with changed innerHTML:
<template>
<div>
{{content}}
<span ref="elem">{{content}}</span>
</div>
</template>
<script lang="ts">
import { Component, Ref, Vue } from "vue-property-decorator";
#Component({})
export default class Test extends Vue {
content = "Hello World!";
#Ref()
elem!: HTMLSpanElement;
mounted(): void {
// Without the following statement, Vue correctly re-renders the whole component after a second with the new content
// With this line, the update does not happen for the span element.
this.elem.innerHTML = "<b>Hello World!</b>";
setTimeout(() => {
this.content = "Greetings!";
}, 1000);
}
}
</script>
I suppose this is intended behavior - but that doesn't solve my problem. Is there some way to force Vue to replace all the component's DOM as soon as a re-render takes place?
You can use a key on your span, but if you don't want to tie it in with content, you can instead set it to a number, and increment it every time you want to make a change. Like so (I am not using TS here):
Set a key on your span:
<span :key="content_key">{{ content }}</span>
Then you can watch content and update the key accordingly:
watch: {
content() {
this.content_key ++;
}
}
In this way you can avoid setting the key to content directly.
Does this work for you?

How can I destroy a cached Vue component from keep-alive?

My Vue app has a dynamic tabs mechanism.
Users can create as many tabs as the want on the fly, each tab having its own state (eg "Pages").
I am using the <keep-alive> component to cache the different pages.
<keep-alive include="page">
<router-view :key="$route.params.id" />
</keep-alive>
But users can also "close" individual tab. As pages tend to store a lot of datas, I would like to delete the according page component from the cache, as the user close the tab.
How can I programmatically destroy a cached component inside keep-alive ?
You can call this.$destroy() before user close the tab and delete all of data and event binding in that one.
If you don't mind losing the state when a tab is added/removed, then you can try these:
Use v-if and turn off the keep-alive component and turn it back on in
nextTick
Use v-bind on the include list, and remove "page" and add it
back in nextTick
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
cachedViews is the array of the route component name
First when create a tab, cachedViews push the cached route name, when you switch the opened tab, the current route is cached.
Second when close the tab, cachedViews pop the cached route name, the route
component will destroyed.
There is no built-in function in keep-alive which allows you to clear a specific component from the cache.
However, you can clear the cache from the VNode directly inside the component you want to destroy by calling this function :
import Vue, { VNode } from 'vue'
interface KeepAlive extends Vue {
cache: { [key: string]: VNode }
keys: string[]
}
export default Vue.extend({
name: 'PageToDestroy',
...
methods: {
// Make sure you are not on this page anymore before calling it
clearPageFromKeepAlive() {
const myKey = this.$vnode.key as string
const keepAlive = this.$vnode.parent?.componentInstance as KeepAlive
delete keepAlive.cache[myKey]
keepAlive.keys = keepAlive.keys.filter((k) => k !== myKey)
this.$destroy()
}
},
})
For me, it doesn't cause any memory leaks and the component is not in the Vue.js devtools anymore.
based on the answer of #feasin, here is the setup I am using
template
<router-view v-slot="{ Component }">
<keep-alive :include="cachedViews">
<component :is="Component" :key="$route.fullPath" />
</keep-alive>
</router-view>
script
import { ref, inject, watch } from "vue";
export default {
components: { CustomRouterLink },
setup() {
const cachedViewsDefault = ["Page1", "Page1", "Page3"];
var cachedViews = ref([]);
const auth = inject("auth");
// check whether user is logged in (REACTIVE!)
const isSignedIn = auth.isSignedIn;
// set the initial cache state
if (isSignedIn.value) {
cachedViews.value = cachedViewsDefault;
}
// clear the cache state
watch(isSignedIn, () => {
if (!isSignedIn.value) {
cachedViews.value = [];
} else {
cachedViews.value = cachedViewsDefault;
}
});
return {
cachedViews,
};
},
};
First I set the initial cached views value based on whether the user is logged in or not.. After the user logs-out I simply set the array value to an empty array.
When the user logs back in - I push the default array keys back into the array.
This example of course does not provide the login/logout functionality, it is only meant as a POC to to the solution proposed by the #feasin (which seems like a good approach to me)
Edit 19.01.2022
I now understand the shortcomings of such approach. It does not allow to gradually destroy a certain component. Given that we have a component named Item and it's path is Item/{id} - there is currently no native way (in Vuejs3) to remove, let's say a cached item with Id = 2. Follow up on this issue on the Github: https://github.com/vuejs/rfcs/discussions/283
Edit 20-21.01.2022
Note that you have to use the computed function for inclusion list. Otherwise the component will not ever be unmounted.
Here is the fiddle with the problem: https://jsfiddle.net/7f2d4c0t/4/
Here's fiddle with the fix: https://jsfiddle.net/mvj2z3pL/
return {
cachedViews: computed(() => Array.from(cachedViews.value)),
}