Nuxtjs/Vuejs/Laravel 5.7 API : Component reactive after action/mutation - vue.js

Maybe you can help me. I searched and tryied lot of things.
I have a Nuxtjs project with Laravel 5.7 API (JWT Auth for the auth).
A user can CRUD a post. Everything works or almost. When the current user create a post, after he is redirected on the page where his posts are indexed. But sometime the new post created appear and sometimes not, I have no error in console logs and server-side the created post is present in database. When I refresh the page, the new post renders. It's very random. Same thing for the delete action.
I use NuxtJs with Vuex for store. I call the action in my component method with dispatch. The action call axios put or delete method and I commit a mutation to update the state.posts array. after dispatch I refetch the user with auth module of NuxtJS to reload the user's posts. and push the route.
Just below the implementation of the delete action to show you my logic.
My component method :
deletePost(post) {
this.$toast.show('Do you really want to remove this post ?', {
duration: 5000,
icon: 'check',
action: [
{
text: 'No',
onClick: (e, toastObject) => {
toastObject.goAway(0)
}
},
{
text: 'Yes',
onClick: (e, toastObject) => {
this.$store.dispatch('posts/deletePost', post).then(() => {
console.log('here')
this.$auth.fetchUser()
this.$router.push({ path: '/:account/posts' })
})
this.$toast.show('Post successfully removed', {
icon: 'check'
})
toastObject.goAway(0)
}
}
]
})
},
The store action :
deletePost({ commit }, post) {
this.$axios
.delete(`/posts/${post.id}`)
.then(res => {
console.log(res.data)
if (res.data.message === 'success') commit('DELETE_POST', post)
})
.catch(err => {
console.log(err)
})
}
The store mutation :
DELETE_POST(state, post) {
const index = state.posts.findIndex(item => item.id === post.id)
state.posts.splice(index, 1)
}

When your are adding or deleting properties of a reactive object, you have to make sure the view updates accordingly. So I think you have to use Vue.delete and Vue.set properties. Please refer https://v2.vuejs.org/v2/api/#Vue-delete and https://v2.vuejs.org/v2/api/#Vue-set
When you are deleting a post from a posts list, your posts list view should be updated accordingly
Edit your DELETE_POST mutation as follows,
DELETE_POST(state, post) {
const index = state.posts.findIndex(item => item.id === post.id)
if (index > -1) {
Vue.delete(state.posts, index);
}
}
Also when you are updating a property of a post do your mutation as follows,
UPDATE_POST(state, updatedPost) {
let index = state.posts.findIndex(post => post.id === updatedPost.id);
if (index > -1) {
Vue.set(state.posts, index, updatedPost)
}
},

Related

Is way i can react to an asynchronous action in vuex in a vue template

I want to display a loading effect in a vue template whilst an asynchronous action in vuex is still running. But the loading effect doesn't seem to work. How do I fix it?. Is there any better way I can achieve this?
This is how I defined the action:
actions: {
signIn({ state }, user) {
auth()
.signInWithEmailAndPassword(userInfo.email, userInfo.password)
.then(result => {
return result
})
},
},
This how defined the dispatch in vue template method:
let loader = this.$loader.show()
this.$store.dispatch('signIn', this.user).then(() => {
loader.hide()
})
I expected the loader to start when the action begins and end when the action ends but it starts and ends almost instantly.
Just add return statement, that returns a Promise so you can then it in your component.
actions: {
signIn({ state }, user) {
return auth()
.signInWithEmailAndPassword(userInfo.email, userInfo.password)
.then(result => {
return result
})
},
},

Pusher Chatkit in Vue - onNewMessage hook triggered twice?

I have a Vue.js application using the Pusher Chatkit.
I have a problem I haven't been able to find an answer for some time now.
Whenever I re-visit a view/component the Chatkit onNewMessage() hook is triggered multiple times. It depends on the times I re-visit the page.
Page refresh or first-time load resolves the issue until next re-visit.
It must be that I am creating multiple listeners each time I visit the view, but I don't get what these listeners are.
Pusher states that room subscriptions "override" the old ones when done twice.
Here is my chat.vue component
import chatConnection from '../chatkit.js'
created(){
let chatManagerConnectPromise = chatConnection(this, uid)
Promise.all([..., chatManagerConnectPromise, ...])
.then(results => {
// ...
this.initiateNewChatState(results[1])
// ...
})
.catch(error =>{
});
},
methods: {
initiateNewChatState(currentUser){
this.subscribeToAllUserRooms(currentUser)
},
subscribeToAllUserRooms(currentUser){
for(let room of currentUser.rooms){
this.subscribeToRoom(currentUser, room.id)
}
},
subscribeToRoom(currentUser, roomId){
currentUser.subscribeToRoom({
roomId: roomId,
hooks: {
onNewMessage: message => {
console.log("CHAT | onNewMessage | new: ", message.text)
}
},
messageLimit: 10
})
.catch(error => {
this.notifyError("Uh oh", "Something is not right")
});
}
}
And here is my chatkit.js content:
import { ChatManager, TokenProvider } from '#pusher/chatkit'
export const chatConnection = ({ state, actions }, uid) =>{
return new ChatManager({
instanceLocator: "##:###:###:####:####",
userId: uid,
tokenProvider: new TokenProvider({url: 'https://...' })
})
.connect({
onAddedToRoom: room => {
// some action taken
},
onRemovedFromRoom: room => {
// some action taken
},
onRoomDeleted: room => {
// some action taken
}
})
.then(user => {
return user
})
.catch(error => console.log('CHATKIT | Error on connection', error))
}
Again, the problem is that the onNewMessage() is triggered once the first time after the page refresh/first load, but then increases by one with each new page visit (back and forth navigation).
I am creating some listeners with each visit but it cannot be the ChatManager not the User.subscribeToRoom!?
Thanks for any pointers.

Why is it considered poor practice to use Axios or HTTP calls in components?

In this article, it says:
While it’s generally poor practice, you can use Axios directly in your components to fetch data from a method, lifecycle hook, or whenever.
I am wondering why? I usually use lifecycle hooks a lot to fetch data (especially from created()). Where should we write the request calls?
Writing API methods directly in components increases code lines and make difficult to read.
As far as I believe the author is suggesting to separate API methods into a Service.
Let's take a case where you have to fetch top posts and operate on data. If you do that in component it is not re-usable, you have to duplicate it in other components where ever you want to use it.
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
})
.catch(e => {
this.errors.push(e)
})
}
}
So when you need to fetch top post in another component you have to duplicate the code.
Now let's put API methods in a Service.
api.js file
const fetchTopPosts = function() {
return axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
}) // you can also make a chain.
}
export default {
fetchTopPosts: fetchTopPosts
}
So you use the above API methods in any components you wish.
After this:
import API from 'path_to_api.js_file'
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
API.fetchTopPosts().then(top => {
this.top = top
})
.catch(e => {
this.errors.push(e)
})
}
}
It's fine for small apps or widgets, but in a real SPA, it's better to abstract away your API into its own module, and if you use vuex, to use actions to call that api module.
Your component should not be concerned with how and from where its data is coming. The component is responsible for UI, not AJAX.
import api from './api.js'
created() {
api.getUsers().then( users => {
this.users = users
})
}
// vs.
created() {
axios.get('/users').then({ data }=> {
this.users = data
})
}
In the above example, your "axios-free" code is not really much shorter, but imagine what you could potentially keep out of the component:
handling HTTP errors, e.g. retrying
pre-formatting data from the server so it fits your component
header configuration (content-type, access token ...)
creating FormData for POSTing e.g. image files
the list can get long. all of that doesn't belong into the component because it has nothing to do with the view. The view only needs the resulting data or error message.
It also means that you can test your components and api independently.

Calling two mutations from the same action

I'm using a Vuex store to keep all the items in a shopping cart.
There's two actions on the store :
getCartContent, which gets called on page load (fetches the initial content from the backend, which in turn retrieves the data from the session)
addToCart, which is dispatched by the <Products> component when the user clicks the add to cart button.
Both of these call a respective mutation (with the same name), since you're not supposed to call mutations directly from within components.
Here is what the store looks like :
const store = new Vuex.Store({
state: {
items: [],
},
mutations: {
getCartContent(state, data){
axios.get('/api/cart').then(response => {
state.items = response.data;
});
},
addToCart(state, data){
axios.post('/api/cart/add', {
item_id: data.item,
});
}
},
actions: {
getCartContent(context){
context.commit('getCartContent');
},
addToCart(context, data){
context.commit('addToCart', {item: data.item});
}
}
});
This is working as expected, but now when an item is added to the cart (with a dispatch to the addToCart action from within the component), I would like it to call the getCartContent mutation just after so that it fetches a fresh list of items from the backend.
I tried commiting the second mutation from the same action, like this :
actions: {
// ...
addToCart(context, data){
context.commit('addToCart', {item: data.item});
context.commit('getCartContent');
}
}
But that doesn't always work, sometimes it will fetch the items but not always.
I also tried dispatching the getCartContent action from within the component itself, right after dispatching the addToCart action, but it's the same problem.
How can I solve this?
Your axios calls are asynchronous, meaning that your addToCart mutation might not necessarily be finished when your getCartContent mutation fires. So, it's not surprising that sometimes getCartContent doesn't return the items you told axios to send a post request for immediately prior.
You should move asynchronous calls to the vuex actions:
actions: {
getCartContent(context, data) {
axios.get('/api/cart').then(response => {
state.items = response.data;
context.commit('getCartContent', response.data),
});
},
addToCart(context, data) {
axios.post('/api/cart/add', {
item_id: data.item,
}).then(() => {
context.commit('addToCart', data.item)
})
},
}
And your mutations should do nothing but make simple, straight-forward changes to the module state:
mutations: {
getCartContent(state, items) {
state.items = items;
},
addToCart(state, item) {
state.items.push(item);
}
}
The above explanation assumes that instead of making a get('/api/cart') request after each POST request, you would just keep track of items by pushing the data to the state.items property.
If however, you really want to make the GET request after adding an item, you can just get rid of the addToCart mutation and dispatch the getCartContent action after the POST request finishes:
addToCart(context, data) {
axios.post('/api/cart/add', {
item_id: data.item,
}).then(() => {
context.dispatch('getCartContent');
})
},

Click on link changes the url but not the content/data on page

the story:
I am on product page #/product/7 and on the same page I have 4 more products that are similar to the one that is being viewed. All these products have links to their pages:
router-link(:to="{ name: 'product', params: { id: product.id }}" v-text='product.title').
the problem:
When I click on any of the product links, the url changes but the content remains the same. So, if I am on #/product/7 and click on #/product/8 the url only will change. If I navigate from /product/:id page and click on a product it takes me to the right page with proper content.
As you can see on screenshot, current product id is 15, but the content is the one from the id 7, as shown in url at the bottom while I was hovering over the Sleek Silk Shirt product in cart.
Any ideas how to fix this?
You have to update the data of products variable when you change the route as vue optimises page reloads and does not reload in your case if you are on same route.
You can adapt the approach: Fetching Before Navigation described in vue-router docs:
With this approach we fetch the data before actually navigating to the new route. We can perform the data fetching in the beforeRouteEnter guard in the incoming component, and only call next when the fetch is complete:
export default {
data () {
return {
product: {},
error: null
}
},
beforeRouteEnter (to, from, next) {
getProduct(to.params.id, (err, product) => {
if (err) {
// display some global error message
next(false)
} else {
next(vm => {
vm.product = product
})
}
})
},
// when route changes and this component is already rendered,
// the logic will be slightly different.
watch: {
$route () {
this.product = {}
getProduct(this.$route.params.id, (err, product) => {
if (err) {
this.error = err.toString()
} else {
this.product = product
}
})
}
}
}
I couldnt really internalise the above answer with 'getProduct', so to be put simply.
I am using a Store and I needed to watch the $route and when it changes I called my store to dispatch the api call.
watch: {
$route () {
this.$store.dispatch('fetchStudyStandards',
this.$route.params.standardID);
}
}
So basically watch the route and if it changes, re do your api call.