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.
Related
I am trying to make a product detail page. The detail page is named _id.
When opened the id is replaced with the product id. On opening the page the state is set with data fetched from an api.
After that i am trying to use a computed property that refers to a getter named getProduct() with an id (this.$route.params.id) in the payload.
This is how my _id.vue looks like:
methods: {
...mapActions("products", ["fetchProducts",]),
...mapGetters("products", ["getProduct",]),
},
async mounted() {
this.fetchProducts()
},
computed: {
product() {
return this.getProduct(this.$route.params.id)
}
}
This is how my store file named products.js looks like:
import axios from "axios"
export const state = () => ({
producten: []
})
export const mutations = {
setProducts(state, data) {
state.producten = data
}
}
export const getters = {
getProduct(state, id) {
console.log(id)
return state.producten.filter(product => product.id = id)
}
}
export const actions = {
async fetchProducts({ commit }) {
await axios.get('/api/products')
.then(res => {
var data = res.data
commit('setProducts', data)
})
.catch(err => console.log(err));
}
}
What works is creating the state, but when i try to use the getter something goes wrong.
As you can see i console.log() the id given to it. Which logs the following:
I also get the error: client.js?06a0:103 Error: [vuex] do not mutate vuex store state outside mutation handlers.
Which I'm not doing as far as I know?
**Note: **these errors get logged as much as the length of my state array is.
From the Vuex documentation:
Vuex allows us to define "getters" in the store. You can think of them as computed properties for stores. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed.
Like computed, getters does not support having arguments.
But there is a way to have "method-style access" to a getter: https://vuex.vuejs.org/guide/getters.html#property-style-access
You can also pass arguments to getters by returning a function. This is particularly useful when you want to query an array in the store:
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
Note that getters accessed via methods will run each time you call them, and the result is not cached.
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'm trying to figure out how to properly update a getter value when some other variable from VueX changes/updates.
Currently I'm using this way in a component to update:
watch: {
dates () {
this.$set(this.linedata[0].chartOptions.xAxis,"categories",this.dates)
}
}
So my getter linedata should be updated with dates value whenever dates changes. dates is state variable from VueX store.
The thing is with this method the value won't be properly updated when I changed route/go to different components. So I think it's better to do this kind of thing using the VueX store.
dates is updated with an API call, so I use an action to update it.
So the question is how can I do such an update from the VueX store?
EDIT:
I tried moving this to VueX:
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit("SET_DATA", {
this.linedata[0].chartOptions.xAxis,"categories": response.data.dates1,
this.linedata[1].chartOptions.xAxis,"categories": response.data.dates2
});
}
SET_DATA(state, payload) {
state = Object.assign(state, payload);
}
But the above does not work, as I cannot set nested object in action this way...
Getters are generally for getting, not setting. They are like computed for Vuex, which return calculated data. They update automatically when reactive contents change. So it's probably best to rethink the design so that only state needs to be updated. Either way, Vuex should be updated only with actions/mutations
Given your example and the info from all your comments, using linedata as state, your action and mutation would look something like this:
actions: {
async loadData({ commit }) {
let response = await Api().get("/cpu");
commit('SET_DATA', response.data.dates);
}
}
mutations: {
SET_DATA(state, dates) {
Vue.set(state.linedata[0].chartOptions.xAxis, 'categories', dates[0]);
Vue.set(state.linedata[1].chartOptions.xAxis, 'categories', dates[1]);
}
}
Which you could call, in the component for example, like:
this.$store.dispatch('loadData');
Using Vue.set is necessary for change detection in this case and requires the following import:
import Vue from 'vue';
Theoretically, there should be a better way to design your backend API so that you can just set state.linedata = payload in the mutation, but this will work with what you have.
Here is a simple example of a Vuex store for an user.
export const state = () => ({
user: {}
})
export const mutations = {
set(state, user) {
state.user = user
},
unset(state) {
state.user = {}
},
patch(state, user) {
state.user = Object.assign({}, state.user, user)
}
}
export const actions = {
async set({ commit }) {
// TODO: Get user...
commit('set', user)
},
unset({ commit }) {
commit('unset')
},
patch({ commit }, user) {
commit('patch', user)
}
}
export const getters = {
get(state) {
return state.user
}
}
If you want to set the user data, you can call await this.$store.dispatch('user/set') in any Vue instance. For patching the data you could call this.$store.dispatch('user/patch', newUserData).
The getter is then reactively updated in any Vue instance where it is mapped. You should use the function mapGetters from Vuex in the computed properties. Here is an example.
...
computed: {
...mapGetters({
user: 'user/get'
})
}
...
The three dots ... before the function call is destructuring assignment, which will map all the properties that will the function return in an object to computed properties. Those will then be reactively updated whenever you call dispatch on the user store.
Take a look at Vuex documentation for a more in depth explanation.
I want to make an API call to the server to fetch the data and then display them in a component. I have a created() method which dispatches an action to my store, which, in turn, commits the mutation to udpate my store with the data I got from the server. I also have computed method where I simply call the getter which fetches the data from the store. The code looks like this:
state
state: {
data: {
rides: []
}
}
component.vue
created() {
this.$store.dispatch('fetchUserRides');
}
computed: {
...mapGetters([
'userRides'
]),
}
store.js
//actions
fetchUserRides({ commit }) {
axios.get('/api/stats/current_week')
.then(response => {
commit('fetchUserRides', response)
})
.catch(error => {
commit('serverResponsError')
})
//mutations...
fetchUserRides(state, payload){
let rides = payload.data
rides.forEach((item) => {
state.data.rides.push(item)
})
//getters
userRides: state => {
let rides = state.data.rides
rides.sort(( a, b) => {
return new Date(a.date) - new Date(b.date);
});
return rides
}
I receive over 40 objects in the response, I did check it by console.log(state.data.rides) and they are there in 100%.
My problem is that when I log off and log back in again it throws an error "TypeError: Cannot read property 'sort' of null". But if I hit Refresh they appear fine. The login action redirects me to the page where I render this component. This looks like the computed property first tries to fetch data by the getter from the array before it is actually populated in the store. How can I make sure I get the array of objects in my component?
You probably need to set an empty array ([]) as an initial value to state.data.rides instead of null.
Another option will be to check that rides is truthy in your getters.
Something like:
if (rides) {
rides.sort(( a, b) => {
return new Date(a.date) - new Date(b.date);
});
}
return []
I was able to resolve my problem and it turns out I made a mistake. I completely forgot I set state.data.rides = null instead of an empty array state.data.rides = null, which would explain why the array was empty. It was a legacy code I had :)
I am not sure when computed property (in vue lifecycle) comes. Let's say I have a method that I run in created() as:
created () {
getSomething()
}
Inside getSomething() I fetch data and fill my data property as:
getSomething(){
axios.get(...) { this.info = response.data }
}
Now, in computed properties, I do:
computed: {
doSomething () {
this.info.forEach(item => {})
}
}
But, inside my computed I get forEach is undefined, as if this.info is not an array or has not be filled.
What am I doing wrong? are computed props called before created() or something?
try something like this
getSomething(){
return axios.get(...) { this.info = response.data }
}
then you can use the promise returned in created method like this....
created () {
getSomething().then( () => {
doSomething()
}}
}
You could utilise Vuex' state management...
Vuex has :
Dispatches
Actions
Mutations
Getters
What i am thinking is, on your page load, map an action to the store that makes a call to your axios service and after it returns, commit a mutation that stores the response to the store...
then you can map the state to your component and retrieve the data.
This might not be the quickest answer but if you are expecting to need the response data from the axios request, it might be worth putting it into some sort of state management :-)