Vuex is resetting already set states - vue.js

Have started to play around with Vuex and am a bit confused.
It triggers the action GET_RECRUITERS everytime I load the component company.vue thus also making an api-call.
For example if I open company.vue => navigate to the user/edit.vue with vue-router and them go back it will call the action/api again (The recruiters are saved in the store accordinly to Vue-dev-tools).
Please correct me if I'm wrong - It should not trigger the action/api and thus resetting the state if I go back to the page again, correct? Or have I missunderstood the intent of Vuex?
company.vue
<template>
<card>
<select>
<option v-for="recruiter in recruiters"
:value="recruiter.id">
{{ recruiter.name }}
</option>
</select>
</card>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
middleware: 'auth',
mounted() {
this.$store.dispatch("company/GET_RECRUITERS")
},
computed: mapGetters({
recruiters: 'company/recruiters'
}),
}
</script>
company.js
import axios from 'axios'
// state
export const state = {
recruiters: [],
}
// getters
export const getters = {
recruiters: state => {
return state.recruiters
}
}
// actions
export const actions = {
GET_RECRUITERS(context) {
axios.get("api/recruiters")
.then((response) => {
console.log('API Action GET_RECRUITERS')
context.commit("GET_RECRUITERS", response.data.data)
})
.catch(() => { console.log("Error........") })
}
}
// mutations
export const mutations = {
GET_RECRUITERS(state, data) {
return state.recruiters = data
}
}
Thanks!

That's expected behavior, because a page component is created/mounted again each time you route back to it unless you cache it. Here are a few design patterns for this:
Load the data in App.vue which only runs once.
Or, check that the data isn't already loaded before making the API call:
// Testing that your `recruiters` getter has no length before loading data
mounted() {
if(!this.recruiters.length) {
this.$store.dispatch("company/GET_RECRUITERS");
}
}
Or, cache the page component so it's not recreated each time you route away and back. Do this by using the <keep-alive> component to wrap the <router-view>:
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>

Related

how to make the nuxt child component wait until asyncData call get finish

For a form we have 2 components parent(for calling asyncdata and pass data as props to child) & child(form). I can properly fetch the props in child if I navigate using a link. But If I try to refresh the child component page it throws error as no props is passed. Found the reason to be that the parents asyncdata is not completing before the child render to sent the data in props.
Parent Component
<template>
<div>
<p>EDIT</p>
<NewListingModal :is-edit="true" :form-props="this.form" />
</div>
</template>
<script>
import NewListingModal from '#/components/NewListingModal.vue'
export default {
components: { NewListingModal },
async asyncData({ params, store }) {
const listing = await store.$db().model('listings').find(params.listing) //vuexorm call
if (typeof listing !== 'undefined') {
const convertedListing = JSON.parse(JSON.stringify(listing))
return {
name: '',
scrollable: true,
form: {names: convertedListing.names}
}
}
},
}
</script>
child component(other form data is removed to keep it understandable)
<template>
<div v-for="name in this.form.names" :key="name">
<p>{{ name }} <a #click.prevent="deleteName(name)">Delete<a /></a></p>
</div>
</template>
<script>
import Listing from '#/models/listing'
export default {
name: 'ListingModal',
props: {isEdit: {type: Boolean, default: false}, formProps: {type: Object}},
data() {
return {
name: '',
scrollable: true,
form: {names: this.formProps.names}
}
},
methods: {
addName() {
this.form.names.push(this.name)
this.name = ''
},
deleteName(name) {
const names = this.form.names
names.splice(names.indexOf(name), 1)
}
}
}
</script>
How can I make the NewListingModal component rendering wait until the asyncData completes in parent?
In my case, I used asyncData in my parent nuxt component, which fetches the data via store dispatch action, then set it to some store state key, via mutation.
Then I used validate method in my child component. Since Nuxt validate can return promises, I checked the vuex store first for fetched data. If there is none, I refetch it and return the promise instead.
In Parent component.vue
export default {
async asyncData({ params, store }) {
// Api operation which may take sometime
const {data} = await store.dispatch('fetch-my-data')
store.commit('setData', data) //This should be within above dispatch, but here for relevance
}
}
Here I am only fetching and saving to vuex store.
Child component.vue
export default {
async validate({ params, store }) {
let somedata = store.state.data //This is what you've set via parent's component mutation
return !!somedata || store.dispatch('fetch-my-data')
}
}
Here I am returning either the vuex store data (if exists), else refetch it.

Nuxtjs: Best way to show data in page header

I started to use Nuxt.js for SSR purposes. In the header I have a navigation menu which it's items should be requested from server side.
Here is default.vue
<template>
<div>
<app-header></app-header>
<nuxt />
<app-footer></app-footer>
</div>
</template>
<script>
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
export default {
components: {
'app-header': Header,
'app-footer': Footer,
}
}
</script>
So, what is the best way to fetch items and prevent sending request on every single page?
I thought of using Vuex to store this data. But I don't if it's a good solution or not.
You can use Vuex and declare your navbar items inside the store.
state: {
navbarItems: []
},
mutations: {
FETCH_ITEMS(state, navbarItems) {
state.navbarItems = navbarItems
}
},
actions: {
fetchItems({ commit }) {
Vue.http.get("your api here ") // or axios
.then((response) => {
commit("FETCH_ITEMS", response.body);
})
.catch((error => {
console.log("error")
}))
}
And inside your header
created() {
this.$store.dispatch("fetchItems")
}
Thanks

Mutations on Page Load [Nuxt] [Vuex]

(I'm new to vue and nuxt).
I currently have a <HeaderImage> component in my layouts/default.vue and would like to have each page to pass a different image url to that component.
Right now I'm using vuex $store for that purpose (but would love if there were a simpler way to pass the data), but I'm trying to figure out where in my pages/xyz.vue I should be using the mutation this.$store.commit('headerImg/setHeaderImage', 'someImage.jpg')
All of the examples I can find only use mutations on user events.
What you are trying to do probably doesn't have a particularly simple solution and how I would do it is use a store state element that is set by the component when it is loaded. The component would commit a mutation in the store that alters the state element. The layout would then use that state element through a getter to set the image url. Here is how I'd code that. In the store state i'd have an array of class names, let's call it 'headState', and an element that would be assigned one of those class names, called 'headStateSelect:
//store/index.js
state: {
headState: ['blue', 'red', 'green'],
headStateSelect : ''
}
In your component you can use fetch, or async fetch to commit a mutation that will set 'headStateSelect' with one of the 'headState' elements.
//yourComponent.vue
async fetch ({ store, params }) {
await store.commit('SET_HEAD', 1) //the second parameter is to specify the array position of the 'headState' class you want
}
and store:
//store/index.js
mutations: {
SET_HEAD (state, data) {
state.headStateSelect = state.headState[data]
}
}
In the store we should also have a getter that returns the 'headStateSelect' so our layout can easily get it.
getters: {
head(state) {
return state.headStateSelect
}
}
finally, in the layout we can use the computed property to get our getter:
//layouts/default.vue
computed: {
headElement() {
return this.$store.getters.head
}
}
and the layout can use the computed property to set a class like so:
//layouts/default.vue
<template>
<div :class="headElement">
</div>
</template>
The div in the layout will now be set with the class name 'red' (ie. store.state.headState[1]) and you can have a .red css class in your layout file that styles it however you want, including with a background image.
For now I've settled on creating it like this:
~/store/header.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const state = () => ({
headerImage: 'default.jpg'
})
const mutations = {
newHeaderImage(state, newImage) {
state.headerImage = newImage
}
}
export default {
namespaced: true,
state,
mutations
}
``
~/layouts/default.vue
<template>
<div id="container">
<Header />
<nuxt />
</div>
</template>
<script>
import Header from '~/components/Header'
export default {
components: {
Header
}
}
</script>
``
~/components/Header.vue
<template>
<header :style="{ backgroundImage: 'url(' + headerImage + ')'}" class="fixed">
<h1>Header Text</h1>
</header>
</template>
<script>
computed: {
var image = this.$store.state.header.headerImage
return require('~/assets/img/' + image)
}
</script>
``
~/pages/customHeader.vue
<template>
<main>
...
</main>
</template>
<script>
export default {
head() {
this.$store.commit('header/newHeaderImage', 'custom-header.jpg')
return {
title: this.title
}
}
}
</script>
But something feels off about putting the mutation in head() Is that correct?
And the next issue I am facing is how to return the header to default.jpg if a page doesn't change the state (which makes me think this is all the wrong approach).

Vuex - When to load/initialize store data from http server

I want to show some data in the menu-bar, that needs to be fetched remotely (http get call) to be correctly displayed. When my application loads, the store wasn't initialized yet. Where should I do that?
This is what I have right now. nodeInfo is an empty object, as long as no data is fetched.
navigation component
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
...
<div class="navbar-end">
<span class="navbar-item">
<div v-if="nodeInfo.latestSolidSubtangleMilestoneIndex">
{{nodeInfo.latestSolidSubtangleMilestoneIndex}} / {{nodeInfo.latestMilestoneIndex}}
</div>
<div v-else>
Node seems offline!
</div>
</span>
</div>
</div>
</nav>
</template>
<script>
import {mapGetters} from 'vuex';
export default {
name: 'Menu',
computed: {
...mapGetters(['nodeInfo']) // Only the getters, no actions called to initialize them.
}
};
</script>
<style scoped>
</style>
store:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import axios from 'axios';
const iri_ip = '192.168.1.199';
const iri_port = '14265';
const state = {
token: null,
loading: false,
nodeInfo: {}
};
const mutations = {
SET_NODE_INFO(state, info) {
state.nodeInfo = info;
}
};
const actions = {
fetchNodeInfo({commit}) {
axios(createIriRequest('getNodeInfo')).then(response => {
console.log(response.data);
commit('SET_NODE_INFO', response.data);
});
}
};
const getters = {
token: state => state.token,
loading: state => state.loading,
nodeInfo: state => state.nodeInfo
};
const loginModule = {
state,
mutations,
actions,
getters
};
function createIriRequest(command) {
return {
url: `http://${iri_ip}:${iri_port}`,
data: {'command': command},
method: 'post',
headers: {
'Content-Type': 'application/json',
'X-IOTA-API-Version': '1'
}
};
}
export default new Vuex.Store({
modules: {
loginModule
}
});
The naming doesn't make much sense at the moment. But would I need to call the "actions" from the create() method of the menu component? That would somehow be weird. It would be cool if my store could somehow make the initial http calls itself without needing to be triggered. I don't even know how to call an action just like that from the create() part.
Have a look at the vue.js lifecycle diagram here: https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram and read on the the lifecycle hooks here: https://v2.vuejs.org/v2/guide/instance.html#Instance-Lifecycle-Hooks.
It will hep you considerably in understanding when and where to add the stores dispatch method. this.$store.dispatch('fetchNodeInfo')
In Short:
Created hook:
Instance has been created, all the data observation, computed properties, methods, watch/event callbacks have been set up but the $el property isn't available yet.
Mounted hook:
Vue instance has been mounted, where el is replaced by the newly created vm.$el. el being the instance creation via new Vue({...}).
For your reading pleasure:
Lifecycle hooks: http://devdocs.io/vue~2-api-options-lifecycle-hooks/
#Bert was right. I added the dispatch method to the created() method of my component.
export default {
name: 'Menu',
created() {
this.$store.dispatch('fetchNodeInfo');
},
...
}

Vuex accessing state BEFORE async action is complete

I'm having issues where a computed getter accesses the state before it is updated, thus rendering an old state. I've already tried a few things such as merging mutations with actions and changing state to many different values but the getter is still being called before the dispatch is finished.
Problem
State is accessed before async action (api call) is complete.
Code structure
Component A loads API data.
User clicks 1 of the data.
Component A dispatches clicked data (object) to component B.
Component B loads object received.
Note
The DOM renders fine. This is a CONSOLE ERROR. Vue is always watching for DOM changes and re-renders instantly. The console however picks up everything.
Goal
Prevent component B (which is only called AFTER component) from running its computed getter method before dispatch of component A is complete.
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
searchResult: {},
selected: null,
},
getters: {
searchResult: state => {
return state.searchResult;
},
selected: state => {
return state.selected;
},
},
mutations:{
search: (state, payload) => {
state.searchResult = payload;
},
selected: (state, payload) => {
state.selected = payload;
},
},
actions: {
search: ({commit}) => {
axios.get('http://api.tvmaze.com/search/shows?q=batman')
.then(response => {
commit('search', response.data);
}, error => {
console.log(error);
});
},
selected: ({commit}, payload) => {
commit('selected', payload);
},
},
});
SearchResult.vue
<template>
<div>
//looped
<router-link to="ShowDetails" #click.native="selected(Object)">
<p>{{Object}}</p>
</router-link>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show);
},
},
}
</script>
ShowDetails.vue
<template>
<div>
<p>{{Object.name}}</p>
<p>{{Object.genres}}</p>
</div>
</template>
<script>
export default {
computed:{
show(){
return this.$store.getters.selected;
},
},
}
</script>
This image shows that the computed method "show" in file 'ShowDetails' runs before the state is updated (which happens BEFORE the "show" computed method. Then, once it is updated, you can see the 2nd console "TEST" which is now actually populated with an object, a few ms after the first console "TEST".
Question
Vuex is all about state watching and management so how can I prevent this console error?
Thanks in advance.
store.dispatch can handle Promise returned by the triggered action handler and it also returns Promise. See Composing Actions.
You can setup your selected action to return a promise like this:
selected: ({commit}, payload) => {
return new Promise((resolve, reject) => {
commit('selected', payload);
});
}
Then in your SearchResults.vue instead of using a router-link use a button and perform programmatic navigation in the success callback of your selected action's promise like this:
<template>
<div>
//looped
<button #click.native="selected(Object)">
<p>{{Object}}</p>
</button>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show)
.then(() => {
this.$router.push('ShowDetails');
});
},
},
}
</script>
You can try to use v-if to avoid rendering template if it is no search results
v-if="$store.getters.searchResult"
Initialize your states.
As with all other Vue' data it is always better to initialize it at the start point, even with empty '' or [] but VueJS (not sure if Angular or React act the same, but I suppose similar) will behave much better having ALL OF YOUR VARIABLES initialized.
You can define initial empty value of your states in your store instance.
You will find that helpful not only here, but e.g. with forms validation as most of plugins will work ok with initialized data, but will not work properly with non-initialized data.
Hope it helps.