I use #nuxtjs/composition-api(0.15.1), but I faced some problems about accessing Vuex getters in computed().
This is my code in composition API:
import { computed, useContext, useFetch, reactive } from '#nuxtjs/composition-api';
setup() {
const { store } = useContext();
const products = computed(() => {
return store.getters['products/pageProducts'];
});
const pagination = computed(() => {
return store.getters['products/pagination'];
});
useFetch(() => {
if (!process.server) {
store.dispatch('products/getPage');
}
});
return {
products,
pagination,
};
}
And the console keeps reporting the warning:
[Vue warn]: Write operation failed: computed value is readonly.
found in
---> <Pages/products/Cat.vue> at pages/products/_cat.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
I'm really confused. Because I didn't try to mutate the computed property, just fetching the Data with the AJAX and then simply assign the data to the state in the Vuex mutations.
But I rewrite the code in option API in this way:
export default {
components: {
ProductCard,
Pagination,
},
async fetch() {
if (process.server) {
await this.$store.dispatch('products/getPage');
}
},
computed: {
products() {
return this.$store.getters['products/pageProducts'];
},
pagination() {
return this.$store.getters['products/pagination'];
},
},
};
Everything works fine, there's no any errors or warnings. Is it the way I'm wrongly accessing the getters in the composition API or that's just a bug with the #nuxtjs/composition-api plugin?
fix: computed property hydration doesn't work with useFetch #207
This problem might not can be solved until the Nuxt3 come out.
But I found an alternative solution which use the middleware() instead of use useFetch(), if you want to the prevent this bug by fetching AJAX data with Vuex Actions and then retrieve it by Getters via the computed().
I make another clearer example which it's the same context like the question above.
~/pages/index.vue :
<script>
import { computed, onMounted, useContext, useFetch } from '#nuxtjs/composition-api';
export default {
async middleware({ store }) {
await store.dispatch('getUser');
},
setup() {
const { store } = useContext();
const user = computed(() => store.getters.user);
return {
user,
};
},
}
</script>
~/store/index.js (Vuex)
const state = () => ({
user: {},
});
const actions = {
async getUser({ commit }) {
const { data } = await this.$axios.get('https://randomuser.me/api/');
console.log(data.results[0]);
commit('SET_USER', data.results[0]);
},
};
const mutations = {
SET_USER(state, user) {
state.user = user;
},
};
const getters = {
user(state) {
return state.user;
},
};
If there's something wrong in my answer, please feel free to give your comments.
Related
I have Vuex Store that will look like this
const config = {
featureA: { isEnabled: true, maxUser: 2 },
featureB: { isEnabled: false, maxData: 5 },
}
const actions = {
getDataCompany(context, payload) {
return new Promise(async (resolve, reject) => {
try {
const result = await firebase.firestore().collection(payload.collection).doc(payload.companyId).get()
if (result) {
if (payload.isLogin) await context.commit('setConfig', result.data())
return resolve(result.data())
}
reject(new Error('Fail To Load'))
} catch (e) {
reject(new Error('Connection Error'))
}
})
}
}
const mutations = {
setConfig(state, payload) {
state.config = payload
}
}
const getters = {
getData: ({ config }) => (feature, key) => {
const state = config
if (state) if (state[feature]) if (state[feature][key]) return state[feature][key]
return null
}
}
export default new Vuex.Store({
state: { config },
actions: { ...actions },
mutations: { ...mutations },
getters: { ...getters }
})
It's working fine with this method to get the data
computed: {
featureAEnabled() {
return this.$store.getters.getData('featureA', 'isEnabled')
},
}
But I have a problem when the data is change, the value is not update in component, and now I want to use mapGetters because it say can detect changes, But I have problem with the documentation and cannot find how to pass params here,
import { mapGetters } from 'vuex'
computed: {
...mapGetters({
featureAEnabled: 'getData'
})
}
I'am calling the action from here
async beforeMount() {
await this.$store.dispatch('getDataCompany', {collection: 'faturelsit', companyId: 'asep', isLogin: true})
}
And try to detect change in here
mounted() {
if (this.featureAEnabled) console.log('feature enabled')
}
The value change is not detected, and need to refresh twice before the changes is implemented in component
My main target is to detect if there any data change in Vuex and make action in component,
nevermind just working with watch without mapgetter,
I just realize that computed cannot re-run the mounted, so I make method that will called when the variable change in watch. thank you.
The main purpose is fulfilled, but the mapgetter with params is still not answered. so if anyone want to answer please share the way to use mapgetter with params.
You could try to use get and set methods for your computed property.
Example:
computed: {
featureAEnabled: {
get() {
return this.$store.getters.getData('featureA', 'isEnabled')
},
set(value) {
...update featureEnabled property in vuex store
}
},
}
My goal is to pass a getter object inside asyncData, because I need to access the state to pass data to axios
Code example
export default {
async asyncData() {
let result = await $axios.$post('/api/test', { data: this.totalPrice })
},
computed: {
...mapGetters(["totalPrice"])
}
}
As you can see I want to access getter object in asyncData However I got
As indicated in the documentation...
Warning: You don't have access to the component instance through this inside asyncData because it is called before initiating the component.
Instead, use the context object provided
async asyncData ({ store }) {
const body = { data: store.getters.totalPrice }
const { data } = await $axios.$post('/api/test', body)
return data
}
Methods should be placed into methods to have the vue context:
export default {
methods : {
async asyncData() {
let result = await $axios.$post('/api/test', { data: this.totalPrice })
}
},
computed: {
...mapGetters(["totalPrice"])
}
}
If you want to do it onload use mounted (https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram)
export default {
async mounted() {
let result = await $axios.$post('/api/test', { data: this.totalPrice })
},
computed: {
...mapGetters(["totalPrice"])
}
}
Okay, I have two different components and each of those get Axios response. But I don't want to fetch data in each component separate. That's is not right, and it cause components run separate...
Updated 3
I did some changes on the code, but still having some problems. I am doing axios call with Vuex in Store.js and import it into my component. it's like below.
This is my store.js component;
import Vue from "vue";
import Vuex from "vuex";
var actions = _buildActions();
var modules = {};
var mutations = _buildMutations();
const state = {
storedData: []
};
Vue.use(Vuex);
const getters = {
storedData: function(state) {
return state.storedData;
}
};
function _buildActions() {
return {
fetchData({ commit }) {
axios
.get("/ajax")
.then(response => {
commit("SET_DATA", response.data);
})
.catch(error => {
commit("SET_ERROR", error);
});
}
};
}
function _buildMutations() {
return {
SET_DATA(state, payload) {
console.log("payload", payload);
const postData = payload.filter(post => post.userId == 1);
state.storedData = postData;
}
};
}
export default new Vuex.Store({
actions: actions,
modules: modules,
mutations: mutations,
state: state,
getters
});
Now importing it into Average component.
import store from './Store.js';
export default {
name:'average',
data(){
return{
avg:"",
storedData: [],
}
},
mounted () {
console.log(this.$store)
this.$store.dispatch('fetchDatas')
this.storedData = this.$store.dispatch('fetchData')
},
methods: {
avgArray: function (region) {
const sum = arr => arr.reduce((a,c) => (a += c),0);
const avg = arr => sum(arr) / arr.length;
return avg(region);
},
},
computed: {
mapGetters(["storedData"])
groupedPricesByRegion () {
return this.storedData.reduce((acc, obj) => {
var key = obj.region;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj.m2_price);
return acc;
}, {});
},
averagesByRegion () {
let arr = [];
Object.entries(this.groupedPricesByRegion)
.forEach(([key, value]) => {
arr.push({ [key]: Math.round(this.avgArray(value)) });
});
return arr;
},
}
}
I can see the data stored in the console. But there are errors too. I can't properly pass the data in myComponent
https://i.stack.imgur.com/J6mlV.png
if you don't want use vuex to distrib data maybe you can try eventBus, when you get data form the axios respose #emit the event and in another component #on this event
The issue is that
To resolve the error you're getting, Below are the steps.
Import your store file inside the file where your Vue Instance is initialized.
// Assuming your store file is at the same level
import store from './store';
Inside your, Add store object inside your Vue Instance
function initApp(appNode) {
new Vue({
el: appNode,
router: Router,
store // ES6 sytax
});
}
There you go, you can now access your store from any component.
UPDATE: For Second Error
Instead of changing data inside your component, change it inside mutation in the store because you do not want to write the same login in other components where the same method is used.
Hence,
computed: {
...mapGetters(["storedData"]),
anotherFunction() {
// Function implementation.
}
}
Inside your mutation set the data.
SET_DATA(state, payload) {
console.log("payload", payload);
state.storedData = payload;
}
Inside getters, you can perform what you were performing inside your computed properties.
storedData: function(state) {
const postData = state.storedData.filter(post => post.userId == 1);
return postData;
}
Vuex Official docs
Here is the working codesandbox
Hope this helps!
Can anyone see why this wouldn't work please,
Trying to use vuex store to manage my axios requests and transfer to a component as follows:
In my vuex store module I have the following
import axios from "axios";
export const state = () => ({
cases: [],
})
export const mutations = {
listCases (state, cases) {
state.cases = cases;
},
}
export const actions = {
loadCases ({ commit, context }) {
return axios.get('http')
.then(res => {
const convertCases = []
for (const key in res.data) {
convertCases.push({ ...res.data[key], id: key })
}
commit('listCases', convertCases)
})
.catch(e => context.error(e));
},
export const getters = {
// return the state
cases(state) {
return state.cases
}
}
I checked amd my axios request is returning my results as expected and passing to the mutation
In my component I have
import { mapMutations, mapGetters, mapActions } from 'vuex'
export default {
created () {
this.$store.dispatch('cases/loadCases');
},
computed: {
...mapGetters ({
cases: 'cases/cases'
})
},
</script>
Now i assumed based on what I've learnt that i could call with
and this would return my items.
but i get an error cases is not defined,
Anyone abe to tell me my error please
Many Thanks
Take a look here: https://v2.vuejs.org/v2/guide/list.html#Array-Change-Detection
You may be able to make it reactive this way:
export const mutations = {
listCases (state, cases) {
state.cases = [];
cases.forEach((c) => {
state.cases.push(c);
});
},
}
I'm just starting to learn Vuex here. Until now I've been storing shared data in a store.js file and importing store in every module but this is getting annoying and I'm worried about mutating state.
What I'm struggling with is how to import data from firebase using Vuex. From what I understand only actions can make async calls but only mutations can update the state?
Right now I'm making calls to firebase from my mutations object and it seems to be working fine. Honestly, all the context, commit, dispatch, etc. seems a bit overload. I'd like to just be able to use the minimal amount of Vuex necessary to be productive.
In the docs it looks like I can write some code that updates the state in the mutations object like below, import it into my component in the computed property and then just trigger a state update using store.commit('increment'). This seems like the minimum amount necessary to use Vuex but then where do actions come in? Confused :( Any help on the best way to do this or best practices would be appreciated!
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
My code is below
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
const db = firebase.database();
const auth = firebase.auth();
const store = new Vuex.Store({
state: {
userInfo: {},
users: {},
resources: [],
postKey: ''
},
mutations: {
// Get data from a firebase path & put in state object
getResources: function (state) {
var resourcesRef = db.ref('resources');
resourcesRef.on('value', snapshot => {
state.resources.push(snapshot.val());
})
},
getUsers: function (state) {
var usersRef = db.ref('users');
usersRef.on('value', snapshot => {
state.users = snapshot.val();
})
},
toggleSignIn: function (state) {
if (!auth.currentUser) {
console.log("Signing in...");
var provider = new firebase.auth.GoogleAuthProvider();
auth.signInWithPopup(provider).then( result => {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// Set a user
var uid = user.uid;
db.ref('users/' + user.uid).set({
name: user.displayName,
email: user.email,
profilePicture : user.photoURL,
});
state.userInfo = user;
// ...
}).catch( error => {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
} else {
console.log('Signing out...');
auth.signOut();
}
}
}
})
export default store
main.js
import Vue from 'vue'
import App from './App'
import store from './store'
new Vue({
el: '#app',
store, // Inject store into all child components
template: '<App/>',
components: { App }
})
App.vue
<template>
<div id="app">
<button v-on:click="toggleSignIn">Click me</button>
</div>
</template>
<script>
import Hello from './components/Hello'
export default {
name: 'app',
components: {
Hello
},
created: function () {
this.$store.commit('getResources'); // Trigger state change
this.$store.commit('getUsers'); // Trigger state change
},
computed: {
state () {
return this.$store.state // Get Vuex state into my component
}
},
methods: {
toggleSignIn () {
this.$store.commit('toggleSignIn'); // Trigger state change
}
}
}
</script>
<style>
</style>
All AJAX should be going into actions instead of mutations. So the process would start by calling your action
...which commits data from the ajax callback to a mutation
...which is responsible for updating the vuex state.
Reference: http://vuex.vuejs.org/en/actions.html
Here is an example:
// vuex store
state: {
savedData: null
},
mutations: {
updateSavedData (state, data) {
state.savedData = data
}
},
actions: {
fetchData ({ commit }) {
this.$http({
url: 'some-endpoint',
method: 'GET'
}).then(function (response) {
commit('updateSavedData', response.data)
}, function () {
console.log('error')
})
}
}
Then to call your ajax, you will have to call the action now by doing this:
store.dispatch('fetchData')
In your case, just replace this.$http({...}).then(...) with your firebase ajax and call your action in the callback.