Vue template does not update when pinia store state changes - javascript

I'm in the process of migrating a Vue 2 app to Vue 3. It's gone well enough to this point, except that templates aren't updating based on Pinia state. I've looked at other SO questions related to this, but I can't resolve this problem.
Here are my components (parent and child) and store - Note, I'm using the options API
// Parent
<template>
<div class="wrapper">
<ActionButtonGroup />
</div>
</template>
<script>
import { useActionStore } from '#/stores/ActionStore';
import ActionButtonGroup from './components/ActionButtonGroup/main.vue';
export default {
name: 'ActionsParent'
components: {
ActionButtonGroup,
},
created() {
this.loadActions();
},
methods: {
loadActions() {
useActionStore().loadActions();
},
}
}
// Child
<template>
<div class="action-buttons d--f ai--c">
<Spinner class="ml-3" v-if="loading" />
</div>
</template>
<script>
import { mapState } from 'pinia';
import { useActionStore } from '#/stores/ActionStore';
import { Spinner } from '#/components/index';
export default {
name: 'ActionButtonGroup',
components: {
Spinner,
},
computed: {
...mapState(useActionStore, ['loading']),
}
}
</script>
// Store
import { ActionApi } from '#/api/index';
export const useActionStore = defineStore('Action', {
state: () => {
return {
loading: false,
};
},
actions: {
async loadActions() {
this.loading = true;
await this.fetchActions();
this.loading = false;
},
async fetchActions(id) {
const actions = await ActionApi.fetchActions();
return actions;
},
}
}
I've also tried using a setup function in the child, but that doesn't resolve the problem:
setup() {
const store = useActionStore();
const loading = computed(() => store.loading);
return {
loading
}
},

Sadly, I can't comment on this, so my response has to be an answer.
I don't think the problem has to do with Pinia. I checked and recreated it, and things are fine. I suggest looking into other areas, such as your Vue instance and the function itself that is being called. Try slowly removing things to bare bones to see if it'll work. You'll also want to confirm that after your "await" function "fetchActions" is called, it actually finishes, and the state updates.
If the state is updating, then you know it has to be related to the Vue instance. You could then try rebuilding in another Vue instance to help narrow down the issue.
I hope this helps!

Related

How to show data from nuxt js store?

I new on Nuxt JS and try tutorial on nuxt website, i make store in store/index.js
export const state = () => ({
mountain: [],
})
export const mutations = {
addMountain(state, mountain) {
state.mountain.push(mountain)
},
}
export const actions = {
async fetchMountain() {
const mountain = await fetch("https://api.nuxtjs.dev/mountains").then(
(res) => res.json()
);
// this.mountain = mountain
}
}
after that i make page on pages/index.js
<template>
<div>
<h1>Nuxt Mountains</h1>
<ul>
<li v-for="mount of mountain">{{ mount.title }}</li>
</ul>
<button #click="$fetch">Refresh</button>
</div>
</template>
<script>
import $store from "~/store";
export default {
fetch() {
return $store.state.mountain;
},
};
</script>
but i dont see anyting? someone can help me
This is how you can achieve this example.
/pages/index.vue
<template>
<div>
<h1>Nuxt Mountains</h1>
<p v-show="$fetchState.pending">Loading mountains...</p>
<ul v-show="!$fetchState.pending">
<li v-for="mountain of mountains" :key="mountain.slug">
{{ mountain.title }}
</li>
</ul>
<hr />
<button #click="$fetch">Refresh</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
async fetch() {
await this.fetchMountains()
},
computed: {
...mapState(['mountains']),
},
methods: {
...mapActions(['fetchMountains']),
},
}
</script>
Few notes on above:
mapState and mapActions is not mandatory here, you could access them via $store.dispatch etc directly but it's IMO more clean and quite explicit/good practice
don't forget the :key on the v-for, it's mandatory
prefer to use async/await everywhere rather than a mix with .then
/store/index.js
export const state = () => ({
mountains: [],
})
export const mutations = {
SET_MOUNTAINS(state, mountains) {
state.mountains = mountains
},
}
export const actions = {
async fetchMountains({ commit }) {
const response = await fetch('https://api.nuxtjs.dev/mountains')
const mountains = await response.json()
commit('SET_MOUNTAINS', mountains)
},
}
Few notes on above:
using UPPER_SNAKE_CASE is a nice convention for mutations
again, async + await combo
mountains (plural) seems more appropriate because we will have several ones
calling the mutation after the HTTP call in the action is the usual way to go with Nuxt2
the store object already exists within your context.
so you don't need to import it at all ...
example of how to use it:
computed: {
mountain () {
return this.$store.state.mountain;
}
}
ps. why inside the computed? because we don't need the component to be rerendered every time an update happens to the module. only if the data you are accessing got updated that should trigger rerendering ...
but that is also not the best practice.
you should always use modules.
to understand that better you should know that nuxt imports all the files inside the store folder automatically.
so every file you create inside the store file will be used as a new module in vuex store.
so for instance if you created a file called todo.js inside the store folder you will access it anywhere in the project using the following :
computed: {
mountain () {
return this.$store.state.todo.mountain;
}
}
i would suggest you take a look into that from here :
https://nuxtjs.org/docs/directory-structure/store/

How to add vuex action with bool return?

Im trying to add a simple common action in vuex that returns a bool,but it looks like it doesn't return what I need.
My child component call:
<ArrowDownIcon
class="campaigns-grid__header-direction"
v-if="isOrderedBy(column.id)"
class="{'campaigns-grid__header-direction--asc': order.isDescending}"
/>
My imports and vuex call on root component:
import { createNamespacedHelpers } from 'vuex'
const { mapActions } = createNamespacedHelpers('campaigns')
export default {
methods: {
...mapActions(['isOrderedBy', 'orderBy'])
}
}
My vuex module ('campaigns'):
export default {
namespaced: true,
actions: {
isOrderedBy (column) {
if (column === 'test') {
return true
}
return false
},
}
}
As much as this might possibly work, You shouldn't use Vuex Actions to return Boolean values since Vuex actions return promises. The ideal process of working with vuex is:
-> dispatch Action -> Action makes Network calls -> Actions Commits Data to State -> Getters Pull data from State
To Solve this issue use Mixins, Since you're trying to make the isOrderedBy(..) function available application wide, and you also want it to return a boolean value depending on the provided argument.
Create a mixin folder if you don't already have one, and add an orderMixin.js file: the file should contain the isOrderedBy() function as a method.
export const orderMixin = {
methods: {
isOrderedBy(column) {
return column === 'test'
}
}
}
Then within your component import the mixin and use it.
<template>
...
<ArrowDownIcon
class="campaigns-grid__header-direction"
v-if="isOrderedBy(column.id)"
class="{'campaigns-grid__header-direction--asc': order.isDescending}"
/>
...
</template>
<script>
import { orderMixin } from 'path-to-mixins/orderMixin'
export default {
mixins: [orderMixin],
...
}
</script>
In vuex action returns promise.
So if you want to get return value from the action you can do like
if (await isOrderedBy()) {
console.log('isOrderBy returns true')
} else {
console.log('isOrderBy returns false')
}

How to receive dynamic data from vuex state into component

I'm trying to learn vuex but I think I am missing some basic understanding. Any advice please.
From one component I am dispatching the scale value of my zoomable map to vuex store.
Store.js
export default new Vuex.Store({
state: {
scale:""
},
getters:{
MJERILO: state => {
return state.scale
}
},
mutations:{
STORESCALE: (state, payload) => {
state.scale = payload
}
},
actions:{
RECEIVECURRENTSCALE: (context, payload) => {
context.commit("STORESCALE", payload);
}
}
})
This part is working well because in vue dev tool I can see that the scale number is changing in mutation, state and getters when I do zoom in/out with my mouse. (Do, in mutation is changing automaticaly, and for state and getters I need to press load state. I guess this work like this)
So the problem is probably in the way how I am trying to receive data from vuex state into some other component.
I tried:
Map1.vue
mounted(){
var scale = this.$store.getters.MJERILO
}
But I just get the value stored in state property mjerilo (in this case empty). And I need dynamic that I sent to state.
For static data this worked perfectly (I tried with simple array).
I also tried to retry data in computed, but I have a similar problem. In this case in mounted I get just the first scale value
computed: {
mjerilo(){
return this.$store.getters.MJERILO
}
}
mounted(){
var scale = this.mjerilo
}
I am quite lost. From readings I understand that when ever I scroll my map with mouse I am sending data to action for "registration", than through mutation I am storing this data in state. From state I can get the last updated data (in this case scale) in any other vue component of my app?
UPDATE: I am adding Map1.vue component
<template>
<svg-map name="Zupanije" ></svg-map>
</template>
<script>
import * as d3 from 'd3'
import SvgMap from "./SvgMap"
export default {
name: "Map1",
components: {
"svg-map":SvgMap
},
mounted(){
......lots of javascrip code
.
.
var scale = this.$store.getters.MJERILO
}
}
</script>
I believe you are looking for a function like watch
data: {
scale: this.$state.scale
},
watch: {
'$store.state.scale': (newVal) => {
this.scale = newVal;
}
}
I'm not sure what's wrong but you could try this, what happens then? How you dispatch your action?
<template>
<div>
{{ MJERILO }}
</div>
</template>
import { mapGetters, mapActions } from "vuex";
export default {
...mapGetters(["MJERILO"]),
}

the state is undefined in components although I use mapGetters to access it

I have a store and a component. In my component and in created() I do an API fetch so my state changes and also in computed I use mapGetters so I could have access to my state. But when I do this nothing renders on the screen. I even use v-if directive to see if the value is undefined or not.
Also I should mention that in Vue devtools I can clearly see that the state updates correctly and I can see that the data is being fetch from API.
I already searched so much on the web and tried whatever I could find. Just one solution worked and it was to also have data inside my component but this solution is not a good solution due to duplication of the data (one local to component and one on the store).
So here is my code:
movies.js (store)
import axios from 'axios';
const state = {
heading: '',
movies: [],
};
const getters = {
getMovies: (state) => state.movies,
getHeading: (state) => state.heading,
};
const actions = {
async fetchTopRatedMovies({ commit }) {
const res = await axios.get(
`https://api.themoviedb.org/3/movie/top_rated?api_key=${process.env.VUE_APP_THEMOVIEDB_API_KEY}&language=en-US&page=1`
);
commit('setMovies', res.data.results);
commit('setHeading', 'Top Rated Movies');
},
};
const mutations = {
setMovies: (state, movies) => (state.movies = movies),
setHeading: (state, heading) => (state.heading = heading),
};
export default {
state,
getters,
actions,
mutations,
};
Movies.vue (component)
<template>
<div v-if="!heading || !movies">
<Spinner />
</div>
<div v-else>
<h5 class="center">{{ heading }}</h5>
<div v-for="movie in movies" :key="movie.id">
<p>{{ movie.title }}</p>
</div>
</div>
</template>
<script>
import Spinner from '../layout/Spinner';
import { mapActions, mapGetters } from 'vuex';
export default {
name: 'Movies',
components: {
Spinner,
},
methods: {
...mapActions(['fetchTopRatedMovies']),
},
computed: {
...mapGetters(['getMovies', 'getHeading']),
},
created() {
this.fetchTopRatedMovies();
},
};
</script>
<style></style>
One way to solve it is to change computed to:
computed: {
...mapGetters({
heading: 'getHeading',
movies: 'getMovies',
}),
},
but I wonder why this has to be like this. Because in the documentation it is written as I did in the code above.
computed: {
...mapGetters({
heading: 'getHeading',
movies: 'getMovies',
}),
},
The above code is working and the reason is, in your condition you are using v-if="!heading || !movies" as well as you are assigning values to variables headings and movies in the computed properties.
But when you use code like below mentioned, as the varaibles you are using in your condition, are not declared in your app so it is not working.
If you want to write the code like below, you can directly use these getter properties getMovies and getHeading in your app (like you use data properties). So in this case in your condition if you write like v-if="!getHeading|| !getMovies" then it will work.
computed: {
...mapGetters(['getMovies', 'getHeading']),
},

How to wait on Vuex state initialization from a view component?

I'm building a Movie website to practice on VueJS. During app initialization, I get a list of movie genres from 3rd-party API. Since this list is needed in several components of the app, I manage and store it via Vuex, like so:
main.js:
new Vue({
router,
store,
vuetify,
render: h => h(App),
created () {
this.$store.dispatch('getGenreList')
}
}).$mount('#app')
Vuex's index.js:
export default new Vuex.Store({
state: {
genres: []
},
mutations: {
setGenreList (state, payload) {
state.genres = payload
}
},
actions: {
async getGenreList ({ commit }) {
try {
const response = await api.getGenreList() // axios call defined in api.js
commit('setGenreList', response)
} catch (error) {
console.log(error)
}
}
}
})
Now, in my Home view, I want to retrieve a list of movies for each genres, something like this:
Home.vue:
<script>
import { mapState } from 'vuex'
import api from '../api/api'
export default {
name: 'home',
data () {
return {
movies: null
}
},
computed: {
...mapState({
sections: state => state.genres
})
},
async mounted () {
const moviesArray = await Promise.all(
this.sections.map(section => {
return api.getMoviesByGenre(section.id)
})
)
this.movies = moviesArray
}
}
</script>
The issue here is that, on initial load, sections===[] since genres list hasn't been loaded yet. If I navigate to another view and come back, sections holds an array of genres objects as expected.
Question: How can I properly wait on sections to be loaded with genres? (since the getGenreList action isn't called from that component, I can't use this method)
I was thinking in implementing the movie list retrieval in a Watcher on sections instead of in mounted() but not sure if it's the right approach.
Yep, it is right approach, that's what watchers are for.
But if you only can... try to do actions like this one inside one component family. (parent passing props to children, controlling it);
You can read this article, about vuex - https://markus.oberlehner.net/blog/should-i-store-this-data-in-vuex/.
It will maybe clarify this idea. Just simply don't store in vuex everything, cause sometimes it' does not make sense
https://v2.vuejs.org/v2/api/#watch - for this one preferably you should use immedaite flag on watcher and delete mounted. Watcher with immedaite flag is kinda, watcher + created at once

Categories