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

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

Related

How to remove an element from a Mobx observable array, without causing the entire consuming component to rerender?

So let's say i have a todoStore. It has an action that deletes a todo by id. Note that i tried both filter and splice:
export default class TodosStore {
constructor() {
makeAutoObservable(this)
}
todos = [
{
id: 1,
name: "name1",
completed: true
},
{
id: 15,
name: "name2",
completed: true
},
{
id: 14,
name: "name3",
completed: true
}
]
removeTodo(id) {
// this.todos = this.todos.filter(todo=>todo.id != id)
for (let todo of this.todos) {
if (todo.id == id) {
const indexOf = this.todos.indexOf(todo)
this.todos.splice(indexOf, 1)
}
}
}
};
The consuming Todos component(Note that i'm wrapping the Todo with observer):
import { combinedStores } from "."
const ObservableTodo = observer(Todo);
export default observer(() => {
const { todosStore } = combinedStores
return (
<div >
{todosStore.todos.map(todo=>{
return(
<ObservableTodo onDelete={()=>{todosStore.removeTodo(todo.id)}} onNameChange={(value)=>{todosStore.editTodoName(todo.id,value)}} key={todo.id} todo={todo}></ObservableTodo>
)
})}
</div>
)
})
The simple Todo component:
export default ({todo,onNameChange,onDelete}) => {
return (
<div style={{padding:'10px',margin:'10px'}}>
<p>ID: {todo.id}</p>
<input onChange={(e)=>{onNameChange(e.target.value)}} value={todo.name}></input>
<p>Completed: {todo.completed ? 'true' : 'false'} <button onClick={onDelete} className="btn btn-danger">Delete</button></p>
</div>
)
}
Even though i'm clearly mutating(as opposed to constructing a new array) the todos array within the store, Todos component rerenders(i see it via console.logs),
and so does every remaining Todo component.
Is there any way around it? Is there anything wrong with my setup perhaps? I'm using latest Mobx(6) and mobx-react.
Todos component is supposed to rerender because it depends on todos array content (because it map's over it). So when you change todos content by adding or removing some todo - Todos component will rerender because it needs to render new content, new list of todos.
Each single Todo rerenders because you have not wrapped it with observer. It is a good practice to wrap every component which uses some observable state, and Todo is clearly the one that does.
You change the length of the todo array, so the map function kicks in. Then while you are iterating over the elements, you are passing new properties to the ObservableTodo component (onDelete, onChange) this will make the ObservableTodo always rerender.
Even though the component is a Mobx observable, it still follows the "React rules", and when react sees new references in component properties, it will render the component.

How to instantaneously update changes committed to vuex store in the ui of another component?

How to instantaneously update changes committed to vuex store in the ui of another component?
following is the vuex store module of a component
const { property } = require("lodash")
export default {
state: {
data: [
{id: 1, name: 'Angular', status: 'active'},
{id: 2, name: 'React', status: 'active'},
{id: 3, name: 'Vue', status: 'inactive'},
]
},
mutations: {
UPDATE_ITEM(state, payload) {
const item = state.data.find(item => _.isEqual(item, payload.item))
Object.assign(item, payload.status);
},
},
actions: {
updateItem({ commit }, payload) {
commit("UPDATE_ITEM", payload);
},
},
getters: {
getData: (state) => state.data,
getMappedStatus: (state) => state.data.map(data => data.status)
},
};
this is how I get mapped status in component 1
computed: {
getMappedStatus() {
return this.$store.geters.getMappedStatus
}
}
and inside the ui of component 1
<ul>
<li v-for="item in getMappedStatus>{{item}} </li>
</ul>
from another component 2 I'm updating the changes as the user inputs the status:
onStatusChanges(item, status) {
this.$store.dispatch("updateItem", {
item, status
});
},
But the problem is that even though the state is getting updated the ui is not getting updated.But
(inside component 1)
I think it is because of the computed property. ... not sure.
How can I implement something like and observable or something reactive so that the getMappedStatus computed property in component 1 will get updated automatically as action is dispatched from component 2.
NOTE Both components come under the same vuex store module
If this was in angular/ngrx I would subscribe to the selectors inside the component 1 and I would get instantaneously the changes. even If I subscribe to the event inside the onInit() method or constructor() and do a console.log() the changes from the other component will be reflected instantaneously.
But this is not happening with vuex.
How can I achieve that:?
or is there a way to trigger the updation of the getMappedStatus computed property inside component 1 as soon as changes occurs from component 2:
computed: {
getMappedStatus() {
return this.$store.geters.getMappedStatus
}
}
so that the ui of component 1 updates instantaneously.
We can think of getters as computed properties for stores. Like computed properties, a getter's result is cached based on its dependencies, and will only re-evaluate when some of its dependencies have changed so that simply means if component1 mutates the state in vuex store and component2 uses that property from the store it will automatically update in all components using it.
Online IDE - Live Demo Here
Vuex Store
const store = new Vuex.Store({
state: {
name: 'mex'
},
mutations: {
mutateName(state, value) {
state.name = value;
}
},
actions: {
updateName(context, payload) {
context.commit('mutateName', payload.name);
}
},
getters: {
getName(state) {
return state.name;
}
}
});
The changeName method when fired will dispatch updateName action which will update the name and all the components will update accordingly because it's reactive.
Then In Any Component
computed:{
name() {
return this.$store.getters.getName
}
},
methods: {
changeName: function () {
this.$store.dispatch('updateName', { name: 'mexxxxx'});
}
}
You need to return something from getMappedStatus
computed: {
getMappedStatus() {
return this.$store.geters.getMappedStatus
}
}
...without a return statement the this.$store.geters.getMappedStatus is just an expression and your computed property always returns undefined (because JS functions without return always return undefined)

Vuetify 2 skeleton-loader with manipulating "loading" in Vuex store

I am using the Vuetify v-skeleton-loader component wrapping a v-data-table component.
The data-table component is using server-side pagination and sorting.
To accomplish the server-side pagination, documentation suggests to watch the options object of the data-table and make a new API call with the changed options.
In my store module I set state.loading = true before the API request starts and state.loading = false right after the response is committed to the state.
On the data-table component I read that state for the :loading prop.
On its own this works fine.
When I now wrap the data-table with v-skeleton-loader and put the prop :loading="loading" I get an infinite loop of API requests and rendering of the skeleton-loader.
My hunch is that after the request got resolved and state.loading = false is set, the skeleton-loader unmounts and data-table mounts, changing the options which are being watched and a new API request is fired which sets state.loading = true and the skeleton-loader is being mounted again while data-table is unmounted. Which continues infinetely.
If that is correct, how do I resolve that issue?
Minimal example:
<template>
<div>
<v-card>
<v-skeleton-loader
:loading="loading"
transition="scale-transition"
height="500"
type="table"
>
<v-data-table
:headers="headers"
:items="orders"
:fixed-header="true"
:server-items-length="totalItems"
:options.sync="options"
>
</v-data-table>
</v-skeleton-loader>
</v-card>
</div>
</template>
<script>
...
export default {
data() {
return {
options: {},
...
}
},
watch: {
options: {
handler() {
this.getDataFromApi();
},
deep: true,
},
},
methods: {
getDataFromApi() {
// build query from options
this.$store.dispatch("orders/getOrdersCustom", query);
},
computed: {
...mapGetters("orders", ["orders"]),
...mapGetters("orders", ["loading"]),
},
mounted() {
this.$store.dispatch("orders/getOrders");
}
}
</script>
And the store module:
const namespaced = true;
const state = {
orders: [],
loading: false,
};
const getters = {
loading: (state) => state.loading,
orders: (state) => state.orders.items,
};
const actions = {
getOrdersCustom({ commit }, query) {
commit("GET_ORDERS_REQUEST");
return orderService.getOrdersCustom(query).then(
(data) => commit("GET_ORDERS_SUCCESS", data),
(error) => commit("GET_ORDERS_FAILURE", error)
);
},
}
const mutations = {
GET_ORDERS_REQUEST(state) {
state.loading = true;
},
GET_ORDERS_SUCCESS(state, data) {
Vue.set(state.orders, "items", data["hydra:member"]);
state.data = data;
state.loading = false;
},
}
I think what I would do in this situation is have a separate variable in the local component state to track the initial loading state, and display the skeleton loader based on that.
Once the initial data set is loaded, I would use the data table component's built-in loading indicator for paginating/sorting the data.. it makes less sense to revert to the skeleton view, as you already have something rendered for the user to look at.

reload vue component from outside

I have a laravel vue project that displays a list of items in a component. The component when mounted makes an ajax call to populate the data element. However, there's other items on the page (not in vue) that can add elements to the database, and I'd like to make sure the list is reactive in the component.
mounted() {
this.getTasks();
},
methods: {
getTasks() {
let self = this;
axios.get('/tasks').then(response => {
self.tasks = response.data;
})
.catch(function (error) {
console.log(error);
});
},
}
When the user does an action that would add a task to the list, is there a way to fire the getTasks method on the component from outside the component?
You can declare a global function (binding context to Vue component) when component is mounted:
mounted() {
windows.getTasks = this.getTasks.bind(this);
this.getTasks();
},
Then you can use it outside calling getTasks() or windows.getTasks()
You should use vuex actions.
Here's an example:
Vue.component('ChildB',{
template:`
<div class="child childB">
<h1> Score: {{ score }} </h1>
<button #click="changeScore">Change Score</button>
</div>`,
computed: {
score () {
return this.$store.getters.score
}
},
methods: {
changeScore () {
this.$store.dispatch('incrementScore', 3000)
}
}
})
full source:
https://codepen.io/tutsplus/pen/MXpPLz

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