Vue2 + Vuex - not rendering array that is set successfully in store - vue.js

I am trying to render 209,579 options and I think that I am not using Vuex lifecycle + axios correctly
I see all options set in store state via Vuex in console.
No errors are displayed
Options are empty and not rendering the state cityNamesarray
main.js is exporting store to all components
I think I am not applying this lifecycle correctly, where I should use getters here, can someone bring order to my lifecycle?
store.js
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
isTrue: true,
cityNames: [],
},
getters:{
getCityNames(state){
return state.cityNames;
}
},
mutations: {
SetCityNames(state, cityNames){
state.cityNames = cityNames
},
actions: {
loadData({commit}){
axios.get('http://localhost:3000/allCities')
.then(function (response) {
commit('SetCityNames', response.data)
}).catch(function (error) {
console.log(error);
});
}
},
});
script
export default {
name: "Weather",
methods: {
allCityNames() {
this.$store.dispatch('loadData')
}
},
created() {
this.allCityNames();
}
}
template
<select>
<option disabled value="">Please select one</option>
<option v-for="cityName in $store.cityNames">{{cityName}}</option>
</select>
Thanks,
Bud

I have changed my code to be executed from compute, only to find out the error (at last!) which was: maximum stacksize exceeded, at this point I understood that Vue is not letting me render such a huge array (209,579 items) into view.
part I - Code Change:
I have created an isLoaded state that is set to true once axios commits it's response,
I'm still not sure if this is the best method due to the async nature of axios's call, it could have not finished with commit('SetCityNames', response.data); and right after invoking the commit, it would invoke the next : commit('changeLoadedState');
so i've added to state:isLoaded: false
added a getter: didItLoad(state){return state.isLoaded}
added a mutation: changeLoadedState(state){state.isLoaded = true}
added a commit (commit('changeLoadedState');) to my axios call in actions:
loadData({commit}) {
axios.get('http://localhost:3000/allCities')
.then(function (response) {
commit('SetCityNames', response.data);
commit('changeLoadedState');
}).catch(function (error) {
console.log(error);
});
}
In my component I am still dispatching the axios call in methods since it being called first, and added a computed method for the render side, as follows:
computed:{
isLoaded(){
return this.$store.getters.didItLoad;
},
renderCities(){
return this.$store.getters.getCityNames;
}
}
In My rendered template I first check with my select the loaded status and only then populate the options:
<select v-if="isLoaded">
<option disabled value="">Please select one</option>
<option v-for="cityName in renderCities">{{cityName}}</option>
</select>
Part II - Change Payload Size
So after setting my code straight, I went into my node express server and changed my route's loop to stop at 1000 items, and all worked great.
At this point I was curious what happens what happens if I start adding zeroes, so at 10K items, takes 1-2 sec to load options, opening the dropdown starts to show signs of latency due to stress, at 50K items it takes around 5 seconds to open the dropdown.
Bottom Line
The issue is not the size of the array, Vuex works amazing, getting a 209,579 item array in ~800ms which included backend parsing by Express.js (my whole stack is local so no network latency).
I will try to create an autocomplete that would start listing from 2nd or 3rd character.
Thanks to replying members.

You have a getters named getCityNames.It is $store.getters.getCityNames not $store.cityNames.
So change
<option v-for="cityName in $store.cityNames">{{cityName}}</option>
to
<option v-for="cityName in $store.getters.getCityNames">{{cityName}}</option>
it would be better to refactor to use a computed property rather than inlining in the template.
<option v-for="cityName in cityNames">{{cityName}}</option>
//script
computed: {
cityNames() {
return this.$store.getters.getCityNames;
}
}

Related

Vue/Nuxt: How to define a global method accessible to all components?

I just want to be able to call
{{ globalThing(0) }}
in templates, without needing to define globalThing in each .vue file.
I've tried all manner of plugin configurations (or mixins? not sure if Nuxt uses that terminology.), all to no avail. It seems no matter what I do, globalThing and this.globalThing remain undefined.
In some cases, I can even debug in Chrome and see this this.globalThing is indeed defined... but the code crashes anyway, which I find very hard to explain.
Here is one of my many attempts, this time using a plugin:
nuxt.config.js:
plugins: [
{
src: '~/plugins/global.js',
mode: 'client'
},
],
global.js:
import Vue from 'vue';
Vue.prototype.globalFunction = arg => {
console.log('arg', arg);
return arg;
};
and in the template in the .vue file:
<div>gloabal test {{globalFunction('toto')}}</div>
and... the result:
TypeError
_vm.globalFunction is not a function
Here's a different idea, using Vuex store.
store/index.js:
export const actions = {
globalThing(p) {
return p + ' test';
}
};
.vue file template:
test result: {{test('fafa')}}
.vue file script:
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions({
test: 'globalThing'
}),
}
};
aaaaaaaaand the result is.........
test result: [object Promise]
OK, so at least the method exists this time. I would much prefer not to be forced to do this "import mapActions" dance etc. in each component... but if that's really the only way, whatever.
However, all I get is a Promise, since this call is async. When it completes, the promise does indeed contain the returned value, but that is of no use here, since I need it to be returned from the method.
EDIT
On the client, "this" is undefined, except that..... it isn't! That is to say,
console.log('this', this);
says "undefined", but Chrome's debugger claims that, right after this console log, "this" is exactly what it is supposed to be (the component instance), and so is this.$store!
I'm adding a screenshot here as proof, since I don't even believe my own eyes.
https://nuxtjs.org/guide/plugins/
Nuxt explain this in Inject in $root & context section.
you must inject your global methods to Vue instance and context.
for example we have a hello.js file.
in plugins/hello.js:
export default (context, inject) => {
const hello = (msg) => console.log(`Hello ${msg}!`)
// Inject $hello(msg) in Vue, context and store.
inject('hello', hello)
// For Nuxt <= 2.12, also add 👇
context.$hello = hello
}
and then add this file in nuxt.config.js:
export default {
plugins: ['~/plugins/hello.js']
}
Use Nuxt's inject to get the method available everywhere
export default ({ app }, inject) => {
inject('myInjectedFunction', (string) => console.log('That was easy!', string))
}
Make sure you access that function as $myInjectedFunction (note $)
Make sure you added it in nuxt.config.js plugins section
If all else fails, wrap the function in an object and inject object so you'd have something like $myWrapper.myFunction() in your templates - we use objects injected from plugins all over the place and it works (e.g. in v-if in template, so pretty sure it would work from {{ }} too).
for example, our analytics.js plugin looks more less:
import Vue from 'vue';
const analytics = {
setAnalyticsUsersData(store) {...}
...
}
//this is to help Webstorm with autocomplete
Vue.prototype.$analytics = analytics;
export default ({app}, inject) => {
inject('analytics', analytics);
}
Which is then called as $analytics.setAnalyticsUsersData(...)
P.S. Just noticed something. You have your plugin in client mode. If you're running in universal, you have to make sure that this plugin (and the function) is not used anywhere during SSR. If it's in template, it's likely it actually is used during SSR and thus is undefined. Change your plugin to run in both modes as well.
This would be the approach with Vuex and Nuxt:
// store/index.js
export const state = () => ({
globalThing: ''
})
export const mutations = {
setGlobalThing (state, value) {
state.globalThing = value
}
}
// .vue file script
export default {
created() {
this.$store.commit('setGlobalThing', 'hello')
},
};
// .vue file template
{{ this.$store.state.globalThing }}

Default select option to first index when populating via array from Vuex store in Vue Component

I have an array in my Vuex store called Projects. I want to loop through these projects and default to the first item. I have setup a v-model on this select input so I can use the chosen result in my local component.
I read on this SO how I can use v-modal to do this.
However because I populate via Vuex store I think I need to do differently.
So I made my Vuex action I call a promise so in my component I can determine when it has resolved, so I can then populate my local component data (and thus the select input).
loadData: ({ commit }, payload) => {
return new Promise((resolve, reject) => {
// get projects on load
axios
.get("/api/projects/")
.then(function(response) {
commit("updateProjects", response.data);
resolve();
})
.catch(function(error) {
// handle error
throw error;
});
});
};
Then in my local component I have the following triggered on created():
created() {
this.$store
.dispatch("loadData")
.then((this.modalCreate.project = this.projects)); // dispatch loading
}
And within my component I have the following:
<select class="form-control" v-model="modalCreate.project">
<option
v-for="(project, index) in projects"
:value="project"
:key="project.id"
>
{{ project.name }}
</option>
</select>
In the above I have used mapState to map my store.projects to local projects.
In this setup I can see the select options populated from the local projects (from Vuex) but I cannot get the select form to default to the first index.
I suspect this is because I have not correctly made my modalCreate.project the first store.project object. Currently my modalCreate.project is undefined.
Most grateful for any advice on how to best achieve this and whether mapping Vuex state to local state is over-engineering a solution.
--
Perhaps this cloning solution can be applied? I had no luck though: SO Link
I suppose you want to select the first project (index === 0) as your default project:
created() {
this.$store
.dispatch("loadData")
.then(() => {
this.modelCreate = {
...this.modelCreate,
project: this.projects[0]
}
});
}

Page reload causes Vuex getter to return undefined

Using Vue.js (Vuetify for FE).
A page reload causes the getter in Vuex to fail with pulling required data from the store. The getter returns undefined. The code can be found on GitHub at: https://github.com/tineich/timmyskittys/tree/master/src
Please see the full details on this issue at timmyskittys.netlify.com/stage1. This page has complete info on the issue and instructions on how to view the issue.
Note, there is mention of www.timmyskittys.com in the issue description. This is the main site. timmyskittys.netlify.com is my test site. So, they are the same for all intents and purposes. But, my demo of this issue is at the Netlify site.
I read the complete issue in the website you mentioned. It's a generic case.
Say, for cat details page url: www.timmyskittys.com/stage2/:id.
Now in Per-Route Guard beforeEnter() you can set the cat-id in store. Then from your component call the api using the cat-id (read from getters)
I found the solution to my issue:
I had to move the call of the action which calls the mutation that loads the .json file (dbdata.json) into a computed() within App.vue. This was originally done in Stage1.vue.
Thanks all for responding.
I had the same issue and my "fix" if it can be called that was to make a timer, so to give the store time to get things right, like so:
<v-treeview
:items="items"
:load-children="setChildren"
/>
</template>
<script>
import { mapGetters } from 'vuex'
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
export default {
data () {
return {
children: []
}
},
computed: {
...mapGetters('app', ['services']),
items () {
return [{
id: 0,
name: 'Services',
children: this.children
}]
}
},
methods: {
async setChildren () {
await pause(1000)
this.children.push(...this.services)
}
}
}
</script>
Even though this is far from ideal, it works.

Data() VS asyncData() in Nuxt & vue

Both data() and async data() gives the same result (and it is obvious that the results from asyncData() override the results from data())
and both results in HTML code in the source code (i.e the code rendered in the server-side)
also, both can be used to "await" the data to be fetched (ex: using axios)
so, what is the difference between them?
<template>
<div>
<div>test: {{ test }}</div>
<div>test2: {{ test2 }}</div>
<div>test2: {{ test3 }}</div>
<div>test2: {{ test4 }}</div>
</div>
</template>
<script>
export default {
asyncData(app) {
return {
test: "asyncData",
test2: "asyncData2",
test3: "asyncData3"
};
},
data() {
return {
test: "data",
test2: "data2",
test4: "data4"
};
},
};
</script>
result:
test: asyncData
test2: asyncData2
test2: asyncData3
test2: data4
The simplest answer is data() is processed on the client side, however asyncData() section is processed on the server side on the call for Nuxt() once and on the client side once more.
The biggest advantage of nuxt is it's ability to render content on the server side. If you load your content using promise on the client side, say for example in the mounted section as:
data() {
return {
products: []
}
},
mounted() {
axios.get('/api/v1/products').then(response => {
this.products = response.data
})
}
the javascript code is sent to the client as it is and the browser is responsible to run the promise to fetch the data from the api. However if you put the promise inside asyncData:
asyncData() {
return axios.get('/api/v1/products').then(response => {
// Note that you can't access the `this` instance inside asyncData
// this.products = response.data
let products = response.data
return { products } // equivalent to { products: products }
})
}
The data fetching is done on the server side and the result is pre-rendered and an html with the data (rendered into it) is sent to the client. So in this case the client won't be receiving the javascript code to process the api call by itself, but instead it receives something like this:
<ul>
<li>
Product 1
</li>
<li>
Product 2
</li>
<li>
Product 3
</li>
</ul>
The result we return from asyncData is merged with what is in data. It's not replaced but merged.
You may want to fetch data and render it on the server-side. Nuxt.js adds an asyncData method that lets you handle async operations before setting the component data.
asyncData is called every time before loading the page component and is only available for such. It will be called server-side once (on the first request to the Nuxt app) and client-side when navigating to further routes. This method receives the context object as the first argument, you can use it to fetch some data and return the component data.
The result from asyncData will be merged with data.
export default {
data () {
return { project: 'default' }
},
asyncData (context) {
return { project: 'nuxt' }
}
}
Nuxt's main attraction is the serverside rendering part, that helps with SEO. So we can assume any deviation from the normal "Vue-way" of doing things is most likely because it is in service of the SSR (which Vue naturally doesn't allow, hence we use Nuxt). Knowing that, we can pretty much say asyncData() contains the SEO-focused data that is send on the first page-load.
Short answer => use asyncData() for fetched template-based SEO-focused content.

How to define a subscription based on route params?

I'm using RxJS using vue-rx and I have a route that needs to fetch a slightly different AJAX API request depending on the params.
I've been playing with all kinds of different approaches, but this is the one that +should have+ worked.
import Vue from 'vue'
import numeral from 'numeral'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/interval'
import 'rxjs/add/operator/switchMap'
import 'rxjs/add/operator/map'
import axiosObservable from '../lib/axiosObservable'
export default {
name: 'Exchange',
props: ['exchange_name'],
methods: {
exchangeFetch (exchangeName) {
return Observable
.interval(3000)
.switchMap(axiosObservable.get(Vue.config.infoCoinUrl + '/exchanges/' + exchangeName + '/market-pairs'))
.map((response) => response.data)
}
},
mounted () {
alert(exchangeName)
this.$subscribeTo(
this.exchangeFetch(this.exchange_name),
(data) => {
this.market_pairs = data
})
},
data () {
return {
market_pairs: []
}
},
But what happens is that the alert gets executed only once during browsing (and the wrong AJAX call gets ran every time).
I'm a bit noobish in all of this (Vue & JS), I'm suspecting this might be a bug in the vue-rx framework - or at least a surprising behavior (for noobie me).
The thing that I love about vue-rx is how it integrates into vue.js lifecycles and removes the danger of leaking observables (which coupled with an AJAX call accounts to a ddos attack).
I'm looking for a solution that uses vue-rx API , or at least doesn't require me to stop the observables "manually".
UPDATE 1 seems like the issue has nothing to do with vue-rx, it's the mounted block that doesn't get executed on props change....
The exchanges are loaded like this and it generally works, there should be nothing wrong with this...
<router-link v-for="exchange in exchanges" v-bind:key="exchange.name" class="navbar-item" :to="{ name: 'exchange-show', params: { exchange_name: exchange.name }}">{{ exchange.name }}</router-link>