Vue3 LocalStorage set after component render - javascript

I have a nav bar that loads user data, all of this happens after a user successfully logs into the application. The problem is, localStorage must be setting slightly after I load the nav bar. If I wrap it in a setTimeout() everything works but I would rather my variables be reactive in nature since they can change based on user activity.
Toolbar.vue
<template>
<!--begin::Toolbar wrapper-->
<div class="d-flex align-items-stretch flex-shrink-0">
<h2>check for value</h2>
<div v-if="activeAccountId">{{activeAccountId}}</div>
</div>
<!--end::Toolbar wrapper-->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "topbar",
data() {
let activeAccountId = ref(JSON.parse(localStorage.getItem('activeAccountId') || '{}')).value;
return {
activeAccountId
}
}
});
</script>
I've tried using watchers, and using setup() verses data(), but nothing seems to work properly. As I mentioned, setTimeout() does work but I'd rather avoid manually triggering a timeout and let vue handle things how it wants to.
Here's a simple example, I can't setup a dummy code side since it won't have the localStorage item set.
For some additional context, after the user is logged in, I am hitting the API with an async() to get the account information and storing the account data in localStorage. I'm guessing at the same time the router is trying to load the navbar area which is why the localStorage items aren't available when the component mounts.
I don't know the vue3 words to use, but ideally I would want some type of async/await call to localStorage because the ref() doesns't seem to be working how I thought it would. It's as if the ref() doesn't see localStorage get updated.
localStorage being synchronous is the main issue.

use mounted lifecycle hook. and initialize user information there

Vue calls the mounted() hook when the component is added to the DOM. You can try putting your initial code in the mounted method and also try to change your code like this
export default defineComponent({
name: "topbar",
data() {
return {
activeAccountId:""
}
},
mounted(){
this.activeAccountId = JSON.parse(localStorage.getItem('activeAccountId')|| '{}');
}
});

Related

How to share variables between functions in Vue

I'm learning Vue and trying to test out how I'd pass a user input from one function to another.
My use case is this. I want to take a users input, store it and then supply that to a second function as a variable that it can use to make an API call.
In isolation the two code sections are:
User Input
<div id="v-model-basic" class="demo">
<input v-model="message" placeholder="edit me" />
<p>Message is: {{ message }}</p>
</div>
Vue.createApp({
data() {
return {
message: ''
}
}
}).mount('#v-model-basic')
API Call
import axios from 'axios';
/* API call for the weather data */
export default {
name: "Weather",
data() {
return {
weatherList: []
};
},
methods: {
getweatherData() {
axios.get("https://anAPIpath.com/that-I-want-to-add-user-input-to").then(response => (this.weatherList = response.data));
}
}
};
Is there a way to do this in Vue where they are both methods within that export function, or is that just the wrong approach? As mentioned, new and learning Vue and I want to ensure I approach this the "Vue way" so things are as neat and tidy as possible.
Vue has several options for passing variables between components.
props
You can make the variable a prop of the other:
props: {
message: String
}
The prop is accessed with this.message but it is recommended you copy it to a variable in data and reflect changes with $emit (see documentation link, and $emit section below).
See documentation: https://v3.vuejs.org/guide/component-props.html#props
$emit
You can let a parent component know about the other component's changes with $emit. You can just name an event yourself and pass the value as the first parameter.
this.$emit('messageUpdate', this.message);
See documentation: https://v3-migration.vuejs.org/breaking-changes/emits-option.html#emits-option
VueX
With the VueX data storage you can transfer variables from any component across your app to any other.
This takes a little more finess to set up. I suggest going with the above first.
If you are interested (definitely worth to learn), here is the link:
https://vuex.vuejs.org/guide/#getting-started
I see you are doing well, you just need to add the API call to a script tag in the same file and then access the input message like this:
axios.get("https://anAPIpath.com/"+ this.message)

Content jump in the first milliseconds

I am creating dynamic pages using Nuxt. In the pages folder I have one file _url.vue. It contains the following code:
<template lang="pug">
div
component(
v-for="component in components"
:key="`${component.type}-${component.id}`"
:is="`the-${component.type}`"
)
</template>
<script>
// import vuex
export default {
computed: {
...mapGetters('app', {
components: 'getComponents'
})
}
}
</script>
setComponents happens at the middleware level:
export default async function ({ store }) {
await store.dispatch('app/setPage')
}
In the first milliseconds of page load, the content "jumps" as the components are rendered on the fly. How can this situation be corrected?
I'd first try to import the components manually, to see where this all comes from: the components taking some time to get injected or the layout being displayed, just to be sure.
Then, I had a discussion about it here, you may give it a look: Vue: wait to render until all components are mounted
There are several ways of handling this kind of micro-jumping explained there. You can choose your own solution. Also depends if you're using your app as universal or SPA only.
Looks like require is a way to go but some alternative are also available.

Vue Router params not updating in page

Hello I have some conditional logic in my view based on the current page
in setup I have
const curVidType = ref(route.params.videoTopic);
I return it and then print out like
<h1>Welcome to {{ curVidType }} videos</h1>
However it only works if I refresh. If I browse to a new page it stays the old one even though the browser url changes. after refresh it updates. Thanks so much for any help
Try adding :key to your Component to make sure it updates when param changes:
<your-component :key="$route.params.videoTopic"></your-component>
You're initializing the variable as a const, which means it does it once and then doesn't set it. If you change curVidType to a computed value, you can have it react to changes in the router params.
computed: {
curVidType() {
return route.params.videoTopics
}
}
This will have the value of curVidType set to change when videoTopics does
EDIT:
Having read the comments and looked at the comments and read some of the documentations, ref() will create a reactive object but I'm not sure it's reacting to the original object. But it looks like the toRef() function can create that bridge.
const curVidType = toRef(route.params, 'videoTopics')
Should allow for curVidType.value to also reflect changes to to route.params.videoTopics
Would be nice if you could share some more code with us in order to help you.
This should just work fine
<template>
{{ curVidType }}
</template>
<script>
import { useRoute } from 'vue-router';
export default {
setup() {
const { params } = useRoute();
const curVidType = params.videoTopic
return {
curVidType,
};
},
};
</script>

Vue - Call a method from a different component (uses vue-routes)

Here is my base App.vue that holds the router-view:
<template>
<div>
<Navbar/>
<div class="container-fluid">
<router-view></router-view>
</div>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue';
export default {
name: 'App',
components: {
Navbar
}
}
</script>
In the Navbar.vue, I have a method:
methods: {
getLoggedStatus(){
console.log('asdasd')
}
}
Lastly, I have a Login.vue that is loaded there on the router-view. I wanna acess the getLoggedStatus() from the Navbar.vue within Login.vue. How can I achieve this?
I tried putting a ref to the Navbar tag in App.vue:
<Navbar ref="navvy"/>
And calling in on the Login.vue with:
this.$refs.navvy.getLoggedStatus()
But it doesn't work.
A ref only works on children. You're rendering <Navbar> within app, so you cannot call that ref from login. You can only access this.$refs.navvy from App.vue.
There are several solutions for your problem.
Emit an event from Login to App, so App calls the method from a ref.
You can set a listener in the router-view, as:
<router-view #loggedStatus="callLoggedStatus" />
In your login, when you would want to call the navbar getLoggedStatus, you would instead emit that event:
this.$emit('loggedStatus')
And then in App.vue, you would defined a callLoggedStatus methods that call the ref:
callLoggedStatus() {
this.$refs.navvy.getLoggedStatus();
}
Given that you add the ref to the <Navbar> component in the APP template.
This solution is arguably the most similar to your proposed code, but I think it is a mess and you should avoid it, since you can end up listening to a lot of different events in your App.vue.
Use Vuex
I don't exactly know what getLoggedStatus does, but if you want to change how your navbar behaves when the user is logged in, you should probably setup a vuex store, so you register there wether the user is logged or not. Then in your navbar component you render things conditionally depending upon the user is logged or not.
#Lana's answer follows this idea, and is probably the closest to the official way to thins in Vue.
 Use an event emitter
If you want to directly communicate between components that are not in the same family, I think an event emitter is a reasonable choice. You could setup an application wide event emitter after creating the app:
const app = new Vue({...});
window.emitter = new Vue();
(in the example we use a new Vue as event emitter. There is also the 'events' module which allow to use a EventEmitter)
And then any component can use it to send messages, so Login could do:
window.emitter.$emit('user-logged', myCustomPayload);
And Navbar on the other hand could do:
window.emitter.$on('user-logged', function() {
this.getLoggedStatus();
})
This last option is not well considered in the Vue community -Vuex is preferred- but for small applications I think it is the simplest.
The dirty hack
You can always export your component to window. In the Navbar created hook you could do:
created() {
window.Navbar = this;
}
And then in Login.vue you could, at any time:
window.Navbar.getLoggedStatus()
And it will work. However, this is surely an anti pattern and can cause a lot of harm to your project maintainability if you start doing this with several components.
It looks like getLoggedStatus returns some global state of the app. Use Vuex for managing these global variables.
Create a store like this:
// Make sure to call Vue.use(Vuex) first if using a module system
const store = new Vuex.Store({
state: {
loggedStatus: 0
},
getters: {
getLoggedStatus: state => {
// to compute derived state based on store state
return state.loggedStatus
}
}
})
Use store.state.loggedStatus in any Vue-component to access the global state or use getters like store.getters.getLoggedStatus if you need to compute derived state based on store state.

How to destroy a VueJS component that is being cached by <keep-alive>

I have a Vue component that's kept alive using Vue's element for caching purposes. However, the problem I am having right now is that once I sign out of one account and create a new account on my Vue application, the component I'm "keeping alive" is being reflected for the new user (which obviously isn't relevant for the new user).
As a result, I want to destroy that component once the user signs out. What is the best way to go about this?
I've managed to solve my issue in the following way. Essentially, if the user is logged in, keep the dashboard alive. Else, don't keep the dashboard alive. I check if the user is logged in or out every time the route changes by "watching" the route (see below). If you are reading this and have a more elegant solution - I'd love to hear it.
The following is the code for my root component
<template>
<div id="app">
<!-- if user is logged in, keep dashboard alive -->
<keep-alive
v-bind:include="[ 'dashboard' ]"
v-if="isLoggedIn">
<router-view></router-view>
</keep-alive>
<!-- otherwise don't keep anything alive -->
<router-view v-else></router-view>
</div>
</template>
<script>
import firebase from "firebase";
export default {
name: 'app',
data() {
return {
isLoggedIn: false // determines if dashboard is kept alive or not
}
},
watch: {
$route (to, from){ // if the route changes...
if (firebase.auth().currentUser) { // firebase returns null if user logged out
this.isLoggedIn = true;
} else {
this.isLoggedIn = false;
}
}
}
}
</script>
I had the same problem and I solved it by using an array of cached components and bus event.
Here is my HTML keep-alive App.vue:
<keep-alive :include="cachedComponents">
<router-view></router-view>
</keep-alive>
Here is what I'm doing in the created() life cycle:
created() {
// Push Home component in cached component array if it doesn't exist in the array
if (!this.cachedComponents.includes('Home')) {
this.cachedComponents.push('Home')
}
// Event to remove the components from the cache
bus.$on('clearCachedComponents', (data) => {
// If the received component exist
if (this.cachedComponents.includes(data)) {
// Get the index of the component in the array
const index = this.cachedComponents.indexOf(data)
// Remove it from the array
this.cachedComponents.splice(index, 1)
}
})
}
And inside another component just trigger the event and send the component to remove in parameter.
Another.vue
bus.$emit('clearCachedComponents', 'Home')
If you don't know how to make a bus event there are lot of tutorials on the internet like this to do that. But bus event is my way to do that and you can use everything you want like a child emitter or Vuex. That I want to show is to use an array of components to manage your cache. All you have to do is to add or remove your components in the array.
for anyone looking for a solution that destroys the cache
in my case I was using this in a logout route, replace router.app with this.$root in Vue instances and the $children index/nesting may differ for your app
setTimeout(() => {
var d = [];
for(var vm of router.app.$children[0].$children) {
if(vm._inactive === true)
d.push(vm);
}
for(var vm of d) {
vm.$destroy();
}
});
If your problem is that the component is still holding the old user's data, the only option is resetting it with an internal reset function, which reloads the data for the new user one way or another.
See:
http://jsfiddle.net/paolomioni/hayskdy8/
var Home = Vue.component('Home', {
template: `<div><h1>{{ title }}</h1>
<input type="button" value="click to change text" v-on:click="title = Math.random()"">
<input type="button" value="click to reset component" v-on:click="reset"></div>`,
data: () => {
return {
title: 'BBB'
}
},
methods: {
reset() {
this.title = 'BBB'
}
}
});
In the fiddle, click on the button "change text" to change the text: if you click the checkbox twice to switch view and back again, you will see that the number you've generated is still kept in memory. If you click on the "reset" button, it will be reset to its initial state. You need to implement the reset method on your component and call it programmaticaly when the user logs out or when the new user logs in.

Categories

Resources