How to make data from localStorage reactive in Vue js - javascript

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++;
...

Related

Vue not updating global state

I have some problems with updating the global state. I'm trying to update that state by listening WebSocket, but it's not updating as expected.
Here is how did I define the global state.
state: {
userData: null
},
getters: {
userData: state => {
return state.userData
},
mutations: {
GET_USER({payload}) {
commit('GET_USER', payload)
},
And I'm updating it in App.vue like so:
mounted() {
window.Echo.channel("user." + this.userData.id).listen(".user-updated", (user) => {
this.$store.commit('GET_USER', JSON.stringify(user.user))
});
Ofcourse I'm closing that websocket. I tried with localStorage, which I think is not a bad idea, but still I'm doing it with global state, and with localstorage would look like:
localStorage.setItem('userData', JSON.stringify(user.user))
So when I want to show that data in some component, for example, Home.vue, the Only way that I can see what is happening, is by defining {{ this.$store.getters.userData }} in the template of that file, but If I try to define it in scripts data, like so:
data() {
return {
data: this.$store.getters.userData,
}
},
It's not updating real time, but only if I go to another page and return here, or update the component.
Any ideas on how to fix it?
I had success by accessing the state. As far as I understand it it should be reactive as well this.$store.state.userData and reflect the current state of the store just as well.
Keep in mind accessing object properties might not be reactive the way you think they are: https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
So you probably want to define a getter where you access the user's name, credentials, whatever userData holds.
The more explicit approach is defining every property of the userData object in the store and write a mutation for every property individually (loops are possible to do that at once, of course, but you'll have to create the mutators still).
{
state: {
userData: {
id: null,
name: '',
email: '',
// etc.
}
},
// ...
}
Doing it this way may also work successfully with the getter you defined.
For my work I would rather not trust handling whole objects at once. This is a bit of extra code to write but explicitly defining which properties the expected object should have is valuable to you and others.

How to properly clear Vuex state in an application with vue-router?

I have a vuex store which can increase when I walk through pages of my site. Every page has its own store where specific information for this page is stored. I know how to write a function which will be responsible for clearing my state, but I don't understand where to call this function in my code.
Let's say I have 5 pages, where 3 of them own their specific store which should be deleted when I move out of a page, but the other 2 have a common state which should be deleted only when I move out of these pages, but when I move between these 2 - the store should be kept in the state it's now. Data for stores are fetched via AJAX requests.
How do you handle this problem? I was thinking about listening to $route changes, but something makes me feel it's wrong.
My function which clean ups the store (reset_state):
const getDefaultState = () => {
return {
widgets: null
}
}
export const items = {
state: () => ({
data: null
}),
mutations: {
reset_state (state) {
Object.assign(state, getDefaultState())
}
},
actions: {
resetItems({ commit }) {
commit("reset_state");
},
}
}
You should call your function either inside the beforeDestroy lifecycle hook or inside the beforeRouteLeave hook - depending on whether you wrap your route(s) inside keep-alive.

Check if the key in redux store is connected to any component in DOM right now

I have a redux store with a reducer data and using redux observable to fill this data in store. I am trying to load some data in store when component is mounted and remove that data when component is unmounted. But before removing I want to check that this data is not used by any other mounted component. What I have till now is this
Store:
{
data: {}
}
My component needs itemList, I dispatch an action LOAD_ITEMS, one epic loads itemList and puts it in store
{
data: { items: {someItems}}
}
This component has following connection to store -
componentDidMount () {
if (!data.items) {
dipatch(LOAD_ITEMS)
}
}
componentWillUnmount() {
// Before doing this I want to make sure that these items are not
// used by any other mounted componeted.
dispatch(REMOVE_ITEMS_FROM_STORE);
}
mapStateToProps = () => ({
data: store.data
})
One way I tried was to save count of all mounted components which uses items from store in store with a key activeComponents. Like following
{
data: {
items: {someItems}
activeComponents: 2 // count of mounted components which are
//using items from store
}
}
So if there are two components which needs items from store the count of activeComponents will be 2, so items will be removed from store only if this count is one, on other removal attempts just activeComponents count is reduced by 1
But this is very complicated approach, I suppose there must be some better and proper way do this. Any thoughts?
I think your idea with storing the number of activeComponents is kinda the right approach, but you should use a boolean value, that will make it much more simple to handle.
So basically instead of storing activeComponents: 3, you can just do isUsed: true / false

Binding a UI element with a Vuex store

I'm trying to bind a UI element (a single-line textbox or 'input' element) with a Vuex store. This fiddle has the code.
When the SearchResult component is visible, it auto-updates -- see the GIF below, where Lisp or Prolog is typed. That's not what I'd like to happen. What I'd really like to do is decouple the UI state (i.e. the value of the textbox) from the model's state, so that if I type Lisp and press Search, the SearchResult component updates itself.
Ideally I'd like to bind the textbox with a variable that's not in the store, but also add some code to observe changes to the store, so that any changes to the store are reflected in the UI.
I read the forms handling documentation for Vuex but wasn't very clear about the best way to get this done. Please could anyone help? I'm new to SPAs so I'm sure there's a better way of getting this done.
I think the approach you have used is the general approach if you want to use a store variable in input. Given that you want to decouple the UI variable with the model's state(Why?), you can do following:
Have a local variable in that vue instace
use that local variable with v-model
put a watch on state variable, if state variable changes, change local variable.
set state variable on button press, or some other way like onblur event
Here are relevant JS changes:
const app = new Vue({
router,
el: '#app',
data: {
localQuery: ''
},
computed: {
query: {
get () { return store.state.query },
set (v) { store.commit('setquery', v) }
}
},
methods: {
s1: function () {
console.log('app.s1 this.query: ' + this.query);
this.query = this.localQuery
router.push({ name: 'qpath', params: { query: this.query }});
}
},
watch:{
query: function (newVal) {
this.localQuery = newVal
}
}
})
see updated fiddle here.

How to update Vue component property when Vuex store state changes?

I'm building a simple presentation tool where I can create presentations, name them and add/remove slides with Vue js and Vuex to handle the app state. All is going great but now I'm trying to implement a feature that detects changes in the presentation (title changed or slide added/removed) and couldn't not yet find the right solution for it. I'll give the example only concerning the title change for the sake of simplicity. Right now in my Vuex store I have:
const state = {
presentations: handover.presentations, //array of objects that comes from the DB
currentPresentation: handover.presentations[0]
}
In my Presentation component I have:
export default {
template: '#presentation',
props: ['presentation'],
data: () => {
return {
shadowPresentation: ''
}
},
computed: {
isSelected () {
if (this.getSelectedPresentation !== null) {
return this.presentation === this.getSelectedPresentation
}
return false
},
hasChanged () {
if (this.shadowPresentation.title !== this.presentation.title) {
return true
}
return false
},
...mapGetters(['getSelectedPresentation'])
},
methods: mapActions({
selectPresentation: 'selectPresentation'
}),
created () {
const self = this
self.shadowPresentation = {
title: self.presentation.title,
slides: []
}
self.presentation.slides.forEach(item => {
self.shadowPresentation.slides.push(item)
})
}
}
What I've done so far is to create a shadow copy of my presentation when the component is created and then by the way of a computed property compare the properties that I'm interested in (in this case the title) and return true if anything is different. This works for detecting the changes but what I want to do is to be able to update the shadow presentation when the presentation is saved and so far I've failed to do it. Since the savePresentation action triggered in another component and I don't really know how pick the 'save' event inside presentation component I fail to update my shadow presentation. Any thoughts on how I could implement such feature? Any help would be very appreciated! Thanks in advance!
I ended up solving this problem in a different way than what I asked in the question but it may be of interest for some. So here it goes:
First I abdicated from having my vue store communicating an event to a component since when you use vuex you should have all your app state managed by the vuex store. What I did was to change the presentation object structure from
{
title: 'title',
slides: []
}
to something a little more complex, like this
{
states: [{
hash: md5(JSON.stringify(presentation)),
content: presentation
}],
statesAhead: [],
lastSaved: md5(JSON.stringify(presentation))
}
where presentation is the simple presentation object that I had at first. Now my new presentation object has a prop states where I will put all my presentation states and each of this states has an hash generated by the stringified simple presentation object and the actual simple presentation object. Like this I will for every change in the presention generate a new state with a different hash and then I can compare my current state hash with the last one that was saved. Whenever I save the presentation I update the lastSaved prop to the current state hash. With this structure I could simple implement undo/redo features just by unshifting/shifting states from states to statesAhead and vice-versa and that's even more than what I intended at first and in the end I kept all my state managed by the vuex store instead of fragmenting my state management and polluting components.
I hope it wasn't too much confusing and that someone finds this helpful.
Cheers
I had this issue when trying to add new properties to my user state so I ended up with this and it works well.
Action in Vuex store
updateUser (state, newObj) {
if (!state.user) {
state.user = {}
}
for (var propertyName in newObj) {
if (newObj.hasOwnProperty(propertyName)) {
//updates store state
Vue.set(state.user, propertyName, newObj[propertyName])
}
}
}
Implementation
Call your store action above from the Vue component
this.updateUser({emailVerified: true})
Object
{"user":{"emailVerified":true},"version":"1.0.0"}

Categories

Resources