update complex array react js useState - javascript

I have a long array. In this array I want to update the qty which is under misc array
I have a list of a person, let's say person with index 0 and index 1, each person can have misc with index 0, and index 1 and each misc can have array with index 0 and 1 and I want to update the qty of misc array.
Here is an example: https://playcode.io/1028032
import React from 'react';
import { useState } from 'react';
export function App(props) {
const[persons,setPersons] = useState([
{
id: 1,
name: "john",
gender: "m",
misc: [
{
id: 1,
name: "xxx",
qty: 1
},
{
id: 2,
name: "xxx1",
qty: 1
}
]
},
{
id: 2,
name: "mary",
gender: "f",
misc: [
{
id: 1,
name: "aaa",
qty: 1
},
{
id: 2,
name: "bbb",
qty: 1
}
]
},
]
)
const updatePersonMiscQty = (personIndex, miscIndex) => {
setPersons(persons => {
const miscItem = persons[personIndex]?.misc?.[miscIndex]
if (miscItem ) {
miscItem.qty += 1;
}
return [...persons];
})
}
return (
<div className='App'>
<h1>Hello React.</h1>
<h2>Start editing to see some magic happen!</h2>
<a href="" onClick={()=>updatePersonMiscQty(0,0)}>Click</a>
{console.log(persons)}
</div>
);
}
Let's say I passed 0,0 in updatePersonMiscQty(), first 0 is personIndex, and second 0 is miscIndex. so now it should update qty of person with index 0 and misc with index 0. This array. But nothing is rendered.
{
id: 1,
name: "john",
gender: "m",
misc: [
{
id: 1,
name: "xxx",
qty: 2
},

This is reloading the page:
<a href="" onClick={()=>updatePersonMiscQty(0,0)}>Click</a>
You can prevent this by setting the href to "#".
<a href="#" onClick={()=>updatePersonMiscQty(0,0)}>Click</a>
Or you probably want to use a button instead.
<button onClick={()=>updatePersonMiscQty(0,0)}>Click</button>

you just need to change <a> tag to <button> but I also would recommend to create deeply nested copies when trying to update a nested object.
Try immer.js, with it you just specify what has changed at any level of nesting and it will automatically take care of all the changes that need to be handled.https://immerjs.github.io/immer/

Related

How get a load more button for a li list vue js

I'm trying to implement a load more button to my code. I would be able to this in javascript but I can't find a similar way in vue.
This is my vue code. I've tried asking the element with the company id but it's not reactive so I can't just change the style.
<main>
<ul>
<li v-for="company in companiesdb" :key="company.id" v-bind:id="company.id" ref="{{company.id}}" style="display: none">
{{company.name}}<br>
{{company.email}}
</li>
</ul>
</main>
this is my failed atempt of doing it in javascript but as I've mentioned before ref is not reactive so I can't do it this way
limitView: function (){
const step = 3;
do{
this.numberCompaniesVis ++;
let li = this.$refs[this.numberCompaniesVis];
li.style = "display: block";
}while (this.numberCompaniesVis % 3 != step)
}
I think the way you are approaching this problem is a little complex. Instead, you can create a computed variable that will change the number of lists shown.
Here's the code
<template>
<div id="app">
<ul>
<li v-for="(company, index) in companiesLoaded" :key="index">
{{ company }}
</li>
</ul>
<button #click="loadMore">Load</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
companiesdb: [3, 4, 1, 4, 1, 2, 4, 4, 1],
length: 5,
};
},
methods: {
loadMore() {
if (this.length > this.companiesdb.length) return;
this.length = this.length + 3;
},
},
computed: {
companiesLoaded() {
return this.companiesdb.slice(0, this.length);
},
},
};
</script>
So instead of loading the list from companiesdb, create a computed function which will return the new array based on companiesdb variable. Then there's the loadMore function which will be executed every time user clicks the button. This function will increase the initial length, so more lists will be shown.
Here's the live example
Just use computed property to create subset of main array...
const vm = new Vue({
el: '#app',
data() {
return {
companies: [
{ id: 1, name: "Company A" },
{ id: 2, name: "Company B" },
{ id: 3, name: "Company C" },
{ id: 4, name: "Company D" },
{ id: 5, name: "Company E" },
{ id: 6, name: "Company F" },
{ id: 7, name: "Company G" },
{ id: 8, name: "Company H" },
{ id: 9, name: "Company I" },
{ id: 10, name: "Company J" },
],
companiesVisible: 3,
step: 3,
}
},
computed: {
visibleCompanies() {
return this.companies.slice(0, this.companiesVisible)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<ul>
<li v-for="company in visibleCompanies" :key="company.id" :id="company.id">
{{company.name}}
</li>
</ul>
<button #click="companiesVisible += step" v-if="companiesVisible < companies.length">Load more...</button>
</div>

How to relate two (or more) objects through a property (in javascript)?

Let's say I have these two objects:
let person1 = {name: "Charlie", age: 65, childrenNames:["Ruth", "Charlie Jr."] parentNames: ["Dolph", "Grace"]};
let person2 = {name: "Charlie Jr.", age: 34, childrenNames:[] parentNames: ["Charlie", "Grace"]};
Now let's say I want to express the fact that person1 is person2's father and, consequently, that person2 is person1's son. That is, the "Charlie Jr" in the person1's childrenNames property is person2 and the "Charlie" in the person2's parentNames property is person1.
How could I achieve this? I don't see how embedding one object inside the other would solve the problem, but simply replicate it. Is there a way to make a property inside an object a sort of identifier for another object?
Thanks so much!
For example if you want to know if someone is the child of person 1, you can do something like this:
person1.childrenNames.forEach((childrenName) => {
if(childrenName=== person2.name) {
console.log(person1.name + ' is the parent of + person2.name);
});
Also you can do a nested function so you can check if the person if the parent of multiple persons.
Why not add relationship indexes ? combine your people, to 1 people array.
Iterate and add instead of parentNames, parentIndexes. This way, instead of looking for parents or sons by names, you have the index.
Note, to make my example simple, I am only doing parent to son relationship. You can easily add a son to parent relationship using the exact same logic.
Example
if (peopleArray[i].parentsIndex.length > 0) {
// get first parent
//peopleArray[peopleArray[i].parentsIndex[0]];
}
Modify your object.
let peopleArray = [
{
name: "Charlie",
age: 65,
parentNames: ["Dolph", "Grace"]
},
{
name: "Grace",
age: 65,
parentNames: ["Dolph", "Grace"]
},
{
name: "Dolph",
age: 65,
parentNames: ["ADSGS", "Grace"]
}
];
peopleArray = peopleArray.map(callback.bind(null));
function callback(item) {
item["parentsIndex"] = [];
for (let i = 0; i < item.parentNames.length; i++) {
let parentObj = peopleArray.find(x => x.name === item.parentNames[i]);
if (parentObj !== undefined) {
item["parentsIndex"].push(peopleArray.indexOf(parentObj));
}
}
return item;
}
console.log(peopleArray);
// use case
if (peopleArray[0].parentsIndex.length > 0) {
// get first parent
//peopleArray[peopleArray[0].parentsIndex[0]];
}
I guess it depends on how complicated your scenario would be and what you would like to achieve, but say you add an extra table for relations. This table could hold information on the type of relation 2 persons share, and could then be used to look up that data.
For example, if we have 4 persons, from which 2 are parents (Charlie & Grace) and 1 is the son (Charlie Jr), we could form a relation table as below.
We don't need to indicate that Charlie Jr is a son, as he we already know the parents of the child.
const familyDb = {
persons: [
{ id: 1, name: 'Charlie', age: 68 },
{ id: 2, name: 'Grace', age: 64 },
{ id: 3, name: 'Charlie Jr', age: 34 },
{ id: 4, name: 'Grace', age: 36 }
],
relations: [
{ id: 1, person1: 1, person2: 2, type: 'spouse', from: 1970, till: null },
{ id: 2, person1: 3, person2: 4, type: 'spouse', from: 2010, till: null },
{ id: 3, person1: 1, person2: 3, type: 'parent' },
{ id: 3, person1: 2, person2: 3, type: 'parent' }
]
};
function getParents( person ) {
return familyDb.relations.filter( relation => relation.person2 === person.id && relation.type === 'parent' );
}
function getChildren( person ) {
return familyDb.relations.filter( relation => relation.person1 === person.id && relation.type === 'parent' );
}
console.log( getParents( familyDb.persons[2] ) );
console.log( getChildren( familyDb.persons[0] ) );
So the above code kinda takes a rudimentary approach to this, you have:
a unique id identifying a person (name matching would be hard in your example as Grace is both the mom of Charlie & Charlie Jr)
a table identifying a relation of some type between two persons
After that you just need a way to look up the information from your dataset and you have a way to get started

Vue cannot read property of null while using v-for

Is there a way to solve an error which says id not defined on v-bind:key="persons.id" ?
My View
<div v-for="reservation in reservationNameByTime" v-bind:key="reservation.id">
{{reservation.id}} /** displays 1 **/
<div v-for="player in reservation.Players" v-bind:key="player.id">
{{player.id}} /**displays 1 **/
<div v-for="persons in player.Person" v-bind:key="persons.id"> /** throws an error as id of null **/
{{persons.name}}
</div>
</div>
</div>
JSON DATA
reservationNameByTime: [
{
id: 1, /** defines reservation id **/
Players: [
id: 1, /** defines players id **/
Person:{
id: 1, /** defines the person id **/
name: John
}
]
}
]
Image for array data
<div v-for="(reservation, i) in reservationNameByTime" v-bind:key="i">
{{reservation.id}} /** displays 1 **/
<div v-for="(player, j) in reservation.Players" v-bind:key="j">
{{player.id}} /**displays 1 **/
<div v-for="(persons, k) in player.Person" v-bind:key="k">
{{persons.name}}
</div>
</div>
</div>
Your data is malformed, try this with the html code in your post
reservationNameByTime: [{
id: 1,
Players: [{
id: 1,
Person: [{
id: 1,
name: 'John'
},
{
id: 2,
name: 'Marc'
}]
}]
}]
But this (below) is better, for each reservation, you have an id and a list of players, player have id and name
reservation: [{
id: 1,
players: [{
id: 21,
name: 'John'
},
{
id: 55,
name: 'Marc'
}]
},
{
id: 2,
players: [{
id: 34,
name: 'Adrien'
},
{
id: 12,
name: 'Marion'
}]
}]
HTML / VUE
<div v-for="reservation in reservations" v-bind:key="reservation.id">
{{reservation.id}}
<div v-for="player in reservation.players" v-bind:key="player.id">
{{player}}
</div>
</div>
player.Person is an object and v-for on an object iterates through the properties of the object and returns its values. In this case it would be 1 and John. So you're trying to get 1.id and John.id.
If you're only going to have one person, you could just do:
div v-bind:key="player.Person.id">
{{player.Person.name}}
</div>

ReactJS two way binding not working, somewhere wrong with binding

I'm new to reactJs, I'm not sure where it went wrong.
I suppose there is something wrong with binding input. I suppose, cant change input because of value={detail.name}. However, even though I have deleted value={detail.name}, Name: {detail.name} still keeps the original value.
Could somebody give me a hint?
import React, { Component } from "react";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: [
{ id: 1, name: "Tom", age: "20" },
{ id: 2, name: "zhunan", age: "22" },
{ id: 3, name: "kobe", age: "35" }
]
};
}
changeName(event) {
this.setState({
name: event.target.value
});
}
onDelete() {}
render() {
return (
<div>
<ul>
{this.state.details.map((detail, index) => (
<li key={index}>
Name: {detail.name} | age: {detail.age}
<input
style={{ marginLeft: "10px" }}
type="text"
onChange={this.changeName.bind(this)}
value={detail.name}
/>
</li>
))}
</ul>
</div>
);
}
}
export default App;
I updated the code a bit.
First of all, I moved the binding of the callback to the constructor (to have ONE callback instead of one per item*render)
I also changed the key used in the map to be the id, rather than the index of the current item.
Try, it, I hope it works for you.
import React, { Component } from "react";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
details: [
{ id: 1, name: "Tom", age: "20" },
{ id: 2, name: "zhunan", age: "22" },
{ id: 3, name: "kobe", age: "35" }
]
};
this.changeName = this.changeName.bind(this);
}
changeName(event) {
const {target} = event;
const id = Number(target.dataset.id);
const { details } = this.state;
this.setState({
details: details.map((detail) => {
if (detail.id === id) {
return {
...detail,
name: target.value,
}
}
return detail;
}),
});
}
onDelete() {}
render() {
return (
<div>
<ul>
{this.state.details.map(({ id, age, name }) => (
<li key={id}>
Name: {name} | age: {age}
<input
style={{ marginLeft: "10px" }}
type="text"
onChange={this.changeName}
data-id={id}
value={name}
/>
</li>
))}
</ul>
</div>
);
}
}
export default App;
Your code works fine, nothing wrong with the input data binding. The problem is you're setting the name property directly to the state object. That would make it go from this:
this.state = {
details: [
{ id: 1, name: "Tom", age: "20" },
{ id: 2, name: "zhunan", age: "22" },
{ id: 3, name: "kobe", age: "35" }
]
}
To this:
this.state = {
details: [
{ id: 1, name: "Tom", age: "20" },
{ id: 2, name: "zhunan", age: "22" },
{ id: 3, name: "kobe", age: "35" }
],
name: "Bob"
}
Which has no effect on how the component gets rendered. To properly change the name of one of the details, which is what I assume you want, you also need to do a find that detail object to modify. Like this:
changeName(e, target_detail) {
this.setState({
// always update the WHOLE STATE OBJECT! using a map
details: this.state.details.map(detail => {
// the detail we want to modify has the same ID
if(detail.id === target_detail.id) {
// modify the name value of only that
target_detail.name = e.target.value
}
})
});
}
render method:
render() {
return (
<div>
<ul>
{this.state.details.map((detail, index) => (
<li key={index}>
Name: {detail.name} | age: {detail.age}
<input
style={{ marginLeft: "10px" }}
type="text"
// arrow functions implicitly "bind" the current this context:
onChange={e => this.changeName(e, detail)}
value={detail.name}
/>
</li>
))}
</ul>
</div>
);
}

problem with infinite loop in function hook useEffect

I have a problem with an infinite loop on my hook,
I'm passing the data of the local JSON breakfast. If you are iterating with map the data and I am taking it to paint a menu of buttons.
If I remove the data at the end of the function, and leave the empty array, it sends me the following error:
const BreakfastComponent = () => {
const breakfast = [
{
id: 0,
name: "Sandwich de jamón y queso",
price: '35',
img: "https://i.ibb.co/ynC9xHZ/sandjc.png",
},
{
id: 1,
name: "Jugo Natural",
price: '15',
img: "https://i.ibb.co/8mrd4MK/orangejuice.png",
},
{
id: 2,
name: "Café americano",
price: '20',
img: "https://i.ibb.co/nsj1GL0/americancoffe.png",
},
{
id: 3,
name: "Café con leche",
price: '28',
img: "https://i.ibb.co/GRPBm7j/coffeandmilk.png",
}
];
const [stateProduct, setStateProduct] = useState([ ]);
useEffect(() => {
setStateProduct(breakfast);
}, [breakfast]);
return (
<section className="databreakfast">
{stateProduct.map((element, item) =>
<ButtonsBComponent
key={item}
{...element}
/>
)}
</section>
)
};
export default BreakfastComponent;
React Hook useEffect has a missing dependency: 'breakfast'. Either include it or remove the dependency array react-hooks/exhaustive-deps
The problem is simple, you have breakfast array as a dependency to useEffect and in useEffect you are setting the breakfast array itself. Now since the const breakfast array is declared inside the component, a new reference to it is generated everytime and since useEffect sets the array in state, it re-renders and on next re-render the comparison for breakfast array fails since the reference has changed.
The solution is simple, you don't need to have the const array in the component and also you don't need to use useEffect
const breakfast = [
{
id: 0,
name: "Sandwich de jamón y queso",
price: '35',
img: "https://i.ibb.co/ynC9xHZ/sandjc.png",
},
{
id: 1,
name: "Jugo Natural",
price: '15',
img: "https://i.ibb.co/8mrd4MK/orangejuice.png",
},
{
id: 2,
name: "Café americano",
price: '20',
img: "https://i.ibb.co/nsj1GL0/americancoffe.png",
},
{
id: 3,
name: "Café con leche",
price: '28',
img: "https://i.ibb.co/GRPBm7j/coffeandmilk.png",
}
];
const BreakfastComponent = () => {
const [stateProduct, setStateProduct] = useState(breakfast);
return (
<section className="databreakfast">
{stateProduct.map((element, item) =>
<ButtonsBComponent
key={item}
{...element}
/>
)}
</section>
)
};
export default BreakfastComponent;
useEffect's second argument is an array of state/hooks to be watched and when they change, to run the effect. Since your breakfast is a const, I'm guessing you just want the original stateProduct to be breakfast. So instead of using [] as the default, use breakfast.
const [stateProduct, setStateProduct] = useState(breakfast);
Also, probably a good idea to declare const breakfast ... outside of your react functional component so it doesn't re-declare it on every re-render.

Categories

Resources