Vue SSR with v-if. How to correctly perform hydration? - vue.js

I have a problem with Vue SSR.
In my project I have a page called slug, where depending on the data received in asyncData, the appropriate component is mounted. It looks more or less like this:
<div>
<component-a v-if='type === a' />
<component-b v-else-if='type === b' />
<component-c v-else-if='type === c' />
</div>
<script>
export default {
asyncData({ store }) {
store.dispatch('product/fetchAsync')
},
computed () {
type () {
return this.$store.state.type
}
}
}
</script>
However, Vue is not able to perform SSR hydration.
Is there a possibility that this is due to v-if statement?
How to solve this correctly? The only thing I can think of is prefixes and making each component a separate page, without v-if. But the client would like to avoid this.

As some comments say, your html structure at server side (where ssr is happening) don't know if the conditions for rendering an item or other are true.
You can use v-show instead of v-if. The difference is that v-show will render that element but with no data. This way your html won't change and hydration is successful

Related

Hydration problem with client-side authentication and computed property in Vue/Nuxt layout

My app (vue/nuxt 3) stores the user authentication state in localStorage. As a consequence it is only available on the client and prerendered pages always show the unauthenticated content. Client will render the authenticated content as soon as it is aware of it. That's ok and accepted.
However, this does not seem to apply for computed properties. My whole layout depends on the authentication state, e.g. like this:
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = computed(() => ({
if ($someReferenceToStore.user.logged.in) {
return 'loggedin'
} else {
return 'anonymous'
}
}))
</script>
The problem is, that even though the user is logged in, the computedClasses is not updated to loggedin but the server generated anonymous is shown. How to solve this? How can I make the client update the computed property and overwrite the server rendered classes?
I know, I can wrap parts of my template that depend on the authentication state with <ClientOnly> to avoid hydration mismatches. Wrapping my layout with <ClientOnly> would basically disable any server rendering. Can I set a property of an element (the :class="...") to client-only?
My current solution is to use a ref that's being updated.
<template>
<div :class="computedClasses">
<slot />
</div>
</template>
<script setup>
const computedClasses = ref('');
onMounted(() => {
computedClasses.value = $someReferenceToStore.user.logged.in ? 'loggedin' : 'anonymous';
});
</script>
Depending on the use case, one might want to add a watcher to update the ref as well.

Vue3 Composition API Reusable reactive values unique to calling component

Running Vue 3.2.6 and Vite 2.5.1
I've been experimenting a bit with the new Composition API and trying to figure out some common usecases where it makes sense to use it in favor of the OptionsAPI. One good usecase I immediately identified would be in Modals, the little popups that occur with a warning message or dialogue or whatever else.
In my old Apps, I'd have to create the modal opening logic in every single component where the modal is being called, which lead to a lot of repetition. With the CompAPI, I tried extracting the logic into a simple modal.ts file that exports 2 things, a reactive openModal boolean, and a toggleModal function. It works great! Until I have more than one modal in my app, that is, in which case it'll open every single Modal at once, on top of one another.
As an example setup
modal.ts
import { ref } from "vue";
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
export { openModal, toggleModal };
App.vue
<template>
<Example1 />
<Example2 />
<Example3 />
</template>
Modal.vue
<template>
<div class="modal" #click.self.stop="sendClose">
<slot />
</div>
</template>
<script setup>
const emit = defineEmits(["closeModal"]);
const sendClose = () => {
emit("closeModal");
};
</script>
Example#.vue
Note that each of these are separate components that have the same layout, the only difference being the number
<template>
<h1>Example 1 <span #click="toggleModal">Toggle</span></h1>
<teleport to="body">
<Modal v-if="openModal" #closeModal="toggleModal">
<h1>Modal 1</h1>
</Modal>
</teleport>
</template>
<script setup>
import { openModal, toggleModal } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
</script>
What happens when clicking the toggle span is obvious (in hindsight). It toggles the value of openModal, which will open all 3 modals at once, one on top of the other. The issue is even worse if you try to implement nested Modals, aka logic in one modal that will open up another modal on top of that one.
Am I misunderstanding how to use ref here? Is it even possible for each component to have and keep track of its own version of openModal? Cause the way I've set it up here, it's acting more like a global store, which isn't great for this particular usecase.
The way I imagined this working is that each component would import the reactive openModal value, and keep track of it independently. That way, when one component calls toggleModal, it would only toggle the value inside of the component calling the function.
Is there a way of doing what I originally intended via the Composition API? I feel like the answer is simple but I can't really figure it out.
That is because you are not exporting your composition correctly, resulting in a shared state, since you are exporting the same function and ref to all components. To fix your issue, you should wrap whatever you're exporting in modal.ts in a function, say:
// Wrap in an exported function (you can also do a default export if you want)
export function modalComposition() {
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
return { openModal, toggleModal };
}
And in each component that you plan to use the composition, simply import it, e.g.:
import { modalComposition } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
// By invoking `modalComposition()`, you are no longer passing by reference
// And therefore there is no "shared state"
const { openModal, toggleModal } = modalComposition();
Why does this work?
When you export a function and then invoke it in the setup of every single component, you are ensuring that each component is setup by executing the function, which returns a new ref for every single instance.

Best way to add components dynamically to a Vue app

What is the best way to add components dynamically to your vue app?
Say you have three different components in your app and you want to show each one depending on the value that a data has.
data:() => ({
tab: 1
})
<custom-component-1></custom-component-1> <!-- Show this if tab is 1 -->
<custom-component-2></custom-component-2> <!-- Show this if tab is 2 -->
<custom-component-3></custom-component-3> <!-- Show this if tab is 3 -->
I'm gonna go through all the possible ways of doing this.
Using v-if or v-show
The first and obvious way is to add v-if to your component like this:
<custom-component-1 v-if="tab === 1"></custom-component-1> <!-- Show this if tab is 1 -->
<custom-component-2 v-if="tab === 2"></custom-component-2> <!-- Show this if tab is 2 -->
<custom-component-3 v-if="tab === 3"></custom-component-3> <!-- Show this if tab is 3 -->
You can also use v-show if you want to, it's up to you.
See the difference between v-show and v-if. v-show vs v-if
This probably is the easiest way of doing it but not the most efficient.
once your code starts to get more complicated this code is going to be your hell
Using Vue's dynamic components
The second way of doing this is by using Vue's dynamic components Link to documention
Here is our example again with dynamic components:
computed: {
component: function () {
return `custom-component-${this.tab}`;
}
},
data:() => ({
tab: 1
})
And we just need to pass the name of the components:
<component is="component">
<!-- instead of -->
<custom-component-number></custom-component-number>
<component :is="component"> </component>
<button #click="tab++"></button>
Using the computed and is property we can have infinite components dynamically.
This is a nice clean way of doing it. You take the computation part away from your markup and put it in the script for a cleaner and more efficient code
If you are using this approach make sure to import and initialize the components you want to use in the page or add them in your main.js as global components like this:
import Vue from "vue";
import Component1 from "~/components/component1.vue";
import Component2 from "~/components/component2.vue";
import Component3 from "~/components/component3.vue";
Vue.component("custom-component-1",Component1);
Vue.component("custom-component-2",Component2);
Vue.component("custom-component-3",Component3);
You can also add the components to your page:
import customComponent from "~components/customComponent";
export default {
components : {
customComponent: "custom-component"
}
}

Vue- best practice for loops and event handlers

I am curious if it is better to include methods within loops instead of using v-if. Assume the following codes work (they are incomplete and do not)
EX: Method
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<span v-on:click="insertPrompt($event)">
{{ d }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
data:[
.....
]
},
methods:{
insertPrompt(e){
body.insertBefore(PROMPT)
}
}
}
</script>
The DOM would be updated via the insertPrompt() function which is just for display
EX: V-IF
//Parent
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<child v-bind:data="d"/>
</div>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data() {
data:[
.....
]
},
}
</script>
//Child
<template>
<div>
<span v-on:click="display != display">
{{ d }}
</span>
<PROMPT v-if="display"/>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data(){
return {
display:false
}
},
props: {
data:{
.....
}
},
}
</script>
The PROMPT is a basic template that is rendered with the data from the loop data click.
Both methods can accomplish the same end result. My initial thought is having additional conditions within a loop would negatively impact performance?!?!
Any guidance is greatly appreciated
Unless you are rendering really huge amounts of items in your loops (and most of the times you don't), you don't need to worry about performance at all. Any differences will be so small nobody will ever notice / benefit from having it a tiny touch faster.
The second point I want to make is that doing your own DOM manipulations is often not the best idea: Why do modern JavaScript Frameworks discourage direct interaction with the DOM
So I would in any case stick with the v-if for conditionally rendering things. If you want to care about performance / speed here, you might consider what exactly is the way your app will be used and decide between v-if and v-show. Citing the official documentation:
v-if is “real” conditional rendering because it ensures that event
listeners and child components inside the conditional block are
properly destroyed and re-created during toggles.
v-if is also lazy: if the condition is false on initial render, it
will not do anything - the conditional block won’t be rendered until
the condition becomes true for the first time.
In comparison, v-show is much simpler - the element is always rendered
regardless of initial condition, with CSS-based toggling.
Generally speaking, v-if has higher toggle costs while v-show has
higher initial render costs. So prefer v-show if you need to toggle
something very often, and prefer v-if if the condition is unlikely to
change at runtime.
https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
There are numerous solutions to solving this issue, but let's stick to 3. Options 2 and 3 are better practices, but option 1 works and Vue was designed for this approach even if hardcore developers might frown, but stick yoru comfort level.
Option 1: DOM Manipulation
Your data from a click, async, prop sets a condition for v-if or v-show and your component is shown. Note v-if removes the DOM element where v-show hides the visibility but the element is still in the flow. If you remove the element and add its a complete new init, which sometimes works in your favor when it come to reactivity, but in practice try not to manipulate the DOM as that will always be more expensive then loops, filters, maps, etc.
<template >
<div>
<div v-for="(d, i) in getData"
:key="i">
<div v-if="d.active">
<child-one></child-one>
</div>
<div v-else-if="d.active">
<child-two></child-two>
</div>
</div>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
data: [],
}
},
computed: {
getData() {
return this.data;
},
},
mounted() {
// assume thsi woudl come from async but for now ..
this.data = [
{
id: 1,
comp: 'ChildOne',
active: false
},
{
id: 2,
comp: 'ChildTwo',
active: true
},
];
}
}
</script>
Option 2: Vue's <component> component
Always best to use Vue built in component Vue’s element with the is special attribute: <component v-bind:is="currentTabComponent"></component>
In this example we pass a slug or some data attribute to activate the component. Note we have to load the components ahead of time with the components: {}, property for this to work i.e. it has to be ChildOne or ChildTwo as slug string. This is often used with tabs and views to manage and maintain states.
The advantage of this approach is if you have 3 form tabs and you enter data on one and jump to the next and then back the state / data is maintained, unlike v-if where everything will be rerendered / lost.
Vue
<template >
<div>
<component :is="comp"/>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
props: ['slug'],
data() {
return {
comp: 'ChildOne',
}
},
methods: {
setComponent () {
// assume prop slug passed from component or router is one of the components e.g. 'ChildOne'
this.comp = this.slug;
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
Option 3: Vue & Webpack Async and Dynamic components.
When it comes to larger applications or if you use Vuex and Vue Route where you have dynamic and large number of components then there are a number of approaches, but I'll stick to one. Similar to option 2, we are using the component element, but we are using WebPack to find all Vue files recursively with the keyword 'module'. We then load these dynamically / asynchronous --- meaning they will only be loaded when needed and you can see this in action in network console of browser. This means I can build components dynamically (factory pattern) and render them as needed. Example, of this might be if a user adds projects and you have to build and config views dynamically for projects created e.g. using vue router you passed it a ID for a new project, then you would need to dynamically load an existing component or build and load a factory built one.
Note: I'll use v-if on a component element if I have many components and I'm unsure the user will need them. I don't want to maintain state on large collections of components because I will end up memory and with loads of observers / watches / animations will most likely end up with CPU issues
<template >
<div>
<component :is="module" v-if="module"/>
</div>
</template>
<script>
const requireContext = require.context('./', true, /\.module\.vue$/);
const modules = requireContext.keys()
.map(file =>
[file.replace(/(.*\/(.+?)\/)|(\.module.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
// console.error("components", components)
components[name] = component.default || component
return components
}, {});
export default {
data() {
return {
module: [],
}
},
props: {
slug: {
type: String,
required: true
}
},
computed: {
getData() {
return this.data;
},
},
methods: {
setModule () {
let module = this.slug;
if (!module || !modules[module]) {
module = this.defaultLayout
}
this.module = modules[module]
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
My initial thought is having additional conditions within a loop would negatively impact performance?
I think you might be confused by this rule in the style guide that says:
Never use v-if on the same element as v-for.
It's only a style issue if you use v-if and v-for on the same element. For example:
<div v-for="user in users" v-if="user.isActive">
But it's not a problem if you use v-if in a "child" element of a v-for. For example:
<div v-for="user in users">
<div v-if="user.isActive">
Using v-if wouldn't have a more negative performance impact than a method. And I'm assuming you would have to do some conditional checks inside your method as well. Remember that even calling a method has some (very small) performance impact.
Once you use Vue, I think it's a good idea not to mix it up with JavaScript DOM methods (like insertBefore). Vue maintains a virtual DOM which helps it to figure out how best to update the DOM when your component data changes. By using JavaScript DOM methods, you won't be taking advantage of Vue's virtual DOM anymore.
By sticking to Vue syntax you also make your code more understandable and probably more maintainable other developers who might read or contribute to your code later on.

Can't stop Vue JS (v2) from reusing components

I have a Vue app that conditionally displays sets of the same component, I'd like to tie in to the created or mounted methods, but Vue keeps re-using earlier components and not triggering the methods. I've tried wrapping the components in keep-alive tags to no effect.
The code below shows what you would expect on the page: changing someVariable to 'test1' or 'test2' shows the relevant components, but the created/mounted methods only fire a maximum of 2 times (e.g. changing someVariable to 'test1' logs creation and mounting of 1 component with the label type1, then changing it to 'test2' only logs 1 more component with the label type3).
Currently using v2.1.10
HTML
<div id="app">
<div v-if="someVariable === 'test1'">
<keep-alive>
<test-component data-type="type1"></test-component>
</keep-alive>
</div>
<div v-if="someVariable === 'test2'">
<keep-alive>
<test-component data-type="type2"></test-component>
</keep-alive>
<keep-alive>
<test-component data-type="type3"></test-component>
</keep-alive>
</div>
</div>
JS/Vue
Vue.component('test-component', {
props: ['data-type'],
template: '<div><p>In the component: {{ dataType }}</p></div>',
created: function () {
console.log('component created: ', this.dataType);
},
mounted: function () {
console.log('component mounted: ', this.dataType);
}
});
var app = new Vue({
el: '#app',
data: {
someVariable: ""
}
});
You should use a watcher on your someVariable instead of trying to hook on created and mounted hooks.
Components are created, and mounted the first time they are visible (rendered). There are NO "shown" or "hidden" hooks.
See https://v2.vuejs.org/v2/guide/computed.html#Watchers:
watch: {
someVariable: function (newValue) {
// test newValue and do what you have to do!
}
}
For your specific example removing keep-alive from the second if should do the trick https://jsfiddle.net/z11fe07p/464/
An interesting thing is that vue seems to re-use the previously rendered component. So if you have one component in the first if when switching to the next if with 2 components it will re-use one component and create a new one for the second component from the block. When getting back to the first if block it will re-use one of the 2 already rendered components.
As mentioned above, a watcher is more suited for such cases, thus getting you rid of handling logic in places where you don't have full control. You can see this tip right here in the docs https://v2.vuejs.org/v2/api/#updated