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

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.

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.

React Redux - quick flash of previous state before dispatch issue

I'm building a React + Redux app with a Node.js backend, and one of the features is that a user can view profiles of other users. To do this, I have a section in the Redux state called users that looks like:
{
...,
users: {
user: {},
isLoading: true
}
}
Every time the /users/:id route is rendered, a getUser(id) action is dispatched and fills the state with the received information.
The main issue is when a user views user1's profile page (therefore redux state is now filled with user1's profile) and then views user2's profile page, the dispatch getUser(2) action is called right after the page is rendered. Since user1's info is still in the state, it will flash their info for a very short time, and then show the loading spinner until the new user is loaded.
I read about dispatching a resetUser(id) action on every unmount of the page, but I'm not sure if this is the right way to go. Also, one of the features is if a user is viewing their own page, they have an edit button which redirects them to /edit-profile. If I reset the state on every unmount, I'll have to fetch their profile again (in the edit page), even though I just did that when they viewed their page.. And that doesn't seem like it makes sense.
Any ideas how to solve this? Thanks!
The render phase runs after mounting. And you stated that previous data is being shown before new data. It seems that you have asynchronous mounting:
async componentDidMount() {}
It will continue rendering even before mounting phase is completed. To avoid issue, you may use synchronous nature of mounting:
componentDidMount(){}
Where you'll call api data.
Now, when you reach to rendering phase it will have new data available before rendering and won't flash you old data.
You now may be wondering how to call api asynchronously. You can create a asynchronous function and call that function inside the synchronous componentDidMount.
componentDidMount() {
yourAsyncFunc()
}
async yourAsyncFunc() {} // api call
How can I do this with React hooks?
While using useEffect, don't implement async:
useEffect(async () =>
Implement it simply:
useEffect(() => {
// you can still use asynchronous function here
async function callApi() {}
callApi()
}, []) // [] to run in similar to componentDidMount
If you miss second parameter to useEffect then you are not ensuring it to run on mounting phase. It will run before, after, and in the rendering phase depending on case.
Implementing something like resetUser(id) seems to be the right way here. I use this approach in my current project and this does solve the problem. The other approach of removing async keyword from useEffect callback as mentioned in another answer didn't work for me (I use hooks, redux-toolkit, Typescript).
After dispatching this action, your state should look something like
{
...,
users: {
user: null,
isLoading: false,
}
}
If you are using hooks, you can dispatch the action this way:
useEffect(() => {
const ac = new AbortController();
...
return () => {
dispatch(resetUser(null));
ac.abort();
};
}, []);
Action could look something like this:
resetListingDetails(state, action) {
// Immer-like mutable update
state.users = {
...state.users,
user: null,
};
}

How can I read from localStorage and mutate state in Vue store

I have property that I set in localStorage, enableLog = false. If I write in the console localStorage.enableLog = true, How can I read that value in my vuex store and flip my state from false to true and trigger a console.log?
What I'm trying to do is add ad-hoc console logging. For example, we have an app that will send data to another app, that is launched in a pop up window. In the pop up app, if another dev changes enableLog from false to true, he/she will be able to see that initial data in the payload sent to the pop up app via a function that console.logs. How can my app read the localStorage property, how then do I update state from that, and lastly where does the re-render to trigger the function to call that console.log go?
index.js, state:
isLoggingData: false
index.js, actions:
async init ({ state, commit, dispatch }) {
try {
const { data } = await messaging.send(ACTION.READY)
commit(types.SET_LOGGING_DATA, data) // capture this initial data to be used later
index.js mutations:
[types.SET_LOGGING_DATA] (state, isLoggingData) {
state.loggedData.initData = isLoggingData
},
App.vue:
async created () {
localStorage.setItem('isLoggingData', this.isLoggingData)
await this.onCreated()
async onCreated () {
await this.init()
I don't know how to listen to the change in localStorage.isLoggingData.
vuex-shared-mutations should fit your needs.
Share vuex mutations across tabs via `localStorage`.

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

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

Categories

Resources