infinite update loop in Vue - javascript

I'm trying to write a custom component. And hope I can use it like this
let app = new Vue({
el:'#app',
template:`
<tab>
<tab-item name='1'>
<h1> This is tab item 1</h1>
</tab-item>
<tab-item name='2'>
<h2> This is tab item 2</h2>
</tab-item>
</tab>`,
components:{
tab,
tabItem
}
})
Everything goes fine until you click the button. I got an error from console:
[Vue warn]: You may have an infinite update loop in a component render function.
found in
---> <Tab>
<Root>
I've tried many ways to solve this problem, however, failure always won the debugging competition.
How can I beat this problem?
Here is my code:
let tabItem = {
props:{
name:{
type: String,
required: true
}
},
render(h){
let head = this.$slots.head || ''
let body = this.$slots.default
let tail = this.$slots.tail || ''
return h('div', [
h('div', head),
h('div', body),
h('div', tail)])
}
}
let tab = {
data(){
return {
items:'',
currentView:0
}
},
methods:{
handleTabClick(item){
return ()=>{
let index = this.items.indexOf(item)
this.currentView = this.items[index]
}
},
extractProps(vnode){
return vnode.componentOptions.propsData
}
},
render(h){
this.items = this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
let headers = this.items.map( item => {
let name = this.extractProps(item).name
return h('button', {
on:{
click: this.handleTabClick(item)
}
}, name)
})
let head = h('div', headers)
this.currentView = this.items[0]
return h('div',[head, this.currentView])
}
}
Or any other ways to implement this component?
Thanks a lot for helping me out from the hell.
Thanks for your reply my friends. I'm pretty sure that I get an infinite loop error from the console and my code doesn't work as expected. I don't think using vnode is a good way to implement this component too. However, this is the best solution I can figure out.
This component -- tab should detect its child whose name is tabItem, which is also a component. And tab can extract some data from tabItem. In my case, tab will extract the name property of tabItemn, which will be used to generate the buttons for switching content. Click the button can switch to the relevant content, which is the body of tabItem. In my code, it's currenView.
Like a famous UI library, Element, its tab component can be used like this:
<el-tabs v-model="activeName" #tab-click="handleClick">
<el-tab-pane label="User" name="first">User</el-tab-pane>
<el-tab-pane label="Config" name="second">Config</el-tab-pane>
<el-tab-pane label="Role" name="third">Role</el-tab-pane>
<el-tab-pane label="Task" name="fourth">Task</el-tab-pane>
</el-tabs>
I need to implement one component like this but mine will be more simple. For learning how to do it, I read its source code. Maybe there's not a good way to filter child components. In the source, they use this to filter the el-tab-pane component:
addPanes(item) {
const index = this.$slots.default.filter(item => {
return item.elm.nodeType === 1 && /\bel-tab-pane\b/.test(item.elm.className);
}).indexOf(item.$vnode);
this.panes.splice(index, 0, item);
}
Source Code
I know that I can use $children to access its child components but doing so doesn't guarantee the order of the child components, which is not what I want. Because the order of switching button is important. Detail messages about vnode are not contained in the doc. I need to read the source.
Therefore, after reading the source of Vue, I wrote my code like this then I got my problem.
I finally didn't solve this bug and admit that using this kind of rare code sucks. But I don't know other solutions. So I need you guys help.
Thanks.

You shouldn't change your data in render function, this is wrong
this.items = this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
because it will keep re-rendering, here is a working example for your code, I simply removed items property from data and added new items computed property which returns tab-items nodes.
let tab = {
data(){
return {
currentView:0
}
},
methods:{
handleTabClick(item){
return ()=>{
let index = this.items.indexOf(item)
this.currentView = this.items[index]
}
},
extractProps(vnode){
return vnode.componentOptions.propsData
}
},
computed: {
items(){
return this.$slots.default.filter( node => {
return /tab-item/.test(node.tag)
})
}
},
render(h){
let headers = this.items.map( item => {
let name = this.extractProps(item).name
return h('button', {
on:{
click: this.handleTabClick(item)
}
}, name)
})
let head = h('div', headers)
this.currentView = this.items[0]
return h('div',[head, this.currentView])
}
}
let tabItem = {
name:"tab-item",
props:{
name:{
type: String,
required: true
}
},
render(h){
let head = this.$slots.head || ''
let body = this.$slots.default
let tail = this.$slots.tail || ''
return h('div', [[
h('div', head),
h('div', body),
h('div', tail)]])
}
}
let app = new Vue({
el:'#app',
template:`
<tab>
<tab-item name='1'>
<h1> This is tab item 1</h1>
</tab-item>
<tab-item name='2'>
<h2> This is tab item 2</h2>
</tab-item>
</tab>`,
components:{
tab,
tabItem
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.min.js"></script>
<div id="app"></div>

Related

React: Realtime rendering Axios return

hope you're well. I've been working on a company list component. I am having a problem with updating it in real time, because the axios call I'm sending to 'getById' after it renders is returning only a promise and not the actual data that it is supposed to and I don't have any idea as to why. So when I push the so called new company that I've just added into the array, which is in state, it is only pushing a promise into the array and not the actual Company. I don't have any idea why this is. What the code is supposed to be doing, is it is supposed to be putting the new company into the database, returning the success result, and then I'm using the item from that to make a fresh get call to the axios DB which is supposed to be returning the information I just entered so that I can then insert it into the same array in state that is within the list that is rendered in the company list. However, as I mentioned, only the promise is coming up for some reason.
At one point I was able to get this working, but I did that by essentially calling, 'componentDidMount' after the promise was pushed into the call back clause of the setState funciton of the push function - which was essentially causing the entire component to re-render. I'm a fairly new coder, but my understanding is is that that is a fairly unconventional way to code something, and contrary to good coding methodologies. I believe I should be able to push it into state, and then have it change automatically. I have attached the relevant code below for you to examine. If you believe you need more please let me know. If someone could please tell me why I am getting this weird promise instead of the proper response object, so that I can then insert that into state, I would greatly appreciate it. I've attached some images below the code snippets that I hope will be helpful in providing an answer. I have also left brief descriptions of what they are.
:
class Companies extends React.Component {
constructor(props) {
super(props);
this.state = {
Companies: [],
formData: { label: "", value: 0 },
};
}
componentDidMount = () => {
this.getListCompanies();
};
getListCompanies = () => {
getAll().then(this.listOfCompaniesSuccess);
};
listOfCompaniesSuccess = (config) => {
let companyList = config.items;
this.setState((prevState) => {
return {
...prevState,
Companies: companyList,
};
});
};
onCompListError = (errResponse) => {
_logger(errResponse);
};
mapCompanies = (Companies) => (
<CompaniesList Companies={Companies} remove={remove} />
);
handleSubmit = (values) => {
if (values.companyName === "PPP") {
this.toastError();
//THIS IS FOR TESTING.
} else {
add(values)
.getById(values.item) //I HAVE TRIED IN TWO DIFFERENT PLACES TO GET THE NEW COMPANY IN. HERE
.then(this.newCompanyPush)
.then(this.toastSuccess)
.catch(this.toastError);
}
};
//THIS CODE RIGHT HERE IS THE CODE CAUSING THE ISSUE.
newCompanyPush = (response) => {
let newCompany = getById(response.item); // THIS IS THE OTHER PLACE I HAVE TRIED.
this.setState((prevState) => {
let newCompanyList = [...prevState.Companies];
newCompanyList.push(newCompany);
return {
Companies: newCompanyList,
};
});
};
toastSuccess = () => {
toast.success("Success", {
closeOnClick: true,
position: "top-right",
});
};
toastError = (number) => {
toast.error(`Error, index is ${number}`, {
closeOnClick: true,
position: "top-center",
});
};
This is the axios call I am using.
const getById = (id) => {
const config = {
method: "GET",
url: companyUrls + id,
withCredentials: true,
crossdomain: true,
headers: { "Content-Type": "application/json" },
};
return axios(config).then(onGlobalSuccess).catch(onGlobalError);
};
After the promise is pushed into the array, this is what it looks like. Which is I guess good news because something is actually rendering in real time.
This is the 'promise' that is being pushed into the array. Please note, when I make the same call in post-man, I get an entirely different response, see below.
This is the outcome I get in postman, when I test the call.

ReactJS: Updating array inside object state doesn't trigger re-render

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

Sorting my API data numerically (0-9) in VueJS

I have data mapped from an API (see below) which I am reaching fine but I am looking at sorting it numerically (0-9). I'm having a hard time doing this with Vue. If I had my data static in the data(){...}, I can get it done a number of ways. But not from an API because I can't point to the API for some reason whenever I try to point to it from a function in my methods. I have no idea what is going on and I'm hoping you guys have some direction.
The template - Because of the nesting of the API, I am nesting loops as well. (maybe there is a better way to also do this. I'm all ears). allBatches is my Getter. I am serving the API through my State Manager (Vuex)
<div>
<div v-for="batches in allBatches" :key="batches.id">
<div
v-for="dispatchstation in batches.dispatchstation"
:key="dispatchstation.id">
<div v-for="apps in dispatchstation.applications" :key="apps.id">
<div>{{apps}}</div>
</div>
</div>
</div>
</div>
The data structure in the API - I intentionally left unrelated data out. There are other layers in between. But this shows the path I am looping and reaching out to.
"batches": [
{
"dispatchstation": [
{
"applications": [
"384752387450",
"456345634563",
"345634563456",
"567845362334",
"567456745677",
"456734562457",
"789676545365",
"456456445556",
"224563456345",
"456878656467",
"053452345344",
"045440545455",
"045454545204",
"000014546546",
"032116876846",
"546521302151",
"035649874877",
"986765151231",
"653468463854",
"653853121324",
"000145456555"
]
}
]
}
],
I've tried to do this with lodash using _.orderBy and use it a pipe. No luck. And I also tried this:
data() {
return {
sortAsc: true,
sortApps: "" // see explanation
};
},
computed: {
...mapGetters(["allBatches"]),
sortedData() {
let result = this.sortApps;
let ascDesc = this.sortAsc ? 1 : -1;
return result.sort(
(a, b) => ascDesc * a.applications.localeCompare(b.applications)
);
}
},
I used (tried) this method by giving sortApps the loop criteria dispatchstations.applications and loop v-for='apps in sortedData'. I'm sure that is wrong. Vue is not really my forte.
I really don't have any preference on how this should be as long as the data renders sorted out numerically ASC.
Any thoughts?
Thanks
EDIT
Using Chase's answer, I am still getting the data through but it doesn't display. I had to remove the negation (!).
Mutation and getters of State view from the vue chrome tool
EDIT 2 - A simplified version of my store module
import axios from "axios";
const state = {
batches: [],
};
const getters = {
allBatches: state => {
return state.batches;
},
};
const actions = {
async fetchBatches({ commit }) {
const response = await axios.get(`${window.location.protocol}//${window.location.hostname}:4000/batches`);
commit("setBatches", response.data);
},
};
const mutations = {
setBatches: (state, batches) => (state.batches = batches),
};
export default {
state,
getters,
actions,
mutations
};
I hope that I understood your question, so you just need to order the data to render it and you don't need it as ordered in your store?
to display orderd data you can use this computed function, I hope it will help you
computed:{
...mapGetters(["allBatches"]),
orderApplications(){
let copieBatches = JSON.parse(JSON.stringify(this.allBatches));
copieBatches.forEach(batches => {
batches.dispatchstation.forEach(dispatchstation=>{
dispatchstation.applications.sort()
})
});
return copieBatches
}
}
and your HTML will be like
<div>
<div v-for="batches in orderApplications">
<div
v-for="dispatchstation in batches.dispatchstation"
:key="dispatchstation.id">
<div v-for="apps in dispatchstation.applications">
<div>{{apps}}</div>
</div>
</div>
</div>
</div>
Hopefully I'm not misunderstanding your question, but essentially I would recommend loading your data in the same way that you are currently and handling the sort in a computed method.
This is assuming that the length of batches and dispatchstation will always be 1.
new Vue({
el: "#app",
data: {
allBatches: null
},
computed: {
batchesSorted() {
if (!this.allBatches) return {}
const output = this.allBatches.batches[0].dispatchstation[0].applications;
output.sort((a, b) => {
return parseInt(a) - parseInt(b)
})
return output
}
},
async created() {
// Equivelent to ...mapGetters(["allBatches"]) for the example
this.allBatches = {
"batches": [{
"dispatchstation": [{
"applications": [
"384752387450",
"456345634563",
"345634563456",
"567845362334",
"567456745677",
"456734562457",
"789676545365",
"456456445556",
"224563456345",
"456878656467",
"053452345344",
"045440545455",
"045454545204",
"000014546546",
"032116876846",
"546521302151",
"035649874877",
"986765151231",
"653468463854",
"653853121324",
"000145456555"
]
}]
}]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, key) in batchesSorted" :key="key">
{{ item }}
</div>
</div>
Let me know if I misunderstood anything or if you have any questions.

Iterate list of strings in mithril and create drop down

I tried searching a lot of internet but could not find answer to a simple question. I am very new to mithril (do not know why people chose mithril for project :( ). I want to iterate through a list of strings and use its value in drop down with a checkbox.
const DefaultListView = {
view(ctrl, args) {
const parentCtrl = args.parentCtrl;
const attr = args.attr;
const cssClass = args.cssClass;
const filterOptions = ['Pending', 'Paid', 'Rejected'];
// as of now, all are isMultipleSelection filter
const selectedValue =
parentCtrl.filterModel.getSelectedFilterValue(attr);
function isOptionSelected(value) {
return selectedValue.indexOf(value) > -1;
}
return m('.filter-dialog__default-attr-listing', {
class: cssClass
}, [
m('.attributes', {
onscroll: () => {
m.redraw(true);
}
}, [
filterOptions.list.map(filter => [
m('.dropdown-item', {
onclick() {
// Todo: Add click metrics.
// To be done at time of backend integration.
document.body.click();
}
}, [
m('input.form-check-input', {
type: 'checkbox',
checked: isOptionSelected(filter)
}),
m('.dropdown-text', 'Pending')
])
])
])
]);
}
};
Not sure. How to do it. This is what I have tried so far but no luck. Can someone help me this?
At the beginning of the view function you define an array:
const filterOptions = ['Pending', 'Paid', 'Rejected'];
But later on in the view code where you perform the list iteration, filterOptions is expected to be an object with a list property:
filterOptions.list.map(filter =>
That should be filterOptions.map(filter =>.
There may be other issues with your code but it's impossible to tell without seeing the containing component which passes down the args. You might find it more helpful to ask the Mithril chatroom, where myself and plenty of others are available to discuss & assist with tricky situations.

Angular 2 display newly added item to http in other component

This is probably easy for someone, but I just can't get it. So I have a list of items to display:
My service that fetches the data, configService.ts
ngOnInit() {
getConfig(): Observable<any> {
return this.http.get<any>('/api/config').map(res => res);
}
}
My savedSearhes component that populates data in the ngFor:
this.configService.getConfig().subscribe(data => {
this.userSavedSearches = data.isr.me.savedSearches //get an array of items
});
the html to display data:
<div *ngFor="let savedsearch of userSavedSearches">
{{savedsearch.name }}
</div>
The main issue I have, is I have another component that I use to add a new item to the same server.
saveSearch() {
this.saveSearchObject = {
name: this.SearchName,
description: this.SearchDescription,
context: this.theContext,
}
this.searchService.createSavedSearch(this.saveSearchObject).subscribe(data => {
console.log(data) // newly added item to server
})
}
The service that posting new item to server:
createSavedSearch(search: SavedSearch): Observable<SavedSearch> {
return this.http.post<SavedSearch>('/api/searches/', search)
}
When I add a new item, the item actually gets added to the server. But I don't see the "savedSearches" component display added item, only when I reload the page I can see new item added.
How to add new item to the server and see its being added with new item in other component without reloading the page.
You can achieve it by Creating a subject where saveSearch function lies
let subjectUserSavedSearches = new Subject();
let obsrvUserSavedSearches = subjectUserSavedSearches.AsObservable();
saveSearch() {
this.saveSearchObject = {
name: this.SearchName,
description: this.SearchDescription,
context: this.theContext,
}
this.searchService.createSavedSearch(this.saveSearchObject).subscribe(data => {
this.userSavedSearches = data;
this.subjectUserSavedSearches.next(this.userSavedSearches);
})
}
Now watch that obsrvUserSavedSearches on the component you need to show data.
The best way is to move methods getConfig() and saveSearch() in a service and just create a subject for userSavedSearches and an Observable to watch the same.
You won't see it unless you do a getConfig() again. Use a .switchMap() to chain your http calls.
saveSearch() {
this.saveSearchObject = {
name: this.SearchName,
description: this.SearchDescription,
context: this.theContext,
}
this.searchService.createSavedSearch(this.saveSearchObject)
.switchMap(data => {
console.log(data) // newly added item to server
return this.configService.getConfig();
})
.subscribe(data => {
this.userSavedSearches = data.isr.me.savedSearches //get an array of items
})
}
Otherwise, unless your savedSearches() actually returned a newly refreshed list, you can do it in your subscribe:
saveSearch() {
this.saveSearchObject = {
name: this.SearchName,
description: this.SearchDescription,
context: this.theContext,
}
this.searchService.createSavedSearch(this.saveSearchObject).subscribe(data => {
this.userSavedSearches = data
})
}

Categories

Resources