I am new in this world of Vue and Vuex, but I want to know if you can help me with this problem that I have right know, and actually don't really know why.
Note: I am using an api developed in laravel.
The scenario is this:
I have a common component that I have created which displays a list (data table) of users.
users/component/Users.vue
<template>
....
</template>
<script>
import { mapActions } from 'vuex'
import { mapGetters } from 'vuex'
export default {
data () {
return {
items: [],
user: {}
}
},
methods: {
...mapActions({
getUsers: 'users/getUsers'
})
},
mounted () {
***this.user = {
...mapGetters({
user: 'auth/user'
})
}
this.user = {
...mapGetters({
user: 'auth/user'
})
}
var routeName = this.$route.name // users or admins
this.getUsers(route, user)
this.items = this.$store.state.users
}
}
</script>
Right now my application will have three roles: regular users, administrator and super administrators.
A super administrator will have two modules for display users:
List of all regular users.
List of all administrators.
This two modules would be in different routes, which is why I reused the common component that displays a list of users.
So, I have in Vuex right now two modules, the first one is auth module that in its state file will contain the information of the user that is logged in: name, email, role. And the second module is users module that in its state will contain the list of users passed via api depending on the route I am entering and the user's role that this one has.
The users module has three actions, the first one is getRegularUsers(), getAdmins() and getUsers(routeName, role). The last action is invoked in users component that received the route (url the user is currently in) and the user's role.
The definition of getUsers() is:
users/store/actions.js
...
export const getUsers = (routeName, role) => {
if (routeName == 'admins') {
if (role === 'SuperAdmin')
this.getAdmins()
}
else
this.getRegularUsers();
}
....
As you can see, this action is considering the route and the role of the current user that is logged in and depending on the route or role, this action invoked the other actions that I created. This two other actions fetch via commit (mutation) the users state, that is an array of user objects:
users/store/state.js
export default {
users = []
}
The problem:
Looking at the chrome console, this is the error:
The undefined or null error, means that this.user.role is udenfined
And If I look at vuex-devtools you will see that user has the role assigned, even in getters and state: User role - getter and state
The question:
I have done anything in actions, such as this.$store.getters.user.role and nothing happens.
So, hope you can help me with this. I think I have explained carefully the situation I am having.
Thanks for your support.
Get the users of the role:
var routeName = this.$route.name // users or admins
this.getUsers(route, this.user.role)
set mapGetters in computed object:
computed: {
...mapGetters({
user: 'auth/user'
})
}
and use vuex getters for this line
this.items = this.$store.state.users
Related
With the first login in my app, users get a possibility to leave their address. When this address is stored, the user are pushed to their dashboard. Second login the user go straight to the dashboard.
I have 2 Vuex states that are updated with the response.data. 'Signed' leads to address page, 'Frequent' leads to 'dashboard'.
//PROMPT.VUE
mounted () {
this.getPrompt()
},
computed: {
promptStatus () {
return this.$store.getters.getPrompt
}
},
methods: {
async getPrompt() {
try{
await //GET axios etc
// push prompt status in Store
let value = response.data
this.$store.commit('setPrompt', value)
if (this.promptStatus === 'signed') {
this.$router.push({path: '/adres'})
}
if (this.promptStatus === 'frequent') {
this.$router.push({path: '/dashboard'})
}
When user leaves the address I reset the vuex.state from 'signed' to 'frequent'.
//ADRES.VUE
//store address
let value = 'frequent'
this.$store.commit('setPrompt', value)
this.$router.push({name: 'Prompt'})
The Vuex.store is refreshed. But the Prompt.vue wil not re-render with the new vuex.status. Many articles are written. Can 't find my solution. Maybe I organize my pages the wrong way.
In views, it is not recommended to mutate data (call commit) outside vuex. Actions are created for these purposes (called from the component using dispatch). In your case, you need to call action "getPrompt" from the store, but process routing in the authorization component. This is more about best practice
To solve your problem, you need to make a loader when switching to dashboard. Until the data is received, you do not transfer the user to the dashboard page
Example
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "DashboardLayout",
components: { ..., ... },
data: () => ({
isLoad: false
}),
async created() {
this.isLoad = false;
try {
await this.$store.dispatch('getData');
this.isLoad = true;
} catch (error) {
console.log(error)
}
}
});
</script>
Data is received and stored in the store in the "getData" action.
The referral to the dashboard page takes place after authorization. If authorization is invalid, the router.beforeEach handler (navigation guards) in your router/index.js should redirect back to the login page.
Learn more about layout in vuejs
Learn more about navigation guards
Imagine the following routes. I'm using Vue and vue-router syntax right now, but I figure the question applies to other SPA frameworks as well.
{
path: 'user/:id', component: require('User.vue'),
children: [
{ path: 'edit', component: require('UserEdit.vue'), }
]
}
In User.vue, the user object is fetched using the route id parameter upon component creation:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
In UserEdit.vue, a user is also fetched, and in 85% of the cases this will be the user that was also fetched in User.vue:
data() {
return { user: null }
},
created() {
this.user = fetchUser(this.$route.params.id)
}
Question: if we would navigate from User.vue to UserEdit.vue, it is apparent that (most probably) the same user object will be fetched again. How can this kind of code duplication be avoided? How should I pass the previously fetched data down to a child route?
I guess I should somewhere check if the route parameters remain equal, because if they aren't we're editing another user and the User data should be fetched anyway...
Time for a state management store (like vuex)? If so, when the app navigates away from user pages, should the user store be cleared, or do you keep the last fetched user always in memory?
I'm having a hard time to come up with something DRY.
Looking forward to your advice and some hands-on code examples.
Use vuex for state management. For example, setting something like lastUser and userData which could be accessed from any component. fetchUser would then be an action in the store:
Store
state: {
lastUser: '',
userData: null
},
actions: {
fetchUser({ state }, user) {
if (state.userData && user == state.lastUser) {
return state.userData;
} else {
// Api call, set userData and lastUser, return userData
}
}
}
User
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}
UserEdit
async created() {
this.user = await this.$store.dispatch('fetchUser', this.$route.params.id);
}
I limit my application to load to the DOM only if I have user details:
<template>
<div id="app">
<template v-if="loggedUser">
//...
</template>
</div>
</template>
where loggedUser is a computed property from the store:
computed: {
loggedUser() {
return this.$store.getters.user;
}
}
The issue is that other components rely on this property existing. In one component, called admin under the route /admin for example, when mounted() I pass the user object from the store to a method which in turn executes an HTTP request:
mounted(){
this.someFunc(this.$store.getters.user)
}
but the issue is sometimes the user exists and sometimes the user doesn't. This is true if the user tries to load the app directly from the admin page andthe user doesn't exist. One possible option to solve this issue is to use watch over the a computed property that returns the user from the store:
computed: {
user() {
return this.$store.getters.user;
}
},
watch: {
user(newVal) {
if(newVal) this.someFunc(this.$store.getters.user)
}
}
and while this might work it feels tedious even for this example. Nevertheless, bigger more complex issues arise due to this problem.
Another possible option came is to try and save the user in localStorage but I guess vuex should be able to solve my issue without using any type of client side storage solutions. Any idea how I can solve this issue? Is there a more robust way to ensure that the user is available across my entire application?
If you using the vue router you can authenticate a user there:
const ifAuthenticated = (to, from, next) => {
if (store.state.token) { // or state.user etc.
next()
return
}
next('/Adminlogin') // if not authenticated
and the router path looks like that:
{
path: '/AdminUI',
name: 'AdminUI',
component: AdminUI,
beforeEnter: ifAuthenticated
}
Another possible solution:
<v-template
v-show="$store.state.isUserLoggedIn"
</v-template>
dont forget to import { mapState } from "vuex";
and in the store:
getters: {
isUserLoggedIn: state => !!state.token
}
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 tried asking this question in the Vue Forums with no response, so I am going to try repeating it here:
I have an app where clients login and can manage multiple accounts (websites). In the header of the app, there’s a dropdown where the user can select the active account, and this will affect all of the components in the app that display any account-specific information.
Because this account info is needed in components throughout the app, I tried to follow the store example shown here (Vuex seemed like overkill in my situation):
https://v2.vuejs.org/v2/guide/state-management.html
In my src/main.js, I define this:
Vue.prototype.$accountStore = {
accounts: [],
selectedAccountId: null,
selectedAccountDomain: null
}
And this is my component to load/change the accounts:
<template>
<div v-if="hasMoreThanOneAccount">
<select v-model="accountStore.selectedAccountId" v-on:change="updateSelectedAccountDomain">
<option v-for="account in accountStore.accounts" v-bind:value="account.id" :key="account.id">
{{ account.domain }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'AccountSelector',
data: function () {
return {
accountStore: this.$accountStore,
apiInstance: new this.$api.AccountsApi()
}
},
methods: {
updateSelectedAccountDomain: function () {
this.accountStore.selectedAccountDomain = this.findSelectedAccountDomain()
},
findSelectedAccountDomain: function () {
for (var i = 0; i < this.accountStore.accounts.length; i++) {
var account = this.accountStore.accounts[i]
if (account.id === this.accountStore.selectedAccountId) {
return account.domain
}
}
return 'invalid account id'
},
loadAccounts: function () {
this.apiInstance.getAccounts(this.callbackWrapper(this.accountsLoaded))
},
accountsLoaded: function (error, data, response) {
if (error) {
console.error(error)
} else {
this.accountStore.accounts = data
this.accountStore.selectedAccountId = this.accountStore.accounts[0].id
this.updateSelectedAccountDomain()
}
}
},
computed: {
hasMoreThanOneAccount: function () {
return this.accountStore.accounts.length > 1
}
},
mounted: function () {
this.loadAccounts()
}
}
</script>
<style scoped>
</style>
To me this doesn’t seem like the best way to do it, but I’m really not sure what the better way is. One problem is that after the callback, I set the accounts, then the selectedAccountId, then the selectedAccountDomain manually. I feel like selectedAccountId and selectedDomainId should be computed properties, but I’m not sure how to do this when the store is not a Vue component.
The other issue I have is that until the selectedAccountId is loaded for the first time, I can’t make any API calls in any other components because the API calls need to know the account ID. However, I’m not sure what the best way is to listen for this change and then make API calls, both the first time and when it is updated later.
At the moment, you seem to use store to simply hold values. But the real power of the Flux/Store pattern is actually realized when you centralize logic within the store as well. If you sprinkle store-related logic across components throughout the app, eventually it will become harder and harder to maintain because such logic cannot be reused and you have to traverse the component tree to reach the logic when fixing bugs.
If I were you, I will create a store by
Defining 'primary data', then
Defining 'derived data' that can be derived from primary data, and lastly,
Defining 'methods' you can use to interact with such data.
IMO, the 'primary data' are user, accounts, and selectedAccount. And the 'derived data' are isLoggedIn, isSelectedAccountAvailable, and hasMoreThanOneAccount. As a Vue component, you can define it like this:
import Vue from "vue";
export default new Vue({
data() {
return {
user: null,
accounts: [],
selectedAccount: null
};
},
computed: {
isLoggedIn() {
return this.user !== null;
},
isSelectedAccountAvailable() {
return this.selectedAccount !== null;
},
hasMoreThanOneAccount() {
return this.accounts.length > 0;
}
},
methods: {
login(username, password) {
console.log("performing login");
if (username === "johnsmith" && password === "password") {
console.log("committing user object to store and load associated accounts");
this.user = {
name: "John Smith",
username: "johnsmith",
email: "john.smith#somewhere.com"
};
this.loadAccounts(username);
}
},
loadAccounts(username) {
console.log("load associated accounts from backend");
if (username === "johnsmith") {
// in real code, you can perform check the size of array here
// if it's the size of one, you can set selectedAccount here
// this.selectedAccount = array[0];
console.log("committing accounts to store");
this.accounts = [
{
id: "001234",
domain: "domain001234"
},
{
id: "001235",
domain: "domain001235"
}
];
}
},
setSelectedAccount(account) {
this.selectedAccount = account;
}
}
});
Then, you can easily import this store in any Vue component, and start referencing values, or call methods, from this store.
For example, suppose you are creating a Login.vue component, and that component should redirect when user object becomes available within a store, you can achieve this by doing the following:
<template>
<div>
<input type="text" v-model="username"><br/>
<input type="password" v-model="password"><br/>
<button #click="submit">Log-in</button>
</div>
</template>
<script>
import store from '../basic-store';
export default {
data() {
return {
username: 'johnsmith',
password: 'password'
};
},
computed: {
isLoggedIn() {
return store.isLoggedIn;
},
},
watch: {
isLoggedIn(newVal) {
if (newVal) { // if computed value from store evaluates to 'true'
console.log("moving on to Home after successful login.");
this.$router.push({ name: "home" });
}
}
},
methods: {
submit() {
store.login(this.username, this.password);
}
}
};
</script>
In addition, with isSelectedAccountAvailable we compute, we can easily disable/enable button on the screen, to prevent user from making API calls until an account is selected:
<button :disabled="!isSelectedAccountAvailable" #click="performAction()">make api call</button>
If you want to see the whole project, you can access it from this runnable codesandbox. Pay attention at how basic-store.js is defined and used in Login.vue and Home.vue. And, if you'd like, you can also see how store is defined in vuex by taking a peek at store.js.
Good luck!
Updated:
About how you should organize dependent/related API calls, the answer is actually right in front of you. If you take a closer look at the store, you'll notice that my login() method subsequently calls this.loadAccounts(username) once the login succeeds. So, basically, you have all the flexibility to chain/nested API calls in store's methods to accommodate your business rules. The watch() is there simply because the UI needs to perform navigation based on change(s) made in the store. For most simple data changes, computed properties will suffice.
Further, from how I designed it, the reason watch() is used in <Login> component is twofold:
Separation of concerns: for me who has been working on server-side code for years, I'd like my view-related code to be cleanly separated from model. By restricting navigation logic inside a component, my model in a store doesn't need to know/care about navigation at all.
However, even if I don't separate concerns, it will still be pretty hard to import vue-router into my store. This is because my router.js already imports basic-store.js to perform navigation guard preventing unauthenticated users from accessing <Home> component:
router.beforeEach((to, from, next) => {
if (!store.isLoggedIn && to.name !== "login") {
console.log(`redirect to 'login' instead of '${to.name}'.`);
next({ name: "login" });
} else {
console.log(`proceed to '${to.name}' normally.`);
next();
}
});
And, because javascript doesn't support cyclic dependency yet (e.g., router imports store, and store imports router), to keep my code acyclic, my store can't perform route navigations.