In component 1, there is some data being loaded using async method and stored in Vuex store. Same store is being used by other component 2 and data must be cleared when the user navigates from component 1 to component 2.
Below is the normal workflow which is working fine.
Component 1 - load data completed (async, await)
User navigates to Component 2
Component 1 data is cleared in deactivated event
Component 2 is displayed fine
Now, when the user opens the component 1 and quickly navigates to the component 2.
Component 1 - data request initiated but data is not loaded yet
User navigates to Component 2
Component 1 data is cleared in deactivated event
Component 1 async operation completed and data is loaded into the state now
Component 2 will display data meant for Component 1
I still think that you should restructure your components so it loads the data in a view component instead of in the store. The store is meant for data that is shared between different views, or at least relevant to a wider portion of the application, while your data seems specific to one view only. Just pass down the data via props instead.
Views
- View A
- View B
Components
Common
- Sidebar (the common sidebar that is loaded in both View A and View B)
- Some other components specific to view a and b
If you intend to continue with using the store for local data, I think you have multiple options:
You could key your loader with the url or the view name. Where in your state you normally just would have the data, you now have an object that maps the route or the view name to some data. You then use a getter that automatically gets the correct data for you. The added benefit for this is that you can leave your data in if you prefer, which speeds up loading when you return to that view (you don't have to do an api call anymore).
You commit some token to the store and only override the data in your store state when the token matches the retrieve token. (e.g. dispatch('get/my/data', { token: 'asdf' }) while previously doing commit('switch/to/view', 'asdf'). If the view does not match the view we are currently on, we simply discard the data.
In both cases you would dispatch your loading action with something like dispatch('get/my/data', { view: this.$options.name }) (or: this.$route.path or this.$route.name)
If you go with the route of using the component name, you will have to do a commit as well as outlined above. Otherwise you can simply import your router with import router from '../router' or something similar.
For the first option you get something like this:
import router from '../router';
import Vue from 'vue';
{
state: {
data: {},
},
getters: {
getData (state) {
return state.data[router.currentRoute.path];
}
},
mutations: {
setData (state, { view, data }) {
Vue.$set(state.data, view, data);
}
},
actions: {
async fetchData ({ commit }, { view }) {
const data = await myApiPromise;
commit('setData', { view, data });
}
}
}
Now getData either returns data if you have loaded data for that view, or undefined if you haven't. Switching will simply grab the previously stored data, which may or may be useful to you.
The second option is similar, but you have an extra mutation to worry about. That mutation is called from created in each view. Don't worry about cleaning up after yourself when destroying the component, but rather cleanup just before doing the fetch.
{
state: {
data: null,
view: '',
},
getters: {
getData (state) {
return state.data[router.currentRoute.path];
}
},
mutations: {
clearData (state) {
Vue.$set(state, 'data', null);
},
setData (state, { view, data }) {
if (state.view !== view) {
// Do not update
return;
}
Vue.$set(state, 'data', data);
},
setView (state, { view }) {
Vue.$set(state, 'view', view);
}
},
actions: {
async fetchData ({ commit }, { view }) {
commit('clearData');
const data = await myApiPromise;
commit('setData', { view, data });
}
}
}
Use destroyed callback in Vue lifecycle to clear the store when user Navigates from component 1 to component 2
Aource: https://v2.vuejs.org/v2/api/#destroyed
You can still add a state to check user current navigated page and set a flag in store mutation to update state only user is in expected component else just ignore
Related
So I have a plain js file which contains some stuff like functions, one of these is a function that gets a value from 1. localstorage if not present 2. vuex if not present use a default value. These values can be updated thru the whole app in several components which means that components that also are using this value neer te update this value. For now I cant seem to make this part reactive.
helper.js
export const helperFunc = () => {
let value
//dummy
if(checkforlocalstorage){
value = localstorage_value
} else {
value = other_value
}
return value
}
ComponentOne.vue
<template>
<div>{{dynamicValue}}</div>
</template>
<script>
import {helperFunc} from './plugins/helpers.js'
export default {
data () {
return {
}
},
computed: {
dynamicValue : function () {
return helperFunc()
}
},
}
<script>
ComponentTwo.vue
ComponentThree.vue
Update the values here
local or session storage is not a reactive data and vue can't watch them. so you better set the variable in a vuex store, this way the data is reactive and all the components can access it from everywhere in the app.
but there's a catch that you might run into and that is if you refresh the page, all the data in the store get lost and you get the initial values.
but there is one thing you can do, and that is:
save the variable in the localStorage to have it even after the page refresh
initialize the store state value to have the data reactive and accessible through out the app
update the state and localStorage every-time data changes so you can have the reactivity and also latest value in case of a page reload.
Here I show you the basic of this idea:
first you need to setup the store file with the proper state, mutation and action:
export default {
state() {
return {
myVar: 'test',
}
},
mutations: {
UPDATE_MY_VAR(state, value) {
state.myVar = value;
}
},
actions: {
updateMyVar({ commit }, value) {
localStorage.setItem('myVar', value);
commit('UPDATE_MY_VAR', value);
},
initializeMyVar({ commit }) {
const value = localStorage.getItem('myVar');
commit('UPDATE_MY_VAR', value);
}
}
}
then in the beforeCreate or created hook of the root component of your app you'll have:
created() {
this.$store.dispatch('initializeMyVar');
}
this action read the data from a localStorage and initialize the myVar state and you can access that form everywhere like $store.state.myVar and this is reactive and can be watched. also if there is no localStorage and you need a fallback you can write the proper logic for this.
then whenever the data needs to be changed you can use the second action like $store.dispatch('updateMyVar', newUpdatedValue) which updates both the localStorage and the state.
now even with a page reload you get the latest value from the localStorage and the process repeats.
I have a Product.vue component that displays product information. It updates whenever the ProductID in the route changes with data that is stored in vuex. It is done like this:
setup() {
...
// function to trigger loading product into vuex store state
async function loadProduct() {
try {
await $store.dispatch('loadProduct', {ProductID: $route.params.ProductID})
} catch (error) {
console.log(error);
}
}
// watch the route for changes to the ProductID param and run loadProduct() function above
watch(() => $route.params.ProductID, async () => {
if ($route.params.ProductID) {
loadProduct();
}
},
{
deep: true,
immediate: true
}
)
// Get the product data using a getter
const Product = computed(() => $store.getters.getProduct);
}
When I use the above code and go to a route like localhost:8080/product/123, the value of const Product is empty then after a split second it will have the correct product data. If I then go to another route like localhost:8080/product/545, the value of const Product will be the old product data of 123 before updating to 545. This is probably expected behaviour, but it messes up SSR applications which will return to the browser the old data as HTML.
I then came across vuex subscribe function which solves the problem. But I don't understand why or how it is different to a computed getter. This is the only change required to the code:
setup() {
...
const Product = ref();
$store.subscribe((mutation, state) => {
Product.value = state.productStore.product
})
}
Now the store is always populated with the new product data before the page is rendered and SSR also gets the correct updated data. Why is this working better/differently to a computed property?
computed() is Vue internal, and is updated when any ref being called inside of it is updated.
subscribe() is Vuex specific and will be called whenever any mutation was called.
apologies for the simple question, I'm really new to Vue/Nuxt/Vuex.
I am currently having a vuex store, I wish to be able to populate the list with an API call at the beginning (so that I would be able to access it on all pages of my app directly from the store vs instantiating it within a component).
store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, testArray) {
state.list = testArray
}
}
export const getters = {
getArray: state => {
return state.list
},
}
I essentially want to pre-populate state.list so that my components can call the data directly from vuex store. This would look something like that
db.collection("test").doc("test").get().then(doc=> {
let data = doc.data();
let array = data.array; // get array from API call
setListAsArray(); // put the array result into the list
});
I am looking for where to put this code (I assume inside store.js) and how to go about chaining this with the export. Thanks a lot in advance and sorry if it's a simple question.
(Edit) Context:
So why I am looking for this solution was because I used to commit the data (from the API call) to the store inside one of my Vue components - index.vue from my main page. This means that my data was initialized on this component, and if i go straight to another route, my data will not be available there.
This means: http://localhost:3000/ will have the data, if I routed to http://localhost:3000/test it will also have the data, BUT if i directly went straight to http://localhost:3000/test from a new window it will NOT have the data.
EDIT2:
Tried the suggestion with nuxtServerInit
Updated store.js
export const state = () => ({
list: [],
})
export const mutations = {
set(state, dealArray) {
state.list = dealArray
}
}
export const getters = {
allDeals: state => {
return state.list
},
}
export const actions = {
async nuxtServerInit({ commit }, { req }) {
// fetch your backend
const db = require("~/plugins/firebase.js").db;
let doc = await db.collection("test").doc("test").get();
let data = doc.data();
console.log("deals_array: ", data.deals_array); // nothing logged
commit('set', data.deals_array); // doesn't work
commit('deals/set', data.deals_array); // doesn't work
}
}
Tried actions with nuxtServerInit, but when logging store in another component it is an empty array. I tried to log the store in another component (while trying to access it), I got the following:
store.state: {
deals: {
list: []
}
}
I would suggest to either:
calling the fetch method in the default.vue layout or any page
use the nuxtServerInit action inside the store directly
fetch method
You can use the fetch method either in the default.vue layout where it is called every time for each page that is using the layout. Or define the fetch method on separate pages if you want to load specific data for individual pages.
<script>
export default {
data () {
return {}
},
async fetch ({store}) {
// fetch your backend
var list = await $axios.get("http://localhost:8000/list");
store.commit("set", list);
},
}
</script>
You can read more regarding the fetch method in the nuxtjs docs here
use the nuxtServerInit action inside the store directly
In your store.js add a new action:
import axios from 'axios';
actions: {
nuxtServerInit ({ commit }, { req }) {
// fetch your backend
var list = await axios.get("http://localhost:8000/list");
commit('set', list);
}
}
}
You can read more regarding the fetch method in the nuxtjs docs here
Hope this helps :)
My app uses
axios to fetch user information from a backend server
vuex to store users
vue-router to navigate on each user's page
In App.vue, the fetch is dispatched
export default {
name: 'app',
components: {
nav00
},
beforeCreate() {
this.$store.dispatch('fetchUsers')
}
}
In store.js, the users is an object with pk (primary key) to user information.
export default new Vuex.Store({
state: {
users: {},
},
getters: {
userCount: state => {
return Object.keys(state.users).length
}
},
mutations: {
SET_USERS(state, users) {
// users should be backend response
console.log(users.length)
users.forEach(u => state.users[u.pk] = u)
actions: {
fetchUsers({commit}) {
Backend.getUsers()
.then(response => {
commit('SET_USERS', response.data)
})
.catch(error => {
console.log("Cannot fetch users: ", error.response)
})
})
Here Backend.getUsers() is an axios call.
In another component which map to /about in the vue-router, it simply displays userCount via the getter.
Now the behavior of the app depends on timing. If I visit / first and wait 2-3 seconds, then go to /about, the userCount is displayed correctly. However, if I visit /about directly, or visit / first and quickly navigate to /about, the userCount is 0. But in the console, it still shows the correct user count (from the log in SET_USERS).
What did I miss here? Shouldn't the store getter see the update in users and render the HTML display again?
Since it's an object Vue can't detect the changes of the properties and it's even less reactive when it comes to computed properties.
Copied from https://vuex.vuejs.org/guide/mutations.html:
When adding new properties to an Object, you should either:
Use Vue.set(obj, 'newProp', 123), or
Replace that Object with a fresh one. For example, using the object spread syntax we can write it like this:
state.obj = { ...state.obj, newProp: 123 }
I have a navigation flow consisting in
SearchPage -> ...Others or SearchPage -> ...Others or SearchPage ->
and wanna persist what was the search string when navigate back.
<template id="ListCustomersPage">
<q-layout>
<q-layout-header>
<toolbar :title="title" :action="doCreate" label="New"></toolbar>
<q-search inverted placeholder="Type Name, Code, Nit, Phone Number or Email" float-label="Search" v-model="search" />
<q-btn icon="search" color="secondary" #click="doSearch" />
</q-layout-header>
</q-layout>
</template>
Now, the problem is how correlate the stack of the queries and the one of the routers, when the fact the user can navigate elsewhere.
P.D All is in a single page. If possible to persist the screen without refresh them (but only for the search pages until popped back) will be better.
Option 1: Navigation Guards
You can use a so called Navigation Guard that allows you to add global actions before, after and on route updates. You can also add it directly to your component by using the In-component Guards, which will allow you to persist the content of the search data.
const VueFoo = {
// I guess your search attribute is in your data
data() {
return {
search: ''
}
},
mounted() {
// retrieve your information from your persistance layer
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
// persist this.search in localstorage or wherever you like it to be stored
}
}
Option 2: Using a (Vuex) Store
If you're able to add a Vuex Store or any Store alike, I would highly recommend to do so. Since you tagged quasar I want to link to the Vuex Store Documentation provided there. You can basically outsource your search property and let the Store persist it for you across your application.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
search_term: ''
},
mutations: {
SET_SEARCH_TERM (state, payload) {
state.search_term = payload.search_term
}
},
actions: {
SEARCH ({ commit, state }, payload) {
commit('SET_SEARCH_TERM', payload.search_term)
// your api call to search which can also be stored in the state
}
}
})
export default store
In your component where you want to persist your search query using a mutation (not bound to an action):
store.commit('SET_SEARCH_TERM', {
search_term: this.search // your local search query
})
In your code where you trigger the search ACTION if you want to persist during every search
store.dispatch('SEARCH', {
search_term: this.search
})
Accessing the property search_term or however you want to call it can be done using a computed property. You can also bind the state and mutations directly without the need for Navigation guards at all:
// your Vue component
computed: {
search: {
get () {
return this.$store.state.search_term
},
set (val) {
this.$store.commit('SET_SEARCH_TERM', { search_term: val })
}
}
}
Make sure to learn about the basic concept before using: https://vuex.vuejs.org/