VUEjs 3 Warning: Maximum recursive updates exceeded - javascript

currently I am facing the problem that Vue issues a warning:
"Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function."
I can't figure out where the error should be.
I iterate over 2 loops and then want to give a counter value to the component inside, which is then passed on and then interpreted by modulus to CSS classes in 3rd level.
Unfortunately I have to do it this way, because the created components are parts of a dynamically created CSS grid. I would like to provide virtually every row, so all cells at the same height with a uniform "even/odd" class.
Here is the vue-component that creates this increment:
<template>
<template v-for="(project, p) in projects" :key="project.id">
<template v-for="(component, c) in project.components" :key="component.id">
<grid-swim-lane
:project="project"
:component="component"
:grid-row-even-odd-count="evenOddCount++"
/>
</template>
</template>
</template>
<script>
import GridSwimLane from "./GridSwimLane";
import {mapGetters} from "vuex";
export default {
components: { GridSwimLane },
data() {
return {
evenOddCount: -1
}
},
computed: {
...mapGetters('projects', { projects: 'getAllProjects' })
},
}
</script>
<style scoped></style>
This increment value is generated and successfully passed through to the last component despite the warning. But how can I make it work without the warning?
I have already tried a few things. But I can't get any further.
I can't do this with CSS selectors, because I want to work with fixed classes.
Thanks in advance for your tips.

I figured out, the each grid-swim-lane component's this.$.uid value is not sequential, but in sequence even and odd :-)
So i use this value to determine the 'even' and 'odd' css-class:
<template>
<!-- ------------------ BEGIN LEFT SIDE BAR ------------------ -->
<grid-swim-lane-info
:grid-row-even-odd-count="gridRowEvenOddCount"
/>
<!-- ------------------ BEGIN RELEASES ------------------- -->
<grid-swim-lane-releases
:grid-row-even-odd-count="gridRowEvenOddCount"
/>
</template>
<script>
import GridSwimLaneInfo from "./GridSwimLaneInfo";
import GridSwimLaneReleases from "./GridSwimLaneReleases";
export default {
components: {
GridSwimLaneInfo, GridSwimLaneReleases
},
props: {
component: { type: Object, default: { id: 0, name: 'no-component'} }
},
data() {
return {
gridRowEvenOddCount: 0
}
}
mounted() {
this.gridRowEvenOddCount = this.$.uid;
}
}
</script>
<style scoped></style>

Related

Vue - Access prop in child component, regular javascript

I am loading a Vue component from another Vue component and am passing a property to that component. I need to access this property in the regular javascript of that component, but cannot figure out how to do this.
The simplified parent component could look as follows:
<template>
<div>
<MenuEdit :menu-list="menuList"></MenuEdit>
</div>
</template>
<script>
import MenuEdit from '#/components/MenuEdit';
export default {
name: 'Admin',
data: function () {
return {
menuList: ["Item1","Item2","Item3","Item4"]
};
},
components: {
MenuEdit
}
}
</script>
<style scoped>
</style>
And the MenuEdit could look as follows:
<template>
<div>
{{ menuList }}
</div>
</template>
<script>
//console.log(this.menuList) // Does not work.
export default {
name: 'MenuEdit',
props: [
'menuList'
],
methods: {
testMenu: function() {
console.log(this.menuList) //This works fine
}
}
}
</script>
<style scoped>
</style>
EDIT
To add some context to the question, I am implementing sortablejs on Buefy using the following example: https://buefy.org/extensions/sortablejs
Instead of calling "vnode.context.$buefy.toast.open(Moved ${item} from row ${evt.oldIndex + 1} to ${evt.newIndex + 1})" at the end of the first const, I want to update the component (or better said, update the related Array).
In the example, the const are defined outside of the component, which is why I ended up with this question.
You cannot access the prop as that code (where your console.log is) runs before the component is mounted, before it's even declared really
If you want to access stuff when the component is first mounted, you can use the mounted lifecycle method

Mixin for destroyed Vue component is still listening for events

I have a parent component that conditionally renders one of two child components:
<template>
<div>
<!-- other code that changes conditional rendering -->
<folders v-if="isSearchingInFolders" :key="1234"></folders>
<snippets v-if="!isSearchingInFolders" :key="5678"></snippets>
</div>
</template>
Each of these components use the same mixin (searchMixin) locally like so:
<template>
<div>
<div>
<snippet
v-for="item in items"
:snippet="item"
:key="item.id">
</snippet>
<img v-if="busy" src="/icons/loader-grey.svg" width="50">
</div>
<button #click="getItems">Get More</button>
</div>
</template>
<script>
import searchMixin from './mixins/searchMixin';
import Snippet from './snippet';
export default {
components: { Snippet },
mixins: [searchMixin],
data() {
return {
resourceName: 'snippets'
}
},
}
</script>
Each of the components is functionally equivalent with some slightly different markup, so for the purposes of this example Folders can be substituted with Snippets and vice versa.
The mixin I am using looks like this (simplified):
import axios from 'axios'
import { EventBus } from '../event-bus';
export default {
data() {
return {
hasMoreItems: true,
busy: false,
items: []
}
},
created() {
EventBus.$on('search', this.getItems)
this.getItems();
},
destroyed() {
this.$store.commit('resetSearchParams')
},
computed: {
endpoint() {
return `/${this.resourceName}/search`
},
busyOrMaximum() {
return this.busy || !this.hasMoreItems;
}
},
methods: {
getItems(reset = false) {
<!-- get the items and add them to this.items -->
}
}
}
In the parent component when I toggle the rendering by changing the isSearchingInFolders variable the expected component is destroyed and removed from the DOM (I have checked this by logging from the destroyed() lifecycle hook. However the searchMixin that was included in that component does not appear to be destroyed and still appears to listen for events. This means that when the EventBus.$on('search', this.getItems) line is triggered after changing which component is actively rendered from the parent, this.getItems() is triggered twice. Once for folders and once for snippets!
I was expecting the mixins for components to be destroyed along with the components themselves. Have I misunderstood how component destruction works?
Yes, when you pass an event handler as you do EventBus keeps the reference to the function you passed into. That prevents the destruction of the component object. So you need clear the reference by calling EventBus.$off so that the component can be destructed. So your destroy event hook should look like this:
destroyed() {
this.$store.commit('resetSearchParams')
EventBus.$off('search', this.getItems)
},

Toggling Vuetify navigation drawer v-modelled to Vuex variable

(I spied some similar questions, but none seemed to address my problem, however please refer if I missed something.)
I'm using Vue, Vuex, and Vuetify. Working from the "Google Keep" example layout, I factored out the NavDrawer and AppBar components. I'm having some trouble getting the NavDrawer toggle to work, however.
Before implementing Vuex, I used props and events, going through the parent component. With Vuex, my code is as follows:
main.js:
const store = new Vuex.Store({
state: {
drawerState: null
},
mutations: {
toggleDrawerState(state) {
state.drawerState = !state.drawerState
}
}
})
AppBar.vue:
<template>
<v-app-bar app clipped-left class="primary">
<v-app-bar-nav-icon #click="toggleDrawer()"/>
</v-app-bar>
</template>
<script>
export default {
name: 'AppBar',
methods: {
toggleDrawer: function() {
this.$store.commit('toggleDrawerState')
}
}
}
</script>
NavigationDrawer.vue:
<template>
<v-navigation-drawer v-model="drawerState" app clipped color="grey lighten-4">
<!-- content -->
</v-navigation-drawer>
</template>
<script>
export default {
name: 'NavigationDrawer',
computed: {
drawerState: {
get: function() { return this.$store.state.drawerState },
set: () => null
}
}
}
</script>
The set: () => null in the NavDrawer's computed properties is because I at first set it to call the mutation, which resulted in a feedback loop of toggling.
Now, my problem is that, given an initial v-model value of null, Vuetify has the Drawer open on desktop and closed on mobile. And when the drawerState = !drawerState is called, the null is made true, but that just keeps the drawer open on desktop, meaning that the button has to be clicked again to close the drawer. After that initial double-trigger problem, it works fine on desktop. On mobile, however, it always needs two triggers. (I say mobile, but really I just resized my browser window down.) I assume this is because when resizing (or on load), the drawer automatically hides, but has no way to update the Vuex store boolean, meaning that a double trigger is necessary.
My question is thus: what is the standard or best-practice way to implement Vuetify's navdrawer, so as to toggle it from another component? I thought that the state (whether open or closed) might be directly stored, but there are no "opened" or "closed" events to access it by. (E.g. this question has no answers.) It works fine on the example page, but how can that be adapted to work as child components?
Thanks!
It's a good option to use Vuex getters and refer to them in your computed getter, as well as retrieving the new value in the computed setter and using that to commit the mutation in the store. So your store would become:
const store = new Vuex.Store({
state: {
drawerState: false
},
mutations: {
toggleDrawerState (state, data) {
state.drawerState = data
}
},
getters : {
drawerState: (state) => state.drawerState
}
})
And your navigation drawer component would become:
<template>
<v-navigation-drawer v-model="drawerState" app clipped color="grey lighten-4">
<!-- content -->
</v-navigation-drawer>
</template>
<script>
export default {
name: 'NavigationDrawer',
computed: {
drawerState: {
get () { return this.$store.getters.drawerState },
set (v) { return this.$store.commit('toggleDrawerState', v) }
}
}
}
</script>
Here is a codepen to show a full example:
https://codepen.io/JamieCurnow/pen/wvaZeRe

Dynamically bind a class in vue js via a method on the parent component

I have a component which needs to know the number of items in the array. The idea is that I will give each instance of my component a specific class using the .length property on the array.
Here is the code that I have so far:
Child component
<template>
<ul class="circle-container" :class="featuresCount">
<slot></slot>
</ul>
</template>
<script>
export default {
props: {
featuresCount: {
type: String,
default: ''
}
}
}
</script>
Parent component
<template>
<div>
<p>{{ featuresLength() }}</p> <!-- This returns 2 -->
<!-- Output is <ul class="circle-container featuresLength"> -->
<Halo featuresCount="featuresLength">
// stuff here
</Halo>
</div>
</template>
<script>
import Halo from '#/components/ProductHalo.vue'
import json from '#/json/data.json'
export default {
name: 'ProductSingle',
components: {
Halo
},
data() {
return {
products: json
}
},
methods: {
// ... more above
featuresLength() {
return this.products[0].features.length
}
}
}
</script>
Not sure why this is happening except maybe the child component doesn't know how to get return value of parent's method?
Thanks to all the people who tried to answer, I figured it out myself in the end. I had to convert the integer to a string before it would be accepted as a viable class name. See below:
featuresLength() {
return this.products[0].features.length.toString()
}
Then combine that with the answers above:
<Halo :featuresCount="featuresLength()"> .. etc
and it worked!
I think you forgot to bind :featuresCount like this
<Halo :featuresCount="featuresLength">...</Halo>

Firestore: Unable to fetch nested documents. Why?

I'm working on a Vue (with Vuex) app, with a firebase/firestore backend, and I'm having trouble with fetching documents referenced by other documents. Specifically, I have a recipes collection (together with users and comments collections as seen in the linked photo) collection, with each contained document having, among others, addedBy and comments fields. Both are id strings (the comments field being an array of ids) of the respective documents referenced. Now, I'm not sure if this is the best way of going about it, but coming from a MongoDB background, I thought it'd be possible fetch the details of these fields like we do with MongoDB.
I have had a couple of tries but nothing seems to work. An example of this is seen in the code snippets below.
Main Recipe Component/Container (I query the DB for a specific recipe document)
<template>
<div class="recipe-detail">
<loader v-if="isLoading" message="Loading Recipe" size="huge" />
<div v-else-if="!recipe" class="no-recipe">
No such recipe in DB
</div>
<div v-else class="comments-and-similar">
<div class="comments">
<h3 class="comments-title">Comments</h3>
<comment-form />
<comment-list :comment-list="recipe.comments" />
</div>
<div class="similar-recipes">
<similar-recipes />
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import Loader from "#/components/shared/Loader";
import PostedBy from "#/components/recipes/detail/PostedBy";
import CommentForm from "#/components/forms/CommentForm";
import CommentList from "#/components/recipes/detail/CommentList";
export default {
name: "recipe-detail",
components: {
Loader,
PostedBy,
CommentForm,
CommentList,
},
data() {
return {
recipeId: this.$route.params.recipeId,
fullPath: this.$route.fullPath
};
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ recipe: "recipes/recipe" }),
},
watch: {
"$route.params.recipeId"(id) {
this.recipeId = id;
}
},
methods: {
...mapActions({ getRecipeById: "recipes/getRecipeById" })
},
created() {
if (!this.recipe || this.recipe.id !== this.recipeId) {
this.getRecipeById(this.recipeId);
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment List Component (Here, I receive the comment id list via props)
<template>
<section class="comments">
<div v-if="commentList.length === 0">Be the first to comment on recipe</div>
<template v-else v-for="comment in commentList">
<comment :comment-id="comment" :key="comment" />
</template>
</section>
</template>
<script>
import Comment from "./Comment";
export default {
name: "comment-list",
components: {
Comment
},
props: {
commentList: {
type: Array,
required: true
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment Component
<template>
<article>
<div v-if="isLoading">Loading comment...</div>
<div v-else>{{ JSON.stringify(comment) }}</div>
</article>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "comment",
props: {
commentId: {
type: String,
required: true
}
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ comment: "recipes/comment" })
},
methods: {
...mapActions({ getCommentById: "recipes/getCommentById" })
},
created() {
this.getCommentById(this.commentId);
}
};
</script>
Now, the Comment component is where I'm having trouble. I get each individual comment id and use it to query the DB, specifically the comments collection. I actually get the comment detail body from the DB, this query wont stop and results in an infinite loop. I have to comment out the method inside created life-cycle for it to stop. I tried the same approach for the addedBy field to query for the user and got the same issue. So, what I'm I doing wrong.
DB structure
PS: I did not feel the need to include the Vuex methods (actions) in order to reduce verbosity. They work just fine sending the corresponding queries.
It looks like you're sharing an isLoading flag between all your components.
I believe what is happening is this:
You try to load a comment and isLoading is set to true.
The component recipe-detail re-renders to show the Loading Recipe message. Note that this will destroy the comment-list.
When the comment finishes loading isLoading will be set back to false.
recipe-details will re-render again, this time showing the comment-list. This will create a new set of comment components, each of which will try to load their data again. This jumps us back to step 1.
On an unrelated note, it looks like your comment component is relying on a single comment being held in the store. This might be fine when there's only one comment but when there are multiple comments they'll all load at the same time and only one of them will ultimately end up in the store.

Categories

Resources