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

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');
},
...
}

Related

Vuex is resetting already set states

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>

Call nuxt/axios module from external js/ts file

I am new to vue and trying to build my first vue app using nuxtjs. My problem right now has to do with architecture and folder structure.
In my other non-vue apps I always have a "services" directory where I keep all my code that makes http requests.
example under my services folder I will have a auth.ts file that contains code that posts login credentials to my API. This file/class returns a promise which I access from within my store.
I am trying to do this with vue using nuxtjs but I realised I am unable to access the axios module from anywhere aside my .vue file.
This is an example of how my code is now:
<template>
...
</template>
<script lang="ts">
import Vue from 'vue'
import ActionBar from '../../components/ActionBar.vue'
export default Vue.extend({
components: { ActionBar },
data() {
return {
example: ''
},
methods: {},
mounted() {
this.$axios.$get('/examples').then((res) => {
this.examples = res.data;
})
}
})
</script>
<style>
...
</style>
I would like to move the axios calls to their own files in my services folder. How do I do this?
what you can do is create a file inside the ./store folder, let's imagine, ./store/products.js, that will create a products store, inside, simple getters, mutations and actions:
export const state = () => ({
products: [],
fetchingProducts: false,
})
export const getters = {
getAllProducts(state) {
return state.products
},
hasProducts(state) {
return state.products.length > 0
},
isFetchingProducts(state) {
return state.fetchingProducts
},
}
export const mutations = {
setInitialData(state, products) {
state.products = products
},
setLoadingProducts(state, isLoading) {
state.fetchingProducts = isLoading
},
}
export const actions = {
async fetchProducts(context, payload) {
context.commit('setLoadingProducts', true)
const url = `/api/example/${payload.something}`
const res = await this.$axios.get(url)
context.commit('setInitialData', res.data)
context.commit('setLoadingProducts', false)
},
}
then in your .vue file, you can now use the store as:
<template>
<div>
<div v-if="isFetchingProducts"> loading... </div>
<div v-else-if="!hasProducts">no products found</div>
<div v-else>
<ul>
<li v-for="product in allProducts" :key="product.id">
{{ product.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
products: []
}
},
methods: {
...mapGetters({
isFetchingProducts: 'products/isFetchingProducts',
allProducts: 'products/getAllProducts',
hasProducts: 'products/hasProducts',
})
},
mounted() {
this.$store.dispatch('products/fetchProducts', {})
},
}
</script>
<style>
...
</style>
remember that:
to call a store action, you should use $store.dispatch()
to call a mutation, you should use $store.commit()
to call a getter, you should use $store.getter()
you can also use the Vuex helper mapGetters, mapActions and even mapMutations
You might also know that you can leverage the Plugins in Nuxt, that article has demo code as well so you can follow up really quick

How to re-use component that should use unique vuex store instance

I try to find a way to use vuex with reusable component which store data in a store. The thing is, I need the store to be unique for each component instance.
I thought Reusable module of the doc was the key but finally it doesn't seem to be for this purpose, or i didn't understand how to use it.
The parent component:
(the prop “req-path” is used to pass different URL to make each FileExplorer component commit the action of fetching data from an API, with that url path)
<template>
<div class="container">
<FileExplorer req-path="/folder/subfolder"></FileExplorer>
<FileExplorer req-path="/anotherfolder"></FileExplorer>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import FileExplorer from "#/components/FileExplorer.vue";
export default {
components: {
FileExplorer
}
};
</script>
The reusable component:
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
export default {
props: ["reqPath"],
},
computed: {
...mapState("fileExplorer", ["folderIndex"])
},
created() {
// FETCH DATA FROM API
this.$store
.dispatch("fileExplorer/indexingData", {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
}
};
</script>
store.js where I invoke my store module that I separate in different files, here only fileExplorer module interest us.
EDIT : I simplified the file for clarity purpose but I have some other state and many mutations inside.
import Vue from 'vue'
import Vuex from 'vuex'
// Import modules
import { fileExplorer } from '#/store/modules/fileExplorer'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
fileExplorer,
…
}
})
#/store/modules/fileExplorer.js
import ApiService from "#/utils/ApiService"
export const fileExplorer = ({
namespaced: true,
state: {
folderIndex: {},
},
mutations: {
// Called from action (indexingData) to fetch folder/fil structure from API
SET_FOLDERS_INDEX(state, data) {
state.folderIndex = data.indexingData
},
actions: {
// Fetch data from API using req-path as url
indexingData({
commit
}, reqPath) {
return ApiService.indexingData(reqPath)
.then((response) => {
commit('SET_FOLDERS_INDEX', response.data);
})
.catch((error) => {
console.log('There was an error:', error.response);
});
}
}
});
I need each component to show different data from those 2 different URL, instead i get the same data in the 2 component instance (not surprising though).
Thanks a lot for any of those who read all that !
Module reuse is about when you are creating multiple modules from the same module config.
First, use a function for declaring module state instead of a plain object.
If we use a plain object to declare the state of the module, then that
state object will be shared by reference and cause cross store/module
state pollution when it's mutated.
const fileExplorer = {
state () {
return {
folderIndex: {}
}
},
// mutations, actions, getters...
}
Then, dynamically register a new module each time a new FileExplorer component is created and unregister that module before the component is destroyed.
<template>
<div class="container">
<ul v-for="(item, index) in folderIndex" :key="index">
<li>Results: {{ item.name }}</li>
</ul>
</div>
</div>
</template>
<script>
import { fileExplorer } from "#/store/modules/fileExplorer";
import store from "#/store/index";
var uid = 1
export default {
props: ["reqPath"],
data() {
return {
namespace: `fileExplorer${uid++}`
}
},
computed: {
folderIndex() {
return this.$store.state[this.namespace].folderIndex
}
},
created() {
// Register the new module dynamically
store.registerModule(this.namespace, fileExplorer);
// FETCH DATA FROM API
this.$store
.dispatch(`${this.namespace}/indexingData`, {
reqPath: this.reqPath
})
.catch(error => {
console.log("An error occurred:", error);
this.errors = error.response.data.data;
});
},
beforeDestroy() {
// Unregister the dynamically created module
store.unregisterModule(this.namespace);
}
};
</script>
You no longer need the static module registration declared at store creation.
export default new Vuex.Store({
modules: {
// fileExplorer, <-- Remove this static module
}
})

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

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).