How to change data in vue-component - javascript

In Vue component I have some data that comes from a localStorage.
if (localStorage.getItem("user") !== null) {
const obj_user = localStorage.getItem('user');
var user = JSON.parse(obj_user);
} else {
user = null;
}
return {
user
}
After in this component I have to check this data. I do it like this
<li v-if="!user">Login</li>
<li v-if="user">
<span>{{user.name}}</span>
</li>
But the data does not change immediately, but only after the page is reloaded. Right after I login in on the page and redirected the user to another page, I still see the Login link.
What am I doing wrong?
Maybe there is some other way how to check and output data from the localstorage in component?
Thank you in advance.

It looks like the issue may be related to they way that your user variable is not a piece of reactive stateful data. There isn't quite enough code in your question for us to determine that for sure, but it looks like you are close to grasping the right way to do it in Vue.
I think my solution would be something like this...
export default {
data() {
return {
user: null,
}
},
methods: {
loadUser() {
let user = null;
if (localStorage.getItem("user")) {
const obj_user = localStorage.getItem("user");
user = JSON.parse(obj_user);
}
this.user = user;
}
},
created() {
this.loadUser();
}
}
I save the user in the data area so that the template will react to it when the value changes.
The loadUser method is separated so that it can be called from different places, like from a user login event, however I'm making sure to call it from the component's created event.
In reality, I would tend to put the user into a Vuex store and the loadUser method in an action so that it could be more globally available.

Related

Possible/How to use a VueJS component multiple times but only execute its created/mounted function once?

I am trying to create a VueJS component that does the following: 1) download some data (a list of options) upon mounted/created; 2) display the downloaded data in Multiselct; 3) send selected data back to parent when user is done with selection. Something like the following:
<template>
<div>
<multiselect v-model="value" :options="options"></multiselect>
</div>
</template>
<script>
import Multiselect from 'vue-multiselect'
export default {
components: { Multiselect },
mounted() {
this.getOptions();
},
methods:{
getOptions() {
// do ajax
// pass response to options
}
},
data () {
return {
value: null,
options: []
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
This is mostly straightforward if the component is only called once in a page. The problem is I may need to use this component multiple times in one page, sometimes probably 10s of times. I don't want the function to be called multiple times:
this.getOptions();
Is there a way to implement the component somehow so no matter how many times it is used in a page, the ajax call will only execute once?
Thanks in advance.
Update: I assume I can download the data in parent then pass it as prop if the component is going to be used multiple times, something like the following, but this defies the purpose of a component.
props: {
optionsPassedByParents: Array
},
mounted() {
if(this.optionsPassedByParents.length == 0)
this.getOptions();
else
this.options = this.optionsPassedByParents;
},
The simple answer to your question is: you need a single place in charge of getting the data. And that place can't be the component using the data, since you have multiple instances of it.
The simplest solution is to place the contents of getOptions() in App.vue's mounted() and provide the returned data to your component through any of these:
a state management plugin (vue team's recommendation: pinia)
props
provide/inject
a reactive object (export const store = reactive({/* data here */})) placed in its own file, imported (e.g: import { store } from 'path/to/store') in both App.vue (which would populate it when request returns) and multiselect component, which would read from it.
If you don't want to request the data unless one of the consumer components has been mounted, you should use a dedicated controller for this data. Typically, this controller is called a store (in fairness, it should be called storage):
multiselect calls an action on the store, requesting the data
the action only makes the request if the data is not present on the store's state (and if the store isn't currently loading the data)
additionally, the action might have a forceFetch param which allows re-fetching (even when the data is present in state)
Here's an example using pinia (the official state management solution for Vue). I strongly recommend going this route.
And here's an example using a reactive() object as store.
I know it's tempting to make your own store but, in my estimation, it's not worth it. You wouldn't consider writing your own Vue, would you?
const { createApp, reactive, onMounted, computed } = Vue;
const store = reactive({
posts: [],
isLoading: false,
fetch(forceFetch = false) {
if (forceFetch || !(store.posts.length || store.isLoading)) {
store.isLoading = true;
try {
fetch("https://jsonplaceholder.typicode.com/posts")
.then((r) => r.json())
.then((data) => (store.posts = data))
.then(() => (store.isLoading = false));
} catch (err) {
store.isLoading = false;
}
}
},
});
app = createApp();
app.component("Posts", {
setup() {
onMounted(() => store.fetch());
return {
posts: computed(() => store.posts),
};
},
template: `<div>Posts: {{ posts.length }}</div>`,
});
app.mount("#app");
<script src="https://unpkg.com/vue/dist/vue.global.prod.js"></script>
<div id="app">
<Posts v-for="n in 10" :key="n" />
</div>
As you can see in network tab, in both examples data is requested only once, although I'm mounting 10 instances of the component requesting the data. If you don't mount the component, the request is not made.

How to make data from localStorage reactive in Vue js

I am using localStorage as a data source in a Vue js project. I can read and write but cannot find a way to use it reactively. I need to refresh to see any changes I've made.
I'm using the data as props for multiple components, and when I write to localStorage from the components I trigger a forceUpdate on the main App.vue file using the updateData method.
Force update is not working here. Any ideas to accomplish this without a page refresh?
...............
data: function () {
return {
dataHasLoaded: false,
myData: '',
}
},
mounted() {
const localData = JSON.parse(localStorage.getItem('myData'));
const dataLength = Object.keys(localData).length > 0;
this.dataHasLoaded = dataLength;
this.myData = localData;
},
methods: {
updateData(checkData) {
this.$forceUpdate();
console.log('forceUpdate on App.vue')
},
},
...............
Here's how I solved this. Local storage just isn't reactive, but it is great for persisting state across refreshes.
What is great at being reactive are regular old data values, which can be initialized with localStorage values. Use a combination of a data values and local storage.
Let's say I was trying to see updates to a token I was keeping in localStorage as they happened, it could look like this:
const thing = new Vue({
data(){
return {
tokenValue: localStorage.getItem('id_token') || '',
userValue: JSON.parse(localStorage.getItem('user')) || {},
};
},
computed: {
token: {
get: function() {
return this.tokenValue;
},
set: function(id_token) {
this.tokenValue = id_token;
localStorage.setItem('id_token', id_token)
}
},
user: {
get: function() {
return this.userValue;
},
set: function(user) {
this.userValue = user;
localStorage.setItem('user', JSON.stringify(user))
}
}
}
});
The problem initially is I was trying to use localStorage.getItem() from my computed getters, but Vue just doesn't know about what's going on in local storage, and it's silly to focus on making it reactive when there's other options. The trick is to initially get from local storage, and continually update your local storage values as changes happen, but maintain a reactive value that Vue knows about.
For anyone facing the same dilemma, I wasn't able to solve it the way that I wanted but I found a way around it.
I originally loaded the data in localStorage to a value in the Parent's Data called myData.
Then I used myData in props to populate the data in components via props.
When I wanted to add new or edit data,
I pulled up a fresh copy of the localStorage,
added to it and saved it again,
at the same time I emit the updated copy of localStorage to myData in the parent,
which in turn updated all the data in the child components via the props.
This works well, making all the data update in real time from the one data source.
As items in localstorage may be updated by something else than the currently visible vue template, I wanted that updating function to emit a change, which vue can react to.
My localstorage.set there does this after updating the database:
window.dispatchEvent(new CustomEvent('storage-changed', {
detail: {
action: 'set',
key: key,
content: content
}
}));
and in mounted() I have a listener which updates forceRedraw, which - wait for it - force a redraw.
window.addEventListener('storage-changed', (data) => {
this.forceRedraw++;
...

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.

Prevent componentDidMount from fetching data if already available from server-side

ComponentDidMount() is triggered when the component is mounted, including when it is hydrated following server-side rendering.
One of the solutions I found online is checking whether we have data in the state; however this requires a lot of code to include on every component. What are other solutions?
componentDidMount() {
// if rendered initially, we already have data from the server
// but when navigated to in the client, we need to fetch
if (!this.state.data) {
this.constructor.fetchData(this.props.match).then(data => {
this.setState({ data })
})
}
}
I have found an alternative solution. In my Redux store I keep the URL of the current page. Therefore on navigation, I am able to do the following:
componentDidMount() {
const { url, match } = this.props;
if (url !== match.url) {
fetchData(match.path);
}
}
Just use a boolean variable in the store, I just use one called "done", when the server fetch the data it set the variable to true, in the component in compoponentDidMount just check if the variable is true, if is, then dont fetch the data, like this:
componentDidMount() {
if(!this.props.done)
this.props.fetchData();
}

Is there a way to store part of application state in URL query with React+Redux?

I'm using React, Redux and React Router. I want to store most of application state in redux store, and some bits of it in URL query. I believe there's two ways of doing that:
syncing redux store with URL query after each state change
don't store "some bits" in redux store, instead have some way (similar to how reducers work) to change URL query as if it was another store
I'm not sure if first way is even possible, since serialized state length may exceed URL limit. So I probably should go with the second.
For example, I have several checkboxes on a page and I want their state (checked/unchecked) to be mirrored in URL, so I could send my URL link to somebody else and they would have the same checkboxes checked as I do. So I render my checkboxes like that:
export class MyComponent extends React.Component {
handleCheckboxClick(...) {
// Some magic function, that puts my urlState into URL query.
updateUrlState(...)
}
render() {
// 'location' prop is injected by react router.
const urlState = getUrlState(this.props.location.query);
// Let's say 'checkboxes' are stored like so:
// {
// foo: false,
// bar: true,
// }
const { checkboxes } = urlState;
return (
<div>
{ checkboxes.map(
(checkboxName) => <Checkbox checked={ checkboxes[checkboxName] } onClick={ this.handleCheckboxClick }>{ checkboxName }</Checkbox>
)}
</div>
)
}
}
What I want magic updateUrlState function to do is get current URL query, update it (mark some checkbox as checked) and push results back to URL query.
Since this improvised URL state could be nested and complex, it probably should be serialised and stored as JSON, so resulting URL would look somewhat like that: https://example.com/#/page?checkboxes="{"foo":false,"bar":true}".
Is that possible?
Refer to this
Modify the URL without reloading the page
You can simply dispatch an action when the checkbox is checked/unchecked that will invoke your magic url function to change the url using window.history.pushState()
function changeUrl() {
isChecked = state.get('checkBox');
window.history.pushState(state, '', 'isChecked=' + isChecked)
}
The first argument above is for when user clicks the back button, the browser will emit popstate event, and you can use the state to set your ui. See https://developer.mozilla.org/en-US/docs/Web/API/History_API
Then when the other user paste the url, you would want to just parse the url and set the correct state in the store in componentDidMount.
function decodeUrl() {
let params = window.location.href.replace(baseUrl, '');
// parse your params and set state
}
FYI: URL does not support certain characters (e.g. '{'). So you will want to parse them into query params, or encode them. Try this tool http://www.urlencoder.org/
If I am getting your issue right, than redux middleware can really help you here. Middleware is a bit like a reduces, but sits in between the flow from the action to the reducer. Best example for such a use case is logging and I have developed middleware, which handles all remote requests. Basically you get the dispatched action in the middleware, there you can analyze it. In case of an action which should change the query parameter of the applycation, you can dispatch new actions from there.
So your code could look like this:
const queryChanges = store => next => action => {
if (/*here you test the action*/) {
//here can change the url, or
return dispatch(actionToTriggerURLChange());
} else {
return next(action);
}
}
In case you neither call return next(action), or dispatch a new one, just nothing will happen, so keep that in mind.
Have a look at the docs and figure out all the workings of middleware works and I guess this approach will be the most »non-repetitive« in this case.

Categories

Resources