Stop full page re-render on route change? (Nuxt.js) - vue.js

I'm trying to implement custom routing in Nuxt using _.vue. My _.vue page file has two child components (let's call them Category and Product), each of which is displayed when their data is present in the store by using v-if. I'm using middleware for the _.vue component to process custom routing.
My problem is that all my components get re-rendered on each route change, which causes delays and image flickering.
Let me explain what I'm trying to achieve. There's a list of products in a category. When you click on a product, it opens in a modal window with the category still in the background, and also changes the current page URL, hence the routing thing. When you close the product, it should go back to the category in the same state as before. Everything seems to be working as needed, except when I open or close a product, all my components components get re-rendered (including _.vue). I tried naming them, using key(), etc. with no results.
Is there any way to keep current components on route change without rerendering? Or is there a workaround?
<template>
<div>
<Category v-if="current_category" />
<Product v-if="current_product" />
</div>
</template>
<script>
import {mapState} from 'vuex'
import Product from '~/components/product/Product'
import Category from '~/components/category/Category'
export default {
middleware: 'router',
scrollToTop: false,
components: {
Product,
Category,
},
computed: {
...mapState({
current_category: state => state.current_category,
current_product: state => state.current_product,
}),
},
mounted: function() {
console.log('_ component mounted')
},
}
</script>

You should use "key" option in page components. By default value is "route.fullPath", so you see rerendering after changing URL parameters.
https://nuxtjs.org/docs/2.x/features/nuxt-components#the-nuxt-component

Related

I cannot watch Vuex state in components within v-tabs until first visit

vuetify: ^2.5.10
vuex: ^3.6.2
vue: ^2.6.14
I have an application developed in v-tabs. In the child component in the first of these tabs, I have a
<v-file-input
v-model="file"
outlined
:rules="selector_rules"
:clearable="false"
:show-size="1000">
</v-file-input>
<v-btn color="primary" #click="commitData" class="mr-2">Commit</v-btn>
with
commitData() {
this.$store.commit('setFile', this.file)
},
The store gets correctly set because the following computed property in the parent component
computed: {
...mapGetters({
file: 'getFile',
}),
},
seems to work: I watch it in this way:
file: {
deep: true,
immediate: true,
handler() {
console.log("Received file: " + this.file);
}
},
The surprising thing, at least to me, is that the very same watch above, implemented in another component in the second tab, does not work until I visit (i.e., switch to) the second panel. However, the computed property works in this component because in the template I can see the new property.
After the first visit to the tab, the watch continues to work without problems.
Is there a reason for that and a way to avoid visiting it to make the watch work?
v-tabs loads content only on visit. Which means component will mount only when it's active.
You can use eager prop on your v-tab-item components to make sure they're always loaded and mounted.
API Docs reference: https://vuetifyjs.com/en/api/v-tab-item/#props-eager

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"
}
}

How to place component in the header with lazy loading?

In my vue application, how to place component (slot?) in the toolbar component?
My app for example:
<template>
<toolbar>…</toolbar>
<router-view />
</template>
and all the routes are lazy loaded.
for some routes I want to place component inside toolbar component. But I can't "insert" the component as slot. and to write the component and turn on/off with v-if seems to me wrong.
I think that I expect is
<div for="toolbar">This content should in toolbar</div>
<div for="router-view">This content for router-view</div>
Is there any way to solve this?
Vue Router Named Views will come in handy.
Sometimes you need to display multiple views at the same time instead
of nesting them, e.g. creating a layout with a sidebar view and a main
view. This is where named views come in handy. Instead of having one
single outlet in your view, you can have multiple and give each of
them a name. A router-view without a name will be given default as its
name.
A view is rendered by using a component, therefore multiple views require multiple components for the same route. Make sure to use the components (with an s) option:
<template>
<toolbar><router-view name="toolbar"></router-view></toolbar>
<router-view />
</template>
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: YourAwesomeComponent,
toolbar: YetAnotherAwesomeComponent
}
},
{
path: '/home',
components: {
default: YourAwesomeHomeComponent,
toolbar: YetAnotherAwesomeComponentThatSouldBeInToolbarOnHomePage
}
}
]
})

Two way binding between controller in routerview and parent

I have a single page application which consists of a:
A navigation component which contains a title and a menu
A router-view
I'd like each component being presented in the router view which correspond to a state in the router, to update the title of the navigation component. How to go about passing the parameters from the components in the router-view to the outer navigation component.
I'm using vue 2.0
I would recommending Vuex for this:
https://github.com/vuejs/vuex
This way you can access your Vuex store in the main component and display its title:
computed: {
title () {
return this.$store.state.title
}
}
Then you'd set up a mutation to update the title and call this wherever required in your components. Now as Vue is reactive the title within the nav component will update.
Below is an abbreviated example:
Vuex Store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
title: ''
},
mutations: {
updateTitle (state, title) {
state.title = title
}
}
})
Your App Root:
import Vue from 'vue'
import store from './vuex/store'
new Vue({
el: '#app',
store,
})
Nav component:
export default {
computed: {
title () {
return this.$store.state.title
}
},
...
Any other component which needs to update your title:
import { mapMutations } from 'vuex'
export default {
methods: {
...mapMutations([
'updateTitle' // map this.updateTitle() to this.$store.commit('updateTitle')
]),
},
// if you wanted to call it on mount
mounted () {
this.updateTitle('hello')
},
...
You can just have a contain-all Main component which stores the current route, and pass it to the navigation and the router view component as the their props, then the situations are:
when Main's stored route changes, the nav will rerender accordingly.
for the router view component, watch the prop change, this.$router.push('/some/where') when it changes.
when the user clicks an item on the nav, it emits an event, along with the data (the route the user wants to go to), the parent receives the event, changes its stored route, which, according to the last 2 bullets, changes how nav and router view looks.
The routes have to be part of the parent, since vue uses a parent-to-child-only one-way data flow. Children can't communicate with each other without help from their common parent unless we use some approach described in the last paragrath.
For details on how to implement parent-child data sharing, see doc.
Above is the simplest solution, which gets dirty quickly when your routes go nested. If so, you'll need a global event bus for non-parent-child data passing, or, vuex if you're looking at a large app and want your global states contained well.