Making Async Calls With Vuex - vuex

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.

Related

How to properly use Vuex getters in Nuxt Vue Composition API?

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.

from same axios to different components, VueJS

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!

currentUser not getting set with Vuex

I added some code to my vue project so I can save the state of a user - which is whether he is logged in or not. If the state is not null, I want to display the navbar and footer. I added all the vuex import statements. I am using an axios call to the db which returns a json response. response.data comes back as true/false. If true, I redirect the user to the main page. Then I create a user object called currentUser, but I'm not sure what to base it on, so it is getting set to null. I need to use the state in a few places throughout my app, but it is not getting set. Please someone help. Thanks in advance. (code is below)
User.js:
import JwtDecode from 'jwt-decode'
export default class User {
static from (token) {
try {
let obj = JwtDecode(token)
return new User(obj)
} catch (_) {
return null
}
}
constructor ({username}) {
this.username = username
}
}
App.vue:
<template>
<div id="app">
<template v-if="currentUser">
<Navbar></Navbar>
</template>
<div class="container-fluid">
<router-view></router-view>
<template v-if="currentUser">
<Foot></Foot>
</template>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Navbar from '#/components/Navbar'
import Foot from '#/components/Foot'
export default {
name: 'App',
components: {
Navbar,
Foot
},
computed: {
...mapGetters({ currentUser: 'currentUser' })
},
mutation_types.js:
export const LOGIN = 'LOGIN'
export const LOGOUT = 'LOGOUT'
auth.js:
/* global localStorage */
import User from '#/models/User'
import * as MutationTypes from './mutation_types'
const state = {
user: User.from(localStorage.token)
}
const mutations = {
[MutationTypes.LOGIN] (state) {
state.user = User.from(localStorage.token)
},
[MutationTypes.LOGOUT] (state) {
state.user = null
}
}
const getters = {
currentUser (state) {
return state.user
}
}
const actions = {
login ({ commit }) {
commit(MutationTypes.LOGIN)
},
logout ({ commit }) {
commit(MutationTypes.LOGOUT)
}
}
export default {
state,
mutations,
getters,
actions
}
The user store should only set the default state. AFter making request to validate user. You should use actions and mutations to set the user state. Call the action via store.dispatch("user/login", user) where you return new User(obj).
let obj = JwtDecode(token)
const user = new User(obj)
store.dispatch("user/login", user)
const actions = {
login ({ commit }, userObject) {
commit(MutationTypes.LOGIN, userObject)
},
logout ({ commit }) {
commit(MutationTypes.LOGOUT)
}
}
const mutations = {
[MutationTypes.LOGIN] (state, user) {
state.user = user;
},
[MutationTypes.LOGOUT] (state) {
state.user = null
}
}
On a other note, you have dumb getters. Meaning they just return the state. You can rather call the user object directly out of state. Use getters when you want to modify the return value before returning it.
I took a little look and it seems you use '=' instead of Vue.set() to set your state variable.
Refer to the answer : vuex - is it possible to directly change state, even if not recommended?

vuex store token doesn't update accross components

I have the following store/index.js file:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = {
token: null
}
const mutations = {
setToken (state, token) {
state.token = token
localStorage.setItem('token', token)
},
removeToken (state) {
state.token = null
localStorage.removeItem('token')
}
}
const getters = {
isLoggedIn: state => {
return state.token != null
}
}
export default new Vuex.Store({
state,
getters,
mutations
})
and in my main.js I add it as such:
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
and when I login I set the token as such:
this.$store.commit('setToken', response.data.token)
Which should be working because the setToken function successfully save it to localStorage.
However, if I go to my other components and call:
data () {
return {
loggedIn: false
}
},
mounted: function () {
this.loggedIn = this.$store.getters.isLoggedIn
console.log(this.loggedIn)
}
then console will print 'false'. I have also tried to change 'mounted' to 'computed', 'created' and 'ready' but the result is the same.
I can only seem to solve the problem if I do like this:
mounted: function () {
this.token = localStorage.getItem('token')
this.$store.commit('setToken', this.token)
},
computed: {
isLoggedIn () {
return this.$store.getters.isLoggedIn
}
}
for which isLoggedIn will finally be true.
But if I do like that then what's the point of vuex..
I'm thinking I either do something wrong with not knowing whether to call computed/mounted/etc, or maybe the way I do mutations is wrong, or maybe I don't initialize Vuex in the proper file.. but I feel like I've tried it all now
Your component is probably mounted before you login and set the token.
Seeing as you're using local storage for token persistence, I would initialise the value that way, ie
const state = {
token: localStorage.getItem('token')
}
This will be null if it has not been set (or has been removed).

VueJS - Vuex Assign data in a mutation

I'm getting used to Vuex as I need to be able to have a store that I can easily access and update when another component is changed.
Currently, my store looks like the following:
import vue from 'vue';
import Vuex from 'vuex';
vue.use(Vuex);
export default new Vuex.Store({
state: {
users: {
columns: [],
model: [],
}
},
mutations: {
fetchUsers: function(state) {
axios.get(`/users?search_input=`)
.then(function(response) {
});
}
}
});
The columns and model is dynamically pulled down from an ajax request and my Users looks like the following:
<script>
export default {
mounted() {
this.$store.commit('fetchUsers');
},
computed: {
columns() {
return this.$store.state.users.columns;
}
model() {
return this.$store.state.users.model
}
},
}
</script>
My problem is that the application needs to have data preloaded from Ajax. For example columns are set from an ajax request in fetchUsers and inside the Users I use this.$store.commit('fetchUsers'); but is there an alternative way I can do that without using commit preferably inside the store itself?
In the store u can load ur data asynchronously via actions then commit the changes using a mutation.
import vue from 'vue';
import Vuex from 'vuex';
vue.use(Vuex);
export default new Vuex.Store({
state: {
users: {
columns: [],
model: [],
}
},
actions: {
fetchUsers: function( context ) {
axios.get(`/users?search_input=`)
.then( function( response ) {
context.commit( "FETCHUSERS", {
columns: response.columns,
model: response.model
});
});
}
}
mutations: {
FETCHUSERS: function( state, payload ) {
state.users.columns = payload.columns;
state.users.model = payload.model;
}
}
});
Dispatch the action from the component
<script>
export default {
.....
methods: {
fetchUsers: function() {
this.$store.dispatch( "fetchUsers" );
}
}
}
</script>
Mutations must be synchronous: https://vuex.vuejs.org/en/mutations.html
You should move ajax request into actions, which can be async. You get data in the action, commit mutation giving received data as a payload, and mutation assigns a value to the state property.
Alternatively, you can make async request in component method, and assign a value to the store property directly: this.$store.state.prop = value