How to remove object from Array in ReactJS after click event - javascript

I'm building little app in React and I have a problem to remove and object from Array after I click on icon.
I have form with inputs and when user fill this inputs and click on a button then inputs will into an array. I have array in a state and the default is empty array.
const [attendees, setAttendees] = useState([]);
I have a function for delete a object from array after I click on icon
const deleteAttendee = (attendee) => {
setAttendees({
attendees: attendees.filter(
(element) => element.firstName !== attendee.firstName
),
});
};
but there is a problem when I clicked on icon I get an error. That map is not a function.
I have in another component map function for rendering each user from the inputs
<div className="attendee-container">
{attendees.map((_attendee, index) => {
return (
<div key={index}>
<div onClick={() => deleteAttendee(_attendee)}>{trashAltIcon}</div>
<h3>
{index + 1}. {_attendee.firstName} {_attendee.lastName} (
{_attendee.age})
</h3>
</div>
);
})}
</div>
Thank you in advance for your help.

When using setState you pass in the full state object like you'r doing. However when using useState hooks the setState functions takes in just that value.
Check out these docs for understanding the difference between setState and updating state with useState hook
setAttendees takes in an array so your handler should be
const deleteAttendee = (attendee) => {
//Pass in filtered array as new state for attendees
setAttendees( attendees.filter((element) => element.firstName !== attendee.firstName);
};

setAttendees comes with a parameter that allows you to have the previous state.
Here's what I did to your deleteAttendee function
const deleteAttendee = (event) => {
const attendeeId = event.target.dataset.id;
setAttendees((previousState) => previousState.filter((attendee) => attendee.id !== attendeeId))
};
I am using the previous state and updating that instead and instead of removing the first name I am instead removing matching ids
Here's what the jsx looks like
return (
<div className="App">
{attendees.map((_attendee, index) => {
return (
<div key={_attendee.id}>
<div data-id={_attendee.id} onClick={(event) => deleteAttendee(event)}>trash</div>
<h3>
{index + 1}. {_attendee.firstName} {_attendee.lastName} (
{_attendee.age})
</h3>
</div>
);
})}
</div>
);
I am using a data-id attribute so whenever the user clicks on the trash icon, I can pass that event into the deleteAttendee, get the value from the attribute and filter the array out if it matches. I made this change because I saw that you was filtering based on names, what if two attendees had the same name?
Initial state of attendees
const [attendees, setAttendees] = useState([
{
id: 'tskdh3-3dfdgg-23ds-23dfsdd-2332',
firstName: 'bob',
lastName: 'jeo',
age: 73
},
{
id: 't3-3dfdgg-23ds-23dfsdd-2332',
firstName: 'bob',
lastName: 'woah',
age: 43
},
{
id: 'bsd233-33dfg-3ds-2f3fsdd-2dfdf',
firstName: 'bob',
lastName: 'yum',
age: 23
}
]);
For random ids to be generated, you an use an npm package called uuid, it generates unique ids.

Related

Updating deeply nested state with useState not working properly

I followed the answer in this thread to try to update my deeply nested object in React.
React: Setting State for Deeply Nested Objects w/ Hooks
What seems to work like a charm there, will somehow break for me when doing the following:
I have a table populated with items from an array defined like so:
const [items, setItems] = useState([
{
selected: false,
title: 'Item 1',
status: 'new'
},
{
selected: false,
title: 'Item 2',
status: 'used'
},
]);
When selecting an item from that list this function gets called to update selected variable for the object with the index i like so:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
prevState[i].selected = !prevState[i].selected;
return [...prevState];
});
};
This will work exactly once. If I trigger select a second time or any time after that return [...prevState] somehow keeps returning the state unchanged. (selected stays true forever). I can't solve this.
items is attached to a component List like so:
<List
items={items}
/>
and inside List (shortened code):
{items.map((item, i) => {
return (
<tr className="list-table-tr">
{hasSelector ? (
<td className="list-table-td-selector">
{item.selected ? (
<div
data-index={i}
className="global-selector-selected"
onClick={select}
></div>
) : (
<div
data-index={i}
className="global-selector-unselected"
onClick={select}
></div>
)}
</td>
) : null}
You're breaking one of the primary rules of React state: You're modifying a state object directly, rather than making a copy.
To correctly do the update, you'd do this:
const select = (e) => {
const i = e.target.getAttribute('data-index');
setItems((prevState) => {
// Copy the array (your code was doing that)
const update = [...prevState];
const item = update[i];
// Copy the object (your code wasn't doing that) and update its
// `selected` property
update[i] = {...item, selected: !item.selected};
return update;
});
};
Note how both the array and the object are copied, rather than just the array.

Why we are making copy of reference types in react before we mutate them?

I am new in react world. I have this example code here where with the deletePersonHandler method i am deleting some objects from the array.
class App extends Component {
state = {
persons: [
{ name: "peter", age: 24 },
{ name: "john", age: 25 },
{ name: "jake", age: 30 }
],
showPersons: true
}
deletePersonHandler = index => {
const persons = this.state.persons;
persons.splice(index,1);
this.setState({persons:persons})
}
togglePersonsHandler = () => {
this.setState({ showPersons: !this.state.showPersons })
}
render() {
let persons = null;
if (this.state.showPersons) {
persons = (
<div>
{this.state.persons.map((person, index) => {
return <Person
click={() => this.deletePersonHandler(index)}
name={person.name}
age={person.age}
key={index}
/>
})}
</div>
);
}
return (
<div className="App">
{persons}
<button onClick={this.togglePersonsHandler}>Click to hide/show</button>
</div>
)
}
}
export default App;
and it is working fine.
My question is:
when this works - without making copy in this case on persons
deletePersonHandler = index => {
const persons = this.state.persons;
persons.splice(index,1);
this.setState({persons:persons})
}
why the recommended way is to make the COPY FIRST AND THEN MODIFY THE REFERENCE TYPE ?
deletePersonHandler = index => {
const persons = [...];
persons.splice(index,1);
this.setState({persons:persons})
}
I am explaining this based on my experience with object in JavaScript. I had one publisher and subscriber code where there was an array of object which used to keep track of some message number and their handler like this
let observer = {"8000":[handler1, handler2]};
So when something happens i publish 8000 message and all handlers get executed like this
for(var item in observer[8000]){
//execute handler/
}
. Till here it was working pretty cool. Then I started removing handler when it has been processed. So after removing handler length of array observer[8000] reduced by 1. So in next sequence it could not find next handler which didn't execute(Objects are pass by reference in JavaScript). So to resolve this I had to make a copy of array object before directly modifying this. In short if object has many dependencies then before processing make copy of it then process or if it is used only single place then use in place processing. It depends on situation, there aren't any strict rules to follow like copy then process.

Having trouble incrementing the age when the button is clicked

New to react and I am having a hard time trying to increment the age of the person when the button is clicked.
EventComp.js code:
import React, { Component } from 'react';
class EventComp extends Component{
constructor(props) {
super(props);
this.state = {
clicks: 0,
show: true,
};
}
IncrementItem = () => {
this.setState({clicks: this.state.clicks + 1});
}
render(){
return(
<div>
<div class = "person">
<h1 class="person_h1">
{this.props.LastName},
{this.props.FirstName}</h1>
<p>Age: {this.props.Age}</p>
</div>
<button onClick = {this.IncrementItem}>Celebrate</button>
</div>
)
}
}
export default EventComp;
App.js code:
import React from 'react';
import EventComp from './components/EventComp';
import './App.css';
function App() {
return (
<div class ="main">
<div>
<EventComp FirstName="Jane "
LastName=" Doe " Age = {45}/>
</div>
<div>
<EventComp FirstName = "Joe " LastName = " Doe " Age={88}/>
</div>
<div>
</div>
</div>
);
}
export default App;
I'm just confused on how it's supposed to work. There are no errors, but when the button is clicked nothing happens.
…having a hard time trying to increment the age…
A common way to do this sort of thing is to have a parent component and a child component (App and EventComp) with responsibilities separated such that the parent manages the state (keeps track of the age) and the child (EventComp) is a "dumb" component that just renders whatever information it's given.
You're already doing most of this by passing the "person" information as props:
<EventComp FirstName="Joe" LastName="Doe" Age={88} /> // 👍
If you keep track of the age as state in App, then changing "age" will re-render, passing the updated age to EventComp:
function App () {
// create a state variable and a setter function for updating its value
const [age, setAge] = React.useState(88);
// pass age as a prop:
return (
<EventComp FirstName="Joe" LastName="Doe" Age={age} />
);
}
How does age get updated? By calling setAge. Here's the same example from above with the addition of a button that increments age:
function App () {
const [age, setAge] = React.useState(88);
return (
<div>
<button onClick={() => setAge(age + 1)}>Increment Age</button>
<EventComp FirstName="Joe" LastName="Doe" Age={age} />
</div>
);
}
Each click will trigger a re-render showing the new age.
But you want the button in EventComp not in App. How do we connect the button in EventComp to the state in App? By passing the update function as a prop:
function App () {
const [age, setAge] = React.useState(88);
return (
<EventComp
FirstName="Joe"
LastName="Doe"
Age={age}
onIncrementAge={() => setAge(age + 1)} // <= pass the age incrementing function as a prop
/>
);
}
function EventComp (props) {
return (
<div>
<div class = "person">
<h1 class="person_h1">
{props.LastName},
{props.FirstName}</h1>
<p>Age: {props.Age}</p>
</div>
<button onClick={props.onIncrementAge}> // <= onClick calls the function provided via props
Celebrate
</button>
</div>
)
}
Using this pattern, there's a single source of truth about the value of age, and EventComp doesn't need to know or care where it comes from.
Extending this to handle more than one person:
The examples above demonstrate the fundamental pattern but don't address the need to handle more than one person. Suppose you have a list of people:
const people = [
{
firstName: 'Jane',
lastName: 'Doe',
age: 45
},
{
firstName: 'Joe',
lastName: 'Doe',
age: 88
},
]
You can iterate over this list, rendering a component for each person:
function App () {
// initial state with 2 people
const [people, setPeople] = React.useState([
{ firstName: 'Jane', lastName: 'Doe', age: 45 },
{ firstName: 'Joe', lastName: 'Doe', age: 88 },
]);
return (
people.map((person) => ( // render an EventComp for each person
<EventComp
FirstName={person.firstName}
LastName={person.lastName}
age={person.age}
key={person.firstName} // key is a react thing. ignore for now.
/>
));
)
}
Updating a person's age becomes slightly more complicated, but it's still fairly straightforward. We need a function that does the following:
Find the person by index (their position in the list)
Update their age
Update state with the new info
const incrementAge = index => {
const person = people[index];
person.age += 1;
setPeople([...people]);
}
Pass this new function to EventComp and you're done.
function App () {
// initial state with 2 people
const [people, setPeople] = React.useState([
{ firstName: 'Jane', lastName: 'Doe', age: 45 },
{ firstName: 'Joe', lastName: 'Doe', age: 88 },
]);
const incrementAge = index => {
const person = people[index];
person.age += 1;
setPeople([...people]); // spread to a new array so react recognizes it as new
}
return (
people.map((person, index) => ( // render an EventComp for each person
<EventComp
FirstName={person.firstName}
LastName={person.lastName}
age={person.age}
onIncrementAge={() => incrementAge(index)} // <= indicate which person via index
key={person.firstName} // key is a react thing. ignore for now.
/>
));
)
}
Notice that we've added the index argument to the map call:
people.map((person, index) => {
And we're using it when we invoke the click handler:
onIncrementAge={() => incrementAge(index)}
Voila.
You could also pass the person as a single prop:
<EventComp
person={person}
onIncrementAge={() => incrementAge(index)}
/>
// use props.person.FirstName, etc. in EventComp
Hope this helps.
You're not updating age in your IncrementItem function. Instead, you're updating clicks. The reason you're not seeing anything happening is because clicks is not rendered anywhere. If you add a log statement and log clicked, you will see it being updated.
But if you are interested in incrementing age, you first need to add state to age:
this.state = {
clicks: 0,
show: true,
age: this.props.Age
};
As a note, in the future, props are usually lowercase and I would suggest having that prop be passed in as "age" instead.
The second thing you should do in your increment function is to update the age rather than clicks. If you add a console statement, you will see that the function is actually being invoked. You will also notice that age is being incremented rather than clicks because we are interested in that piece of state. The function should also be defined where the state is defined to retain context.
Note: functions in react are lowercased, components are capitalized. Functions also tend to use a "handle" naming convention, in this case, the function should be named something like "handleIncrementAge"
IncrementItem = () => {
this.setState({clicks: this.state.clicks + 1,
age: this.state.age + 1
});
}
Lastly, in your render, because it seems like you are interested in updating state, you want to display that piece of state.
<p>Increment Age: {this.state.age}</p>
*class attributes should be changed to className because class is a reserved keyword in React. Not doing so will result in your styles not being rendered.
I have attached a CodeSandbox with your code and the recommended changes: Code Sandbox
Updating State with React Class Components

onClick on "a" tag not working properly if it has some child elements in react

Below is the dynamic form which I created. It works fine. but Inside "a" tag if i add some child element to "a" tag, onClick event on "a" tag does not execute properly and pass proper name.
import React, { Component } from "react";
import "./stylesheet/style.css";
export default class Main extends Component {
state = {
skills: [""]
};
dynamicInputHandler = (e, index) => {
var targetName = e.target.name;
console.log(targetName);
var values = [...this.state[targetName]];
values[index] = e.target.value;
this.setState({
[targetName]: values
});
};
addHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
let values = [...this.state[targetName]];
values.push("");
this.setState({
[targetName]: values
});
};
removeHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
console.log(e.target.name);
let values = [...this.state[targetName]];
values.splice(index, 1);
this.setState({
[targetName]: values
});
};
render() {
return (
<div>
<form className="form">
{this.state.skills.map((value, index) => {
return (
<div className="input-row row">
<div className="dynamic-input">
<input
type="text"
placeholder="Enter skill"
name="skills"
onChange={e => {
this.dynamicInputHandler(e, index);
}}
value={this.state.skills[index]}
/>
</div>
<div>
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
</div>
</div>
);
})}
<button
name="skills"
onClick={e => {
this.addHandler(e);
}}
>
Add
</button>
</form>
{this.state.skills[0]}
</div>
);
}
}
i want to add Icon inside "a" tag , after adding icon tag, forms breaks and gives error "TypeError: Invalid attempt to spread non-iterable instance"
This works -
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
This does not (after adding icon inside a tag)
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
<i>Remove</i>
</a>
</span>
Inside removeHandler let targetName = e.target.parentElement.name;. When you wrap Remove in another tag, the event target is now the i tag, for which name is undefined.
I think this mixture of DOM manipulation (getting target of an event) is okay, but in React, where data is preferred over DOM values, you could work entirely on the component state and touch the DOM element values only once in the input where the skill value is displayed. Like this:
class Main extends Component {
state = {
skills: [],
lastSkillAdded: 0
};
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
render() {
return (
<form>
{this.state.skills.map((skill, index) => (
<div key={skill.id}>
<input
type="text"
value={skill.value}
onChange={e => this.updateSkillById(skill.id, e.target.value)}
/>
<button onClick={() => this.removeSkillById(skill.id)}>
Remove
</button>
</div>
))}
<button onClick={() => this.appendEmptySkill()}>Add</button>
</form>
);
}
}
Let's deconstruct what's going on there.
First, the lastSkillAdded. This is a controversial thing, and I guess someone will correct me if I'm wrong, but
when we iterate over an array to render, say, list items, it's recommended that we provide key prop that represents the item key,
and the value of key prop is not recommended to be the index of the array element.
So we introduce an artificial counter that only goes up and never goes down, thus never repeating. That's a minor improvement though.
Next, we have the skills property of component state. It is where all the values are going to be stored. We agree that each skill is represented by an object of two properties: id and value. The id property is for React to mark array items property and optimize reconciliation; the value is actual text value of a skill.
In addition, we have three methods to work on the list of skills that are represented by three component methods:
appendEmptySkill to add an empty skill to the end of the state.skills array,
updateSkillById to update the value of a skill knowing id and new value of it,
and removeSkillById, to just remove the skill by its id from state.skills.
Let's walk through each of them.
The first one is where we just append a new empty skill:
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
Because appending a new skill never depends on any input values, this method takes zero arguments. It updates the skill property of component state:
[...prevState.skills, { id: lastSkillAdded, value: '' }]
which returns a new array with always the same empty skill. There, we also assign the value to id property of a skill to our unique counter.
The next one is a bit more interesting:
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
We agree that in order to change a skill, we need to know its id and the new value. So our method needs to receive these two values. We then map through the skills, find the one with the right id, and change its value. A pattern
(id, value) => array.map(item => item.id !== id ? item : ({ ...item, value }));
is, I believe, a pretty common one and is useful on many different occasions.
Note that when we update a skill, we don't bother incrementing our skill id counter. Because we're not adding one, it makes sense to just keep it at its original value.
And finally, removing a skill:
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
Here, we only need to know the id of a skill to remove it from the list of skills. We filter our state.skills array and remove the one skill that matches the id. This is also a pretty common pattern.
Finally, in render, four things are happening:
existing skills are iterated over, and for each skill, input and "Remove" button are rendered,
input listens to change event and refers to EventTarget#value attribute in updateSkillById function call, because that's the only way to feed data from DOM back into React, and
the "Remove" buttons refers to skill id and calls removeSkillById,
and outside the loop, the "Add" button listens to click events and calls appendEmptySkill whenever it happens without any arguments, because we don't need any.
So as you can see, this way you're in total control of the render and state updates and never depend on DOM structure. My general advice is, if you find yourself in a situation when DOM structure dictates the way you manage component or app state, just think about a way to separate DOM from state management. So hope this answer helps you solve the issue you're having. Cheers!

Uncaught TypeError: Cannot read property of null, removing element from array javascript

I am trying to remove an element from an array when the onClick on the remove button is called, when the button is clicked I am getting Uncaught TypeError: Cannot read property 'name' of null
Why am I getting this error?
removeData(key) {
console.log(key);
const data = this.state.data;
data[key] = null;
this.setState({ data });
}
renderData(key){
const user = this.props.data[key];
console.log(user.name);
console.log(user.id);
return(
<div key={key}>
<li> <strong> Name: </strong> {user.name},
<strong> ID: </strong> {user.id} </li>
<button onClick={() => this.props.removeData(key)}> Remove </button>
</div>
)
}
Why am I getting this error?
You are explicitly setting data[key] to null:
data[key] = null;
When the component rerenders it presumably calls renderData with the key that was "removed" (because the property is still there, its value is just null). user will be null and accessing null.name throws an error.
Depending on what you actually want, either skip null values in your renderData method, or actually delete the entry with
delete data[key];
Assigning null does not delete a property:
var obj = {foo: 42};
// Doesn't actually remove the property
obj.foo = null;
console.log(obj);
// This removes the property
delete obj.foo;
console.log(obj);
I am trying to remove an element from an array
If you really have an array, then there more things wrong. {...this.state.data} is not an appropriate way to clone an array because you end up with an object.
To properly perform this action on an array you would do
removeData(key) {
// Assuming `key` is the index of the element
// Create copy
const data = Array.from(this.state.data);
// Remove element at index `key`
data.splice(key, 1);
this.setState({ data });
}
Try:
renderData(key){
const user = this.props.data[key] || {}; // change
console.log(user.name);
console.log(user.id);
return(
<div key={key}>
<li> <strong> Name: </strong> {user.name},
<strong> ID: </strong> {user.id} </li>
<button onClick={() => this.props.removeData(key)}> Remove </button>
</div>
)
It seems like your data isn't ready when the component renders, so putting in a blank object will allow the component to render empty while the data loads.
edit: You could consider:
renderData(key){
if (!this.props.data[key]) return null; //change
const user = this.props.data[key];
console.log(user.name);
console.log(user.id);
return(
<div key={key}>
<li> <strong> Name: </strong> {user.name},
<strong> ID: </strong> {user.id} </li>
<button onClick={() => this.props.removeData(key)}> Remove </button>
</div>
)
This would hide the component as the data is being fetched. It's up to you then to communicate that the data is loading to the user. You could pass down a loading prop that is set to true when the request is fired and set to false when the request returns data and show either some loading text or an animation.
Removing objects from arrays
A good way to remove objects from arrays is to use filter. Using filter keeps us safe from bugs that come from using the index inside of the map function. data.map((item,index) => <div key={index} onClick={this.removeData(index)}> {item.fruit} </div>). This can cause serious problems when removing objects from the array.
What react says about this.
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a
key. If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
The Filter Approach
This approach takes whatever id the object has and returns every object that has a different id.
this.setState({ data: temp.filter(item => item.id !== id) });
Working Example
import React, { Component } from 'react';
import { render } from 'react-dom';
class App extends Component {
constructor() {
super();
this.state = {
data:[
{ fruit: "apple", id:1 },
{ fruit: "orange", id:2 },
{ fruit: "pineapple", id:3 },
],
}
}
removeData = (id) => {
const { data } = this.state;
const temp = data.slice();
this.setState({ data: temp.filter(item => item.id !== id) });
}
renderData = () =>{
const { data } = this.state;
return data.map((item) => {
return <div key={item.id}>
<label> {item.fruit} </label>
{item.id}
<button onClick={() => this.removeData(item.id)}> Remove </button>
</div>
})
}
render() {
return (
<div>
{this.renderData()}
</div>
);
}
}
render(<App />, document.getElementById('root'));

Categories

Resources