How do I keep vue-router alive with different params separately?
TL:DR:
Let's consider an example when we're developing a website like facebook. Each user has a profile page. Because there are a lot of users we don't want to iterate all users and load all profile page on load like bellow
<template v-for="profile in profilePages">
<profile-page :data="profile" v-show="this.route.params['id'] === channel.id"/>
</template>
The common approach would be:
router.js:
{
component: ProfileWrapper,
path: '/profile',
children: [
{
path: ':id',
component: ProfilePage
}
]
}
ChannelsPage:
<keep-alive>
<router-view :=key="$route.fullPath"></router-view>
</keep-alive>
But here's the issue. Since user visits someone's page and navigates away, I want the router to keep it alive in cache somewhere, or just hide it. In my particular case, user visits 2-3 profile at most and switches a lot between them. And switching operation is time costly, because there are a lot of DOM in it.
Can I do it with vue-router and keep-alive?
EDIT:
Please check the sandbox. Each time you switch between pages (#1,#2,#3,#4) Vue creates new components ProfileInnerComponent from the scratch (not from the cache like v-show). That's noticeably by checking red div, the create hook of ProfileInnerComponent is called, which emits the event, and App adds the div with current time.
I changed your sandbox by adding this codes:
// in App.vue
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
and
//in Profile.vue
<keep-alive>
<profile-inner-component v-for="i in comps" :key="i" :data="i"/>
</keep-alive>
In order this to work you need unique names on your components, which you would then use the include property on <keep-alive>.
<keep-alive include="Foo,Bar">
...
</keep-alive>
In you case, you would be better served using dynamic components rather than a single route.
<component :is="$route.params.id"></component>
keep-alive with Dynamic Components
keep-alive API reference
update
Pre-fetching channel content based on the query param id:
// routes.js
routes = [
{
path: '/channel/:id',
name: 'show.channel',
props: true,
component: Channel
}
...
]
// Channel.vue
import axios from 'axios'
export default {
data () {
return {
content: ''
}
}
beforeRouteEnter(to,from,next) {
axios.get('/api/channel/' + to.params.id).then(response => {
next(vm => {
vm.content = reponse.data
})
})
},
watch: {
'$route' (to, from) {
// fetch new channel content when a query param is changed.
}
}
}
I am not sure if i understand correctly but lets suppose you have 2 pages.
Admins and Users and each page has an counter which is initially 0.
So you want when you are in Admins page and you increasing the counter,when navigating to another page and return back to Admins page the counter to be as you have left it.
I made a sandbox for this.Also please keep open the console to see how many times the components is rendered.
NOTE that in the sandbox example,is illustrated the logic how to achieve this.Never use keep-alive in router-view in App.vue.
Related
I have setup a route in vue-router 4 that should load a component dynamically depending on whether the user is logged in or not. I did it like this (there may be a better way?):
import Personal from '../views/Personal.vue';
import Public from '../views/Public.vue';
routes: [
{
path: '/',
component: async () => {
const isLoggedIn = await authenticateUser();
if (isLoggedIn == true) {
return Personal
} else {
return Public
}
}
}
]
The App.vue file is this:
<template>
<div id="app">
<Site-Header></Site-Header>
<router-view></router-view>
<Site-Footer></Site-Footer>
</div>
</template>
The problem is that if a user logs in from the homepage route with path of '/', he doesn't navigate away from this route. Instead I would like vue-router to just load the Personal component instead.
The switch between Personal and Public only seems to work if I hard refresh the page, otherwise no changes happen. So if a user logs in, they still see the Public.vue component, then after a page refresh they see the Personal.vue component. If they then logout, they still see the Personal.vue component until they refresh the page.
How could I force vue-router to analyse the route after log-in/log-out and load the correct component?
To have multiple routes utilizing the same path, your best bet is using Named Views. You can define the default component for your index, or / path to be your Public component, while conditionally selecting a different component using v-if in your template.
You could define your routes as:
routes: [
{
components: {
default: Public,
Personal: Personal
},
name: "index",
path: "/"
}
]
Important to note that the syntax here differs. The component field here has to be pluralized in order for this to work, so it has to be components.
Then in your root template that's calling the views, you can then use a simple v-if to switch between them, depending on whether the user is logged in or not. How you store that information is up to you, so just adapt the below code to reflect your own app's logic
<!-- The default is Public, so you don't have to provide the name here -->
<router-view v-if="!user.loggedIn" />
<router-view v-else name="Personal" />
You can see this in this codesandbox
I have encountered a weird case when using VueX and Vue-Router and I am not too sure how to cleanly solve it.
I have a component (let's call it "ComponentWithStore") that registers a named store module a bit like this : (the actual content of the store don't matter. obviously in this toy example using VueX is overkill, but this is a very simplified version of a much more complexe app where using VueX makes sense)
// ComponentWithStore.vue
<script>
import module from './componentStore.js';
export default {
name: 'ComponentWithStore',
beforeCreate() {
this.$store.registerModule(module.name, module);
},
beforeDestroy() {
this.$store.unregisterModule(module.name);
}
}
</script>
Then I place this component in a view (or page) which is then associated to a route (let's call this page "Home").
// Home.vue
<template>
<div class="home">
Home
<ComponentWithStore/>
</div>
</template>
<script>
import ComponentWithStore from '#/components/ComponentWithStore.vue';
export default {
name: "Home",
components: { ComponentWithStore }
};
</script>
So far so good, when I visit the Home route, the store module is registered, and when I leave the Home route the store module is cleaned up.
Let's say I then create a new view (page), let's call it "About", and this new About page is basically identical to Home.vue, in that it also uses ComponentWithStore.
// About.vue
<template>
<div class="about">
About
<ComponentWithStore/>
</div>
</template>
<script>
import ComponentWithStore from '#/components/ComponentWithStore.vue';
export default {
name: "About",
components: { ComponentWithStore }
};
</script>
Now I encounter the following error when navigating from Home to About :
vuex.esm.js?2f62:709 [vuex] duplicate namespace myComponentStore/ for the namespaced module myComponentStore
What happens is that the store module for "About" is registered before the store module for "Home" is unregistered, hence the duplicate namespace error.
So I understand well what the issue is, however I am unsure what would be the cleanest solution to solve this situation. All ideas are welcome
A full sample may be found here : https://github.com/mmgagnon/vue-module-router-clash
To use, simply run it and switch between the Home and About pages.
As you have mentioned, the issue is due to the ordering of the hooks. You just need to use the correct hooks to ensure that the old component unregisters the module first before the new component registers it again.
At a high level, here is the order of hooks in your situation when navigating from Home to About:
About beforeCreate
About created
Home beforeDestroy
Home destroyed
About mounted
So you can register the module in the mounted hook and unregister it in either beforeDestroy or destroyed.
I haven't tested this though. It might not work if your component requires access to the store after it is created and before it is mounted.
A better approach is to create an abstraction to register and unregister modules that allows for overlaps.
Untested, but something like this might work:
function RegistrationPlugin(store) {
const modules = new Map()
store.registerModuleSafely = function (name, module) {
const count = modules.get(name) || 0
if (count === 0) {
store.registerModule(name, module)
}
modules.set(name, count + 1)
}
store.unregisterModuleSafely = function (name) {
const count = modules.get(name) || 0
if (count === 1) {
store.unregisterModule(name)
modules.delete(name)
} else if (count > 1) {
modules.set(name, count - 1)
}
}
}
Specify the plugin when you create your store:
const store = new Vuex.Store({
plugins: [RegistrationPlugin]
})
Now register and unregister your modules like this:
beforeCreate() {
this.$store.registerModuleSafely(module.name, module)
},
destroyed() {
this.$store.unregisterModuleSafely(module.name)
}
I use this.$router.push({name: 'nameComponent', params: {data:someObject}}) to switch pages. On the next page, params are detected when the page is first loaded. However, after reloading the page, params are no longer detected.
FYI, route configuration in Nuxt JS already uses history mode
You are using your router to share state between different components. On refreshing a route, the original data that was passed from page A to B is not retained - that behaviour is expected since you aren't accessing page A which is fetching the data.
If you want to be able to access data after a page refresh, you either need to fetch it again from the source in page B, cache it locally or change the way your components and routes work.
From Source
Fetching it from the source would mean you don't pass the actual data in the route, just the id and then fetch the data again in page B.
Downside
You might be fetching the same data twice
Local Storage
You can persist data you loaded in page A. You can either do this directly by
saving directly to local storage
using a library like localForage
using Vuex and the vuex-persist plugin
In page B you could access your local storage and retrieve the data from there.
Downside
This would still require the user to have visited page A at some stage and if that was a long time ago, data might be outdated if the user keeps visiting page B directly. It also means that you'd have to check if the data exists since the user might have cleared the cache.
Adjusting component and route architecture
Probably the way to go. You can create a parent component that fetches the data, and two child components that get the data passed as props, e.g.:
// Routes
cont routes = {
path: '',
name: 'parent',
compoent: Parent.vue,
children: [{
path: '/pageA'
name: 'pageA',
component: PageA
},
{
path: '/pageB'
name: 'pageB',
component: PageB
}
]
// Parent.vue
<template>
<router-view :user="user" />
</template>
<script>
export default {
data() {
return {
user: null
}
}
},
created() {
...fetch data
}
</script>
// PageA.vue & PageB.vue
<template>
...
</template>
<script>
export default {
props: {
user: {
type: Object,
require: true
}
}
}
</script>
Without reloading the whole page I need to reload the current route again (Only a component reload) in a vue app.
I am having a path in vue router like below,
{
path: "/dashboard",
name: "dashboard",
component: loadView("Dashboard"),
},
When user clicks on the Dashboard navigation item user will be redirected to the Dashboard page with vue router programmatic navigation
this.$router.push({ name: "dashboard" });
But when user already in the dashboard route and user clicks the Dashboard nav item again nothing happens. I think this is vue router's default behaviour. But I need to force reload the Dashboard component (Not to refresh the whole page).
I can't use beforeRouteUpdate since the router is not updated. Also I have tried the global before guards like beforeEach. But it is also not working.
How can I force reload the dashboard component without reloading the whole page?
It can be done in two ways.
1) Try doing vm.$forceUpdate(); as suggested here.
2) You can take the strategy of assigning keys to children, but whenever you want to re-render a component, you just update the key.
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
Every time that forceRerender is called, the prop componentKey will change. When this happens, Vue will know that it has to destroy the component and create a new one.
What you get is a child component that will re-initialize itself and “reset” its state.
Not mentioned here, but as the offered solutions require a lot of additional work just to get the app to render correctly, which imo is a brittle solution.. we have just implemented another solution which works quite well..
Although it is a total hack.
if (this.$route.name === redirect.name) {
// this is a filthy hack - the vue router will not reload the current page and then have vue update the view.
// This hack routes to a generic page, then after this has happened the real redirect can happen
// It happens on most devices too fast to be noticed by the human eye, and in addition does not do a window
// redirect which breaks the mobile apps.
await this.$router.push({
name: RouteNames.ROUTE_REDIRECT_PLACEHOLDER
});
}
... now continue to do your normal redirect.
Essentially, redirect to a placeholder, await the response but then immediately continue to another page you actually wanted to move toward
So I'm building an application using Laravel Spark, and therefore taking the opportunity to learn some Vue.js while I'm at it.
It's taken longer for me to get my head around it than I would have liked but I have nearly got Vue-multiselect working for a group of options, the selected options of which are retrieved via a get request and then updated.
The way in which I've got this far may well be far from the best, so bear with me, but it only seems to load the selected options ~60% of the time. To be clear - there are never any warnings/errors logged in the console, and if I check the network tab the requests to get the Tutor's instruments are always successfully returning the same result...
I've declared a global array ready:
var vm = new Vue({
data: {
tutorinstruments: []
}
});
My main component then makes the request and updates the variable:
getTutor() {
this.$http.get('/get/tutor')
.then(response => {
this.tutor = response.data;
this.updateTutor();
});
},
updateTutor() {
this.updateTutorProfileForm.profile = this.tutor.profile;
vm.tutorinstruments = this.tutor.instruments;
},
My custom multiselect from Vue-multiselect then fetches all available instruments and updates the available instruments, and those that are selected:
getInstruments() {
this.$http.get('/get/instruments')
.then(response => {
this.instruments = response.data;
this.updateInstruments();
});
},
updateInstruments() {
this.options = this.instruments;
this.selected = vm.tutorinstruments;
},
The available options are always there.
Here's a YouTube link to how it looks if you refresh the page over and over
I'm open to any suggestions and welcome some help please!
Your global array var vm = new Vue({...}) is a separate Vue instance, which lives outside your main Vue instance that handles the user interface.
This is the reason you are using both this and vm in your components. In your methods, this points to the Vue instance that handles the user interface, while vm points to your global array that you initialized outside the Vue instance.
Please check this guide page once more: https://v2.vuejs.org/v2/guide/instance.html
If you look at the lifecycle diagram that initializes all the Vue features, you will notice that it mentions Vue instance in a lot of places. These features (reactivity, data binding, etc.) are designed to operate within a Vue instance, and not across multiple instances. It may work once in a while when the timing is right, but not guaranteed to work.
To resolve this issue, you can redesign your app to have a single Vue instance to handle the user interface and also data.
Ideally I would expect your tutorinstruments to be loaded in a code that initializes your app (using mounted hook in the root component), and get stored in a Vuex state. Once you have the data in your Vuex state, it can be accessed by all the components.
Vuex ref: https://vuex.vuejs.org/en/intro.html
Hope it helps! I understand I haven't given you a direct solution to your question. Maybe we can wait for a more direct answer if you are not able to restructure your app into a single Vue instance.
What Mani wrote is 100% correct, the reason I'm going to chime in is because I just got done building a very large scale project with PHP and Vue and I feel like I'm in a good position to give you some advice / things I learned in the process of building out a PHP (server side) website but adding in Vue (client side) to the mix for the front end templating.
This may be a bit larger than the scope of your multiselect question, but I'll give you a solid start on that as well.
First you need to decide which one of them is going to be doing the routing (when users come to a page who is handling the traffic) in your web app because that will determine the way you want to go about using Vue. Let's say for the sake of discussion you decide to authenticate (if you have logins) with PHP but your going to handle the routing with Vue on the front end. In this instance your going to want to for sure have one main Vue instance and more or less set up something similar to this example from Vue Router pretending that the HTML file is your PHP index.php in the web root, this should end up being the only .php file you need as far as templating goes and I had it handle all of the header meta and footer copyright stuff, in the body you basically just want one div with the ID app.
Then you just use the vue router and the routes to load in your vue components (one for each page or category of page works easily) for all your pages. Bonus points if you look up and figure using a dynamic component in your main app.vue to lazy load in the page component based on the route so your bundle stays small.
*hint you also need a polyfill with babel to do this
template
<Component :is="dynamicComponent"/>
script
components: {
Account: () => import('./Account/Account.vue'),
FourOhFour: () => import('../FourOhFour.vue')
},
computed: {
dynamicComponent() {
return this.$route.name;
}
},
Now that we are here we can deal with your multiselect issue (this also basically will help you to understand an easy way to load any component for Vue you find online into your site). In one of your page components you load when someone visits a route lets say /tutor (also I went and passed my authentication information from PHP into my routes by localizing it then using props, meta fields, and router guards, its all in that documention so I'll leave that to you if you want to explore) on tutor.vue we will call that your page component is where you want to call in multiselect. Also at this point we are still connected to our main Vue instance so if you want to reference it or your router from tutor.vue you can just use the Vue API for almost anything subbing out Vue or vm for this. But the neat thing is in your main JS file / modules you add to it outside Vue you can still use the API to reference your main Vue instance with Vue after you have loaded the main instance and do whatever you want just like you were inside a component more or less.
This is the way I would handle adding in external components from this point, wrapping them in another component you control and making them a child of your page component. Here is a very simple example with multiselect pretend the parent is tutor.vue.
Also I have a global event bus running, thought you might like the idea
https://alligator.io/vuejs/global-event-bus/
tutor.vue
<template>
<div
id="user-profile"
class="account-content container m-top m-bottom"
>
<select-input
:saved-value="musicPreviouslySelected"
:options="musicTypeOptions"
:placeholder="'Choose an your music thing...'"
#selected="musicThingChanged($event)"
/>
</div>
</template>
<script>
import SelectInput from './SelectInput';
import EventBus from './lib/eventBus';
export default {
components: {
SelectInput
},
data() {
return {
profileLoading: true,
isFullPage: false,
isModalActive: false,
slackId: null,
isActive: false,
isAdmin: false,
rep: {
id: null,
status: '',
started: '',
email: '',
first_name: '',
},
musicTypeOptions: []
};
},
created() {
if (org.admin) {
this.isAdmin = true;
}
this.rep.id = parseInt(this.$route.params.id);
this.fetchData();
},
mounted() {
EventBus.$on('profile-changed', () => {
// Do something because something happened somewhere else client side.
});
},
methods: {
fetchData() {
// use axios or whatever to fetch some data from the server and PHP to
// load into the page component so say we are getting the musicTypeOptions
// which will be in our selectbox.
},
musicThingChanged(event) {
// We have our new selection "event" from multiselect so do something
}
}
};
</script>
this is our child Multiselect wrapper SelectInput.vue
<template>
<multiselect
v-model="value"
:options="options"
:placeholder="placeholder"
label="label"
track-by="value"
#input="inputChanged" />
</template>
<script>
import Multiselect from 'vue-multiselect';
export default {
components: { Multiselect },
props: {
options: {
type: [Array],
default() {
return [];
}
},
savedValue: {
type: [Array],
default() {
return [];
}
},
placeholder: {
type: [String],
default: 'Select Option...'
}
},
data() {
return {
value: null
};
},
mounted() {
this.value = this.savedValue;
},
methods: {
inputChanged(selected) {
this.$emit('selected', selected.value);
}
}
};
</script>
<style scoped>
#import '../../../../../node_modules/vue-multiselect/dist/vue-multiselect.min.css';
</style>
Now you can insure you are manging the lifecycle of your page and what data you have when, you can wait until you get musicTypeOptions before it will be passed to SelectInput component which will in turn set up Multiselect or any other component and then handle passing the data back via this.$emit('hihiwhatever') which gets picked up by #hihiwhatever on the component in the template which calls back to a function and now you are on your way to do whatever with the new selection and pass different data to SelectInput and MultiSelect will stay in sync always.
Now for my last advice, from experience. Resist the temptation because you read about it 650 times a day and it seems like the right thing to do and use Vuex in a setup like this. You have PHP and a database already, use it just like Vuex would be used if you were making is in Node.js, which you are not you have a perfectly awesome PHP server side storage, trying to manage data in Vuex on the front end, while also having data managed by PHP and database server side is going to end in disaster as soon as you start having multiple users logged in messing with the Vuex data, which came from PHP server side you will not be able to keep a single point of truth. If you don't have a server side DB yes Vuex it up, but save yourself a headache and wait to try it until you are using Node.js 100%.
If you want to manage some data client side longer than the lifecycle of a page view use something like https://github.com/gruns/ImmortalDB it has served me very well.
Sorry this turned into a blog post haha, but I hope it helps someone save themselves a few weeks.