Infinite render loop when instantiating 2 new models
See original GitHub issueFirst of all, thanks a lot for this excellent framework which finally brings some strong opinions as of how Vuex should be structured and configured šŖ
Steps to reproduce
I am reproducing the CRUD form example from the documentation using FeathersVuexFormWrapper
.
Things work fine when I am using a single instance of a form wrapper, which contains a new model initializer. Below is my code snippet reproducing the logic from the documentation.
Now, I want to mount another component that also uses FeathersVuexFormWrapper
, and also contains a new model initializer. For my use case, I actually have a companyFormWrapper
and a personFromWrapper
. But the problem also arises when Iām simply mounting any of these components twice on the same page - basically, anytime there is more than one new XXX()
instantiation instruction on the same page.
Could you kindly let me know what Iām doing wrong here?
EDIT: I realize that this is also happening when one new
instruction exists in a component and another one exists in the servicePlugin. For example I have this when loading relations with setupInstance
, where I instantiate a new model for each linked object. So I guess there is something deep down that I really havenāt understood about the workings of the framework?
Expected behavior
Each form wrapper should correctly instantiate its new model if no existing model is provided for edition.
Actual behavior
Anytime there is more than one new XXX()
instantiation instruction on the same page, an infinite render loop occurs. I get the following message in the console:
vue.runtime.esm.js?2b0e:619 [Vue warn]: You may have an infinite update loop in watcher with expression "item"
found in
---> <PersonFormWrapper>
<List> at src/views/dataroom/entities/List.vue
<Stakeholders> at src/views/dataroom/Stakeholders.vue
<DefaultLayout> at src/layouts/Default.vue
<App> at src/App.vue
<Root>
I have put a watcher on the item property and it enters an infinite refresh loop where temp id is incremented forever:
assigning temporary id to item PersonĀ {documents: Array(0), addresses: Array(0)}
**logger.js?b054:87 mutation people/addItem @ 22:29:37.904**
PersonFormWrapper.vue?0ad9:44 ITEM {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042ede","__isTemp":true} {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042edc","__isTemp":true}
PersonFormWrapper.vue?0ad9:44 ITEM {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042ede","__isTemp":true} {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042ede","__isTemp":true}
**logger.js?b054:87 mutation people/createCopy @ 22:29:37.914**
utils.js?225b:158 assigning temporary id to item PersonĀ {documents: Array(0), addresses: Array(0)}
**logger.js?b054:87 mutation people/addItem @ 22:29:37.922**
PersonFormWrapper.vue?0ad9:44 ITEM {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042edf","__isTemp":true} {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042edd","__isTemp":true}
utils.js?225b:158 assigning temporary id to item PersonĀ {documents: Array(0), addresses: Array(0)}
**logger.js?b054:87 mutation people/addItem @ 22:29:37.930**
PersonFormWrapper.vue?0ad9:44 ITEM {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042ee0","__isTemp":true} {"documents":[],"addresses":[],"__id":"5ec987b14cf79c9a15042ede","__isTemp":true}
Configuration
ā@feathersjs/feathersā: ā^4.5.3ā, ā@feathersjs/rest-clientā: ā^4.5.4ā, āfeathers-hooks-commonā: ā^5.0.3ā, āfeathers-vuexā: ā^3.10.4ā,
The client is configured as follows:
const feathersClient = feathers()
.configure(restClient.axios(axios))
.hooks({
before: {
all: [
iff(
context => ['create', 'update', 'patch'].includes(context.method),
discard('__id', '__isTemp')
)
]
}
})
export default feathersClient
const { makeServicePlugin, makeAuthPlugin, BaseModel, models, FeathersVuex } = feathersVuex(
feathersClient,
{
idField: 'id',
nameStyle: 'path',
preferUpdate: true,
replaceItems: true,
debug: ['development', 'staging'].includes(process.env.NODE_ENV)
}
)
export { makeAuthPlugin, makeServicePlugin, BaseModel, models, FeathersVuex }
My service is plain vanilla from the documentation:
import feathersClient, { makeServicePlugin, BaseModel } from '../../../feathers-client'
class Person extends BaseModel {
static modelName = 'Person'
static instanceDefaults () {
return {
documents: [],
addresses: []
}
}
}
const servicePath = 'people'
const servicePlugin = makeServicePlugin({
Model: Person,
service: feathersClient.service('people'),
servicePath
})
export default servicePlugin
and finally here is the actual form component:
<template>
<FeathersVuexFormWrapper
:item="item"
watch
>
<template v-slot="{ clone, save, reset }">
<person-editor
:modal-id="modalId"
:item="clone"
@close="handleClose"
@save="save().then(handleClose)"
@reset="reset"
/>
</template>
</FeathersVuexFormWrapper>
</template>
<script>
import PersonEditor from './PersonEditor'
import { models, FeathersVuexFormWrapper } from 'feathers-vuex'
export default {
components: {
PersonEditor,
FeathersVuexFormWrapper
},
data () {
return {
modalId: 'personFormModal',
itemId: null
}
},
computed: {
item () {
const { Person } = models.api
// As soon as 2 models are instantiated with `new XXX()` on the same page, an infinite loop occurs. If the `new Person()` instruction is replaced by `{}`, the form crashes on new items but the infinite loop disappears.
return this.itemId ? Person.getFromStore(this.itemId) : new Person()
}
},
watch: {
item: function (newVal, oldVal) {
// Watching this shows an infinite loop of temp id increments
console.log('ITEM', JSON.stringify(newVal), JSON.stringify(oldVal))
}
},
methods: {
// Note that all these methods are called via tight coupling directly from the parent component.
handleAdd () {
this.itemId = null
this.$bvModal.show(this.modalId)
},
handleEdit ({ id }) {
this.itemId = id
this.$bvModal.show(this.modalId)
},
handleClose () {
this.itemId = null
this.$bvModal.hide(this.modalId)
}
}
}
</script>
Issue Analytics
- State:
- Created 3 years ago
- Comments:5 (2 by maintainers)
Top GitHub Comments
After a superficial read-through, my best guess is that the issue is youāre calling
new Person()
andgetFromStore
in the samecomputed
.new Person()
is going to create a new Person Model and store it the serviceās Vuex state intempsById
. But thengetFromStore
will cause the computed to fire again because it is reactive totempsById
changes. ButitemId
is still null so it again callsnew Person()
ā¦ thus the infinite loop.When instantiating a model, you can pass a second parameter to the modelās constructor
{ commit: false }
which will not add the temporary item to the store.But based on your usage of the model as a placeholder, when your data
itemId
is null, I would suggest having adata
field on your component calledplaceholder
or something that is instantiated once when your component is created and then in your computed, return that placeholder in your ternary instead of callingnew Person()
.The following is untested and everything not relevant to what Iām discussing is removed for simplicityās sake.
Itās been quite some time and now I bumped into this issue again on another project. Strangely enough @hamiltoes the solution youāre suggesting works fine when using
{ commit: true }
(otherwise itās not possible to actually save new instances). Thanks for that!