Problem with passing data from Vuex getters to child component - vue.js

I am creating movie-app with Vue and Vuex which will display all movies in Home component and on click single movie in Details (child) component. I get all movies in Home component but I can not display single movie in Details component.
I have enabled props in child component but I have problem with passing movie ID to method imported from getters (in Details component) which should filter selected movie from movies list.
Home component
<template>
<div class="home">
<router-view />
<h1>Home component:</h1>
<div v-for="movie in movies"
:key="movie.id">
<div>
<router-link :to="'/movie/' + movie.id">{{ movie.title }}</router-link>
</div>
</div>
</div><!--home-->
</template>
<script>
import axios from 'axios'
import { mapGetters, mapActions } from 'vuex'
export default {
name: "home",
methods: {
...mapActions(['fetchMovies']),
},
computed: {
...mapGetters(['movies'])
},
created() {
this.fetchMovies()
}
};
</script>
Details component
<template>
<div>
<h1>Movie details - child component</h1>
<h2>Title: {{ movie.title }}</h2>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: ['id'],
computed: {
movie() {
return this.$store.getters.singleMovie(this.id)
}
}
}
</script>
Vuex store
const state = {
movies: []
};
const getters = {
movies: state => state.movies,
singleMovie: state => movieId => {
return state.movies.find(item => item.id === movieId);
}
};
const actions = {
async fetchMovies({ commit }) {
const response = await axios.get(
movie_url + "movie/popular" + "?api_key=" + api_key
);
commit("setMovies", response.data.results);
}
};
const mutations = {
setMovies: (state, items) => (state.movies = items)
};
export default {
state,
getters,
actions,
mutations
};
I tried to replace {{movie.title}} with {{id}} and then I get displayed movie ID as I click on listed movies in Home component. I also tried to hard code movie id as parameter: return this.$store.getters.singleMovie(299536) and I successfully get title displayed but in console I get error "TypeError: Cannot read property 'title' of undefined". Obviously I am making mistake with passing delivered movie ID.

Your computed property movie is not loaded in every vue life cycle. So, in the first time you get movie, it is undefined and your mustache {{ movie.title }} is actually something like {{ 'undefined'.title }}, which causes the error.
To solve it, you can both add a v-if="movie" conditional in template or return a default value in computed property movie. Like this, for example: return state.movies.find(item => item.id === movieId) || {};

Related

Vue3 best way to fetch data based on asynchronous prop passed by parent

In Vue 3, my component "App" makes an asynchronous API request to retrieve info about a "purchase". This "purchase" is passed on to its child component "DeliveryInfo".
In "DeliveryInfo", I need to make another request based on the "customerId" property contained in the "purchase" prop. However, when "DeliveryInfo" receives the "purchase" prop, it's value is at first undefined. The second API request would then fail.
To avoid this, I used a watcher, so that when "DeliveryInfo" eventually gets the content of the "purchase" prop, it would then make a call to the API and update its own data.
I heard this is not good for performance. Could someone help me to improve my code ?
Here's my App.vue component :
<template>
<Purchase :purchase="purchase" />
</template>
<script>
import Purchase from "./components/Purchase"
export default {
name: "App",
components: { Purchase },
data() {
return {
purchase: []
};
},
methods: {
async fetchPurchase(id) {
const response = await fetch(`myapi.com/purchases/${id}`);
const data = await response.json();
return data;
}
},
async created() {
// example with a specific id
this.purchase = await this.fetchPurchase(79886);
}
}
</script>
And my DeliveryInfo component :
<template>
<div class="delivery-info embossed">
<div>
<h4>Adress</h4>
<p>{{ purchase.deliveryAdress }}</p>
<p>{{ purchase.deliveryCity }}</p>
</div>
<div>
<h4>Customer info</h4>
<p>{{ customerData.firstname }} {{customerData.lastname}}</p>
<p>{{ customerData.phone }}</p>
</div>
</div>
</template>
<script>
export default {
name: "DeliveryInfo",
props: ["purchase"],
data(){
return{
customerData: {}
}
},
methods:{
async fetchCustomer(){
if(!this.purchase){
return
}
const response = await fetch(`myapi/customers/${this.purchase.customerId}`)
this.customerData = await response.json()
}
},
watch : {
purchase(){
this.fetchCustomer()
}
}
}
</script>

How can I pass a variable value from a "page" to "layout" in Nuxt JS?

I'm a beginner in VUE and donnow this one is the correct syntax. I need the variable {{name}} to be set from a page. Which means I need to change the value of the variable page to page. How can I achieve that? Help me guys.
My "Layout" Code is like below -
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
And my "Page" code is following -
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
data: function() {
return {
name: 'Victor'
}
}
}
</script>
this can be achieved by using the vuex module. The layout have access to the vuex store, so once a page is open, you can call a mutation to set the page name and listen the name state in the layout component.
First the Vuex module, we can add a module by creating a file in the store folder,
in this case we are creating the page module:
// page.js file in the store folder
const state = {
name: ''
}
const mutations = {
setName(state, name) {
state.name = name
}
}
const getters = {
getName: (state) => state.name
}
export default {
state,
mutations,
getters
}
Now we can use the setPageName mutation to set the pageName value once a page reach the created hook (also can be the mounted hook):
// Page.vue page
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
created() {
this.$store.commit('page/setName', 'Hello')
},
}
</script>
And in the layout component we have the computed property pageName (or name if we want):
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
name() {
return this.$store.getters['page/getName']
}
}
}
</script>
And it's done!
Answer to your question in the commets:
The idea behind modules is keep the related information to some functionality in one place. I.e Let's say you want to have name, title and subtitle for each page, so the page module state variable will be:
const state = { name: '', title: '', subtitle: ''}
Each variable can be updated with a mutation, declaring:
const mutations = {
setName(state, name) {
state.name = name
},
setPageTitle(state, title) {
state.title = title
},
setPageSubtitle(state, subtitle) {
state.subtitle = subtitle
},
}
And their values can be updated from any page with:
this.$store.commit('page/setPageTitle', 'A page title')
The same if you want to read the value:
computed: {
title() {
// you can get the variable state without a getter
// ['page'] is the module name, nuxt create the module name
// using the file name page.js
return this.$store.state['page'].title
}
}
The getters are good for format or filter information.
A new module can be added anytime if required, the idea behind vuex and the modules is to have a place with the information that is required in many places through the application, in one place. I.e. the application theme information, if the user select the light or dark theme, maybe the colors can be changed. You can read more about vuex with nuxt here: https://nuxtjs.org/guide/vuex-store/ and https://vuex.vuejs.org/

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 / Vuex : Rendering list of items

I'm trying to render a list of offers from my vuex store. The problem is when i'm loading my list of offers page, they are not rendered.
Here is my code :
offers.js
export const namespaced = true
export const state = {}
export const mutations = {
[types.UPDATE_OFFERS] (state, offers) {
Object.assign(state, offers)
}
}
export const actions = {
async fetchOffers ({ commit }) {
const { data } = await axios.get('/api/offers')
commit(types.UPDATE_OFFERS, data)
}
}
offers.vue (my page component)
<template>
<div>
<div v-if="offers"
v-for="offer in offers"
:key="offer.id">
<div>
<div>
<router-link :to="{ name: 'offer', params: { offer: offer.id } }">
{{ offer.project }}
</router-link>
</div>
<div>
<div v-if="offer.versions[0] && offer.versions[0].status == 'edition'">
Validate the offer
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
...mapState(['offers'])
},
beforeMount () {
this.$store.dispatch('offers/fetchOffers')
}
}
</script>
When i'm loading the page, I can see that the store as loaded the offers but the page doesnt render them. The weird thing is when I load the page offers then another page (example createAnOffer) and then I come back to offers, it renders the offers proplery.
I tried beforemount, beforeCreate, mounted, created.
I admit I have no clue what's going on.
Any tip ?
Thank you for your answers.
Louis

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