Vuex how to Access state data on component when it's mounted? - javascript

I'm trying to create a Quill.js editor instance once component is loaded using mounted() hook. However, I need to set the Quill's content using Quill.setContents() on the same mounted() hook with the data I received from vuex.store.state .
My trouble here is that the component returns empty value for the state data whenever I try to access it, irrespective of being on mounted() or created() hooks. I have tried with getters and computed properties too. Nothing seems to work.
I have included my entry.js file, concatenated all the components to make things simpler for you to help me.
Vue.component('test', {
template:
`
<div>
<ul>
<li v-for="note in this.$store.state.notes">
{{ note.title }}
</li>
</ul>
{{ localnote }}
<div id="testDiv"></div>
</div>
`,
props: ['localnote'],
data() {
return {
localScopeNote: this.localnote,
}
},
created() {
this.$store.dispatch('fetchNotes')
},
mounted() {
// Dispatch action from store
var quill = new Quill('#testDiv', {
theme: 'snow'
});
// quill.setContents(JSON.parse(this.localnote.body));
},
methods: {
setLocalCurrentNote(note) {
console.log(note.title)
return this.note = note;
}
}
});
const store = new Vuex.Store({
state: {
message: "",
notes: [],
currentNote: {}
},
mutations: {
setNotes(state,data) {
state.notes = data;
// state.currentNote = state.notes[1];
},
setCurrentNote(state,note) {
state.currentNote = note;
}
},
actions: {
fetchNotes(context) {
axios.get('http://localhost/centaur/public/api/notes?notebook_id=1')
.then( function(res) {
context.commit('setNotes', res.data);
context.commit('setCurrentNote', res.data[0]);
});
}
},
getters: {
getCurrentNote(state) {
return state.currentNote;
}
}
});
const app = new Vue({
store
}).$mount('#app');
And here is the index.html file where I'm rendering the component:
<div id="app">
<h1>Test</h1>
<test :localnote="$store.state.currentNote"></test>
</div>
Btw, I have tried the props option as last resort. However, it didn't help me in anyway. Sorry if this question is too long. Thank you for taking your time to read this. Have a nice day ;)

I will recommend the following steps to debug the above code:
In the Vue dev tools, check if the states are getting set after the network call
As you are trying to fetch data asynchronously, there can be a chance that data has not arrived when created/mounted hook is called.
Add an updated hook into your component and try to log or access the state and you should be able to see it.
Please provide the results from the above debugging, then I'll be able to add more details.

Related

How to resolve Cannot read properties of undefined error?

I have a this warning:
[Vue warn]: Error in render: "TypeError: Cannot read properties of undefined (reading 'nestedArray')"
What is the solution to this? This is my beforeCreate functions:
beforeCreate() {
this.$store.dispatch("loadCities").then((response) => {
this.cities = response;
this.sortingCities=this.cities.slice(0).sort(function(a,b) {
return a.row - b.row || a.col-b.col;
})
this.sortingCities.map(item => {
if (!this.nestedArray[item.row]) {
this.nestedArray[item.row] = [];
}
this.nestedArray[item.row][item.col] = item;
});
});
My data property:
data() {
return {
cities: [],
selectedCity: null,
sortingCities:[],
nestedArray:[],
};
},
I use this property:
<img :src="require(`../images/${this.nestedArray?.[row]?.[col].imageId}.png`)" alt="">
Inside beforeCreate you should not expect any data, methods or computed to be available. This is documented here (vue 3) and here (vue 2).
Move the contents of beforeCreated into mounted.
If you have anything in <template> depending on the fetched data, give it an appropriate v-if (e.g: v-if="nestedArray.length").
Root cause of the problem : Issue is in accessing the data object properties inside beforeCreate life cycle hook. This life cycle hook called immediately after the instance has been initialized, before processing data option.
Solution : You can put your logic inside mounted() hook instead of beforeCreate as it is called after the instance has been mounted.
Live Demo :
new Vue({
el: '#app',
data() {
return {
message: []
}
},
beforeCreate() {
this.message.push('beforeCreate hook called!'); // ❌
},
mounted() {
this.message.push('mounted hook called!'); // ✅
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<pre>{{ message }}</pre>
</div>
Accessing the vue data without vue is created? Strange, isn't it? Instead, you can access the data using created and after that lifeCycles. Here is a small sample for you.
new Vue({
el: "#app",
data() {
return {
nestedArrays: []
}
},
created() {
console.log(this.nestedArrays)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Nuxt: destroy and recreate current page's component without refresh

I have a bunch of Vue components and I'm using Nuxt as the routing layer. My global layout is pretty standard:
<template>
<nav>
<nuxt-link to="/foo">foo</nuxt-link> | <nuxt-link to="/bar">bar</nuxt-link> | etc.
</nav>
<main>
<nuxt />
</main>
</template>
In each page, I update the query string when data in the vuex store changes. If the page is loaded server-side, I parse the query string to pre-load the necessary data into the store:
<template>
<h1>foo</h1>
<!-- forms and stuff -->
</template>
<script>
export default {
data() {
return {
// static data
}
},
computed: {
fooStoreParams() {
return this.$store.state.foo.params
}
},
watch: {
fooStoreParams: async function() {
await this.$nextTick()
let query = {}
if (this.fooStoreParams.page > 1) {
query.page = this.fooStoreParams.page
}
this.$router.push({ path: this.$route.path, query: query })
}
},
async asyncData({ store, query }) {
let params = {}
let data = {
form: {
page: 1
}
}
if (query.page) {
data.form.page = query.page
params.page = query.page
}
store.dispatch('foo/updateParams', params)
await store.dispatch('foo/getStuffFromAPI')
return data
}
}
</script>
This works well, but there's a feature that I'm missing.
If I'm on already on /foo?page=2&a=1&b=2 and I click on the /foo link in the main navigation, nothing happens. This makes sense considering how Nuxt/vue-router works, but what I want to happen is for the page component to be reloaded from scratch (as if you had navigated from /bar to /foo).
The only ways I can think to do this are to either 1) do a server-side request (e.g. <b-link href="/foo">) if I'm already on /foo?whatever or 2) write a resetPage() method for each individual page.
Is there a way to just tell Nuxt to destroy and recreate the current page component?
You need to use watchQuery in order to enable client-navigation for query-params:
watchQuery: ['page', 'a', 'b']
https://nuxtjs.org/api/pages-watchquery/
If you have a component e.g
<titlebar :key="somekey" />
and the value of somekey changes the component re-renders. You could maybe work around this to achieve what you want. Read more here: https://michaelnthiessen.com/force-re-render/

Vue component doesn't update when state in store is updated

I'm trying to do a simple todo list in Vue but I want to abstract everything out and use a dummy REST API so I can start to get used to production-level projects in Vue and it's all making my head spin. GET, PUT, and POST requests seem to be working, but I can't figure out why the list of todos doesn't update automatically when I do a successful POST request to the back end.
I've got a TodoList component that loops through a todosFiltered() computed property to show the todos. The computed property refers back to the getter todosFiltered in the Vuex store. I also use the created() lifecycle hook here to dispatch an action in the store that makes the initial GET request and then populates an array called todos in the store when the page is first loaded. The getter todosFiltered in the store returns state.todos, so I assumed that when my component re-renders, it would have the new todos array from the state grabbed from todosFiltered, only that's not happening. What am I missing here? Any advice would be greatly appreciated.
TodoList.vue
(I know I'll have to work out a solution for the ids, it's on my list :p)
<template>
<div class="container">
<input v-model="newTodo" type="text" placeholder="What must be done?" class="todo-input" #keyup.enter="addTodo">
<transition-group name="fade" enter-active-class="animated zoomIn" leave-active-class="animated zoomOut">
<todo-item v-for="todo in todosFiltered" :key="todo.id" :checkAll="!anyRemaining" :todo="todo"></todo-item>
</transition-group>
<div class="extra-container">
<todos-filtered></todos-filtered>
</div>
</div>
</template>
<script>
import TodosFiltered from './TodosFiltered'
import TodoItem from './TodoItem'
export default {
name: 'todolist',
components: {
TodosFiltered,
TodoItem
},
data() {
return {
beforeEditCache: '',
newTodo: '',
idForTodo: 10,
}
},
// Methods
methods: {
addTodo() {
if (this.newTodo.trim().length == 0) {
return
}
this.$store.dispatch('addTodo', {
id: this.idForTodo,
title: this.newTodo,
completed: false
})
this.newTodo = ''
this.idForTodo++
}
},
computed: {
todosFiltered() {
return this.$store.getters.todosFiltered
},
},
created() {
this.$store.dispatch('loadTodos')
},
}
</script>
store.js
export const store = new Vuex.Store({
state: {
filter: 'all',
todos: []
},
getters: {
todosFiltered(state) {
if (state.filter == 'all') {
return state.todos
} else if (state.filter == 'active') {
return state.todos.filter(todo => !todo.completed)
} else if (state.filter == 'completed') {
return state.todos.filter(todo => todo.completed)
}
return state.todos
},
showClearCompleted(state) {
return state.todos.filter(todo => todo.completed).length > 0
}
},
mutations: {
addTodo(state, todo) {
state.todos.push(todo)
},
setTodos(state, todos) {
state.todos = todos
},
},
actions: {
loadTodos(context) {
axios.get('http://localhost:3000/todos')
.then(r => r.data)
.then(todos => {
context.commit('setTodos', todos)
})
},
updateTodo(context, todo) {
axios.put('http://localhost:3000/todos/' + todo.id, {
"id": todo.id,
"title": todo.title,
"completed": todo.completed
})
},
addTodo(context, todo) {
axios.post('http://localhost:3000/todos', {
"id": todo.id,
"title": todo.title,
"completed": todo.completed
})
.then(todo => {
context.commit('addTodo', todo)
})
},
}
})
EDIT: Here's what's going on in Vue Dev Tools when I add a todo -- todos in the store's state gets updated immediately, and the todosFiltered computed property in the TodoList component ALSO reflects that -- but the new todo doesn't appear in the list! Strange.
A way to solve this can be to create what I like to call a refresh() method.
Basically, you will have a local list of todos in your data() method, the refresh() method will load all the todos from the store into the local todos list, every time you do an action, such as creating, deleting, or updating, you would call the refresh method to re-load the list for you.
So, in your TodoList.vue:
<template>
<todo-item v-for="todo in todosFiltered" :key="todo.id"></todo-item>
</template>
<script>
export default {
data() {
return {
// Where we store the local list of Todos
// so the component will react when we do something to it
todosFiltered: []
}
},
methods {
refresh() {
// Get the todo list from the store.
// which in turn will trigger a change event so the component
// react to what we did.
this.todosFiltered = this.$store.getters.todosFiltered;
},
addTodo() {
this.$store.dispatch('addTodo').then(() => {
// Refresh the list after adding a new Todo
this.refresh();
})
},
updateTodo() {
this.$store.dispatch('updateTodo').then(() => {
// Refresh the list after updating a Todo
this.refresh();
})
},
deleteTodo() {
this.$store.dispatch('deleteTodo').then(() => {
// Refresh the list after deleting a Todo
this.refresh();
})
}
},
created() {
this.$store.dispatch('loadTodos').then( () => {
// Refresh the list when first loaded after the Todos been saved in Vuex
this.refresh();
})
}
}
</script>
Don't actually delete what you already have and replace it with
this, just apply what's here to your code.
The problem is using a $store.getter on the v-for loop.
Try the following:
Set your computed to
todos() {
return this.$store.todos;
}
Change your v-for loop to use todo in todos
Add a v-if condition to the loop like v-if="filtered(todo)"
Create a new method called filtered (or whatever you prefer), and add your "filteredTodos" logic there, returning true/false as needed
If you need to share this code, you can always use a mixin and share it between your components
Hope this works for you

Error in render function: “TypeError: Cannot read property of undefined”, using Vuex and Vue Router

I have been dealing with an issue using Vue, Vuex and Vue-Router. I'm building a flash cards app, fetching all the cards on main app creation, then using a Vuex getter to get each card by its id which is passed as a route parameter.
Relevant bits:
App.vue
export default {
components: {
'app-header': header,
},
data() {
return {
}
},
created() {
this.$store.dispatch('getAllCards');
}
}
The dispatch('getAllCards') is just pulling all the cards from the DB and committing to Vuex store.js.
Now I set up a getter:
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
Here is Card.vue:
<template>
<div>
<br>
<div v-if="flipped" class="container">
<div class="box">
<pre v-if="card.code"><code class="preserve-ws">{{card.back}}</code></pre>
<p class="preserve-ws center-vertical" v-else>{{card.back}}</p>
</div>
</div>
<div v-else class="container">
<div class="box">
<h1 class="title has-text-centered center-vertical is-2">{{card.front}}</h1>
</div>
</div>
</template>
<script>
export default {
data() {
return {
card: {},
flipped: false,
general_card: false,
code_card: true,
random_card: false
}
},
computed: {
},
methods: {
},
created() {
this.card = this.$store.getters.cardById(this.$route.params.id);
}
}
</script>
I am getting the TypeError referenced in the title. My understanding is that the created() hook happens after data() has been set up, so then I can assign {card} using the getter. Unfortunately this displays nothing...
If I assign card() as a computed property:
computed: {
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
}
The card shows, but I still get that error in console. Any idea why? I looked at this and attempted that solution, but to no avail.
The question don't have all the premise to get a correct answer.
(We don't know the mutations, the state, neither the actions and we have no clues about app-header component)
So we have to admit some hypothesis :
state.allCards is an empty Array when you mount the component
action getAllCards is an async function to retrieve or set state.allCards with a mutation
cardById's getters, return a function, so vuex can't apply reactive on a function. So if the state change, the getter won't be trigger correctly.
To correct this, use computed as you mention here.
But it don't fix the undefined error, that from my point of view, is because the getters return undefined on mount.
getters: {
cardById: (state) => (id) => {
var card = state.allCards.find((card) => card._id === id);
if(card) {
return card;
}
// on undefined return a default value
return {
front:'default value'
};
}
}
You can see the implementation on this jsfiddle with no error on console.
Or you can have a loading state on undefined value for your card component.
If my hypothesis is wrong please provide a jsfiddle to help you.
What you need is a derived state based on store state which is to return a filtered card based on a card id. This id is received to your component via the route params. So its better you use a computed property instead of passing arguments to the store getters
Instead of initializing card in data property make card a computed property like this:
computed:{
card(){
return this.$store.state.allCards.find((card) => card._id === this.$route.params.id);
}
}
Note this
If a component needs derived store state based on its own state(in your case rourte params), it should define a local computed property
I tried everyone else's solutions and they did not work. But I got it to work finally. Here is what worked for me:
I ended up including a top-level:
<div v-if="!card"> Loading card... </div>
<div v-else> Rest of card template </div>
That seems to have silenced the error. Also, the card lives as a computed property:
card() {
return this.$store.getters.cardById(this.$route.params.id);
}
I think it was throw error in this step
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card._id === id);
}
}
in this step, it can not find _id of cars, because allcards was null;
and then you use computed instead, when allcards has been change, it will get again;
you can change code like this
getters: {
cardById: (state) => (id) => {
return state.allCards.find((card) => card && card._id === id);
}
}
$route.params.id must be string, so what is the type of card._id? It seems each card._id is number, I think.

Setting props for components using v-for in Vue JS

I have a PhoneCard.vue component that I'm trying to pass props to.
<template>
<div class='phone-number-card'>
<div class='number-card-header'>
<h4 class="number-card-header-text">{{ cardData.phone_number }}</h4>
<span class="number-card-subheader">
{{ cardData.username }}
</span>
</div>
</div>
</template>
<script>
export default {
props: ['userData'],
components: {
},
data() {
return {
cardData: {}
}
},
methods: {
setCardData() {
this.cardData = this.userData;
console.log(this.cardData);
}
},
watch: {
userData() {
this.setCardData();
}
}
}
The component receives a property of userData, which is then being set to the cardData property of the component.
I have another Vue.js component that I'm using as a page. On this page I'm making an AJAX call to an api to get a list of numbers and users.
import PhoneCard from './../../global/PhoneCard.vue';
export default {
components: {
'phone-card': PhoneCard
},
data() {
return {
phoneNumbers: [],
}
},
methods: {
fetchActiveNumbers() {
console.log('fetch active num');
axios.get('/api').then(res => {
this.phoneNumbers = res.data;
}).catch(err => {
console.log(err.response.data);
})
}
},
mounted() {
this.fetchActiveNumbers();
}
}
Then once I've set the response data from the ajax call equal to the phoneNumbers property.
After this comes the issue, I try to iterate through each number in the phoneNumber array and bind the value for the current number being iterated through to the Card's component, like so:
<phone-card v-for="number in phoneNumbers" :user-data="number"></phone-card>
However this leads to errors in dev tools such as property username is undefined, error rendering component, cannot read property split of undefined.
I've tried other ways to do this but they all seem to cause the same error. any ideas on how to properly bind props of a component to the current iteration object of a vue-for loop?
Try
export default {
props: ['userData'],
data() {
return {
cardData: this.userData
}
}
}
Answered my own question, after some tinkering.
instead of calling a function to set the data in the watch function, all I had to do was this to get it working.
mounted() {
this.cardData = this.userData;
}
weird, I've used the watch method to listen for changes to the props of components before and it's worked flawlessly but I guess there's something different going on here. Any insight on what's different or why it works like this would be cool!

Categories

Resources