Dynamically Delete Elements - javascript

I'm having trouble deleting elements. Instead of deleting a specific element, it only deletes the last newly created element. I'm not sure where I'm going wrong here. I referenced this tutorial that shows what I kinda want to do. (I'm new to React)
import React,{useState, useRef} from "react";
const Body = () => {
const [list, setList] = useState([]);
const AddInput = () => {
setList([...list, {placeholder:"Class Name"}]);
};
const DeleteInput = (index) => {
const l = [...list];
l.splice(index,1);
setList(l);
};
const InputChangeHandler = (event, index) => {
const l = [...list];
(l[index]).value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key)=>
<div key={key}>
<input type={"text"} id={key} placeholder={item.placeholder} onChange={e=>InputChangeHandler(e, key)}/>
<button id={key} onClick={() => DeleteInput(key)}>Delete</button>
</div>
)}
</div>
);
}
export default Body;
Element (input fields + button):
Deletes Last Created:

I think the main problem is the key of items that you set as react doc says:
When you don’t have stable IDs for rendered items, you may use the item index as a key as a last resort:
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
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.
As in this Article says:
Reordering a list, or adding and removing items from a list can cause issues with the component state, when indexes are used as keys. If the key is an index, reordering an item changes it. Hence, the component state can get mixed up and may use the old key for a different component instance.
What are some exceptions where it is safe to use index as key?
-If your list is static and will not change.
-The list will never be re-ordered.
-The list will not be filtered (adding/removing items from the list).
-There are no ids for the items in the list.
If you set an reliable key in your items with some counter or id generator your problem would solve.
something like this:
export default function App() {
const [list, setList] = useState([]);
const id = useRef({ counter: 0 });
const AddInput = () => {
console.log(id);
setList([...list, { placeholder: "Class Name", id: id.current.counter++ }]);
};
const DeleteInput = (id) => {
setList(list.filter((item, i) => item.id !== id));
};
const InputChangeHandler = (event, index) => {
const l = [...list];
l[index].value = event.target.value;
setList(l);
};
return (
<div>
<button onClick={AddInput}>Add</button>
{list.map((item, key) => (
<div key={item.id}>
<input
type={"text"}
id={key}
placeholder={item.placeholder}
onChange={(e) => InputChangeHandler(e, key)}
/>
<button id={item.id} onClick={() => DeleteInput(item.id)}>
Delete
</button>
</div>
))}
</div>
);
}

Use filter.
const DeleteInput = (index) => {
const l = list.filter((_, i) => i !== index);
setList(l);
};

Pass id to your DeleteInput function and for remove just filter the item list with id
const DeleteInput = (id) => {const filterItemList = list.filter((item) => item.id!== id);setList(filterItemList ); };

Related

Why the filter does not return the list on the initial render?

What I have is a list that was fetched from an api. This list will be filtered based on the input. But at the first render it will render nothing, unless I press space or add anything to the input. Another solution is set the fetched data to the filteredList. But I don't know if it is the right thing to set the fetched data to two arrays.
import React, { useState, useEffect } from "react";
const PersonDetail = ({ person }) => {
return (
<div>
Id: {person.id} <br />
Name: {person.name} <br />
Phone: {person.phone}
</div>
);
};
const App = () => {
const [personsList, setPersonsList] = useState([]);
const [personObj, setPersonObj] = useState({});
const [showPersonDetail, setShowPersonDetail] = useState(false);
const [newPerson, setNewPerson] = useState("");
const [filter, setFilter] = useState("");
const [filteredList, setFilteredList] = useState(personsList);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((data) => {
setPersonsList(data);
//setFilteredList(data) <-- I have to add this to work
console.log(data);
});
}, []);
const handleClick = ({ person }) => {
setPersonObj(person);
if (!showPersonDetail) {
setShowPersonDetail(!showPersonDetail);
}
};
const handleChange = (event) => {
setNewPerson(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
const tempPersonObj = {
name: newPerson,
phone: "123-456-7890",
id: personsList.length + 1,
};
setPersonsList((personsList) => [...personsList, tempPersonObj]);
//setFilteredList(personsList) <-- to render the list again when add new person
setNewPerson(" ");
};
const handleFilter = (event) => {
setFilter(event.target.value);
const filteredList =
event.target.value.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(event.target.value.toLowerCase())
)
: personsList;
setFilteredList(filteredList);
};
return (
<div>
<h2>List:</h2>
Filter{" "}
<input value={filter} onChange={handleFilter} placeholder="Enter" />
<ul>
{filteredList.map((person) => {
return (
<li key={person.id}>
{person.name} {""}
<button onClick={() => handleClick({ person })}>View</button>
</li>
);
})}
</ul>
<form onSubmit={handleSubmit}>
<input
placeholder="Add Person"
value={newPerson}
onChange={handleChange}
/>
<button type="submit">Add</button>
</form>
{showPersonDetail && <PersonDetail person={personObj} />}
</div>
);
};
export default App;
Your filtered list is actually something derived from the full persons list.
To express this, you should not create two apparently independent states in this situation.
When your asynchronous fetch completes, the filter is probably already set and you are just setting personsList which is not the list you are rendering. You are rendering filteredList which is still empty and you are not updating it anywhere, except when the filter gets changed.
To avoid all of this, you could create the filtered list on each rendering and — if you think this is not efficient enough — memoize the result.
const filteredList = useMemo(() =>
filter.length > 0
? personsList.filter((person) =>
person.name.toLowerCase().includes(filter.toLowerCase())
)
: personsList,
[filter, personsList]
);
When the filter input gets changed, you should just call setFilter(event.target.value).
This way, you will always have a filtered list, independent of when your asynchronous person list fetching completes or when filters get updated.
Side note: Writing const [filteredList, setFilteredList] = useState(personsList); looks nice but is the same as const [filteredList, setFilteredList] = useState([]); because the initial value will be written to the state only once, at that's when the component gets initialized. At that time personsList is just an empty array.

filter items based on multiple properties

I am trying to filter an array of objects based on both title and description.
It is working fine for a single item, how can I filter items based on both title and description.
Here is my code.
import * as React from "react"
const items = [
{
title: "React",
description:
"React (also known as React.js or ReactJS) is a JavaScript library for building user interfaces. It is maintained by Facebook and a community of individual developers and companies."
},
];
export default function App () {
const [searchTerm, setSearchTerm] = React.useState("");
const inputRef = React.useRef();
React.useEffect(() => {
inputRef.current.focus();
}, []);
const handleChange = event => {
setSearchTerm(event.target.value);
};
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<>
<header>
<input
type="text"
placeholder="Type to filter..."
value={searchTerm}
onChange={handleChange}
ref={inputRef}
/>
</header>
<ul>
{filteredNames.map(({title, description}, i) => (
<li key={i} className="box">
<h1>{title}</h1>
<p>{description}</p>
</li>
))}
</ul>
</>
)
}
I tried with filtered chaining
const filteredNames = items
.filter((entry) =>
entry.title.toLowerCase().includes(searchTerm.toLowerCase())
).filter((entry) =>
entry.description.toLowerCase().includes(searchTerm.toLowerCase())
)
However, this not working.
Your approach is the same as using an AND conditional. The first filter returns only title matches and the second returns title matches that also have description matches. You want OR instead
You can use Array#some() as an OR conditional
const filteredNames = items.filter(({title,description}) =>
const term = searchTerm.toLowerCase();
return [title, description].some(str => str.toLowerCase().includes(term));
})
Filter function expects a boolean query, and will run through the iterable list, and return every item that will return true for that boolean query.
const filteredNames = items.filter(item => {
return item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.description.toLowerCase().includes(searchTerm.toLowerCase())
})

How to filter an item out of an array in state in React functional component (To Do List)

I am trying to get a to do list item to disappear when I click on it. Everything works except my deleteHandler method, what logic do I need to filter out the item clicked?
import React, { useState } from 'react';
const ToDoList = () => {
const [list, setList] = useState([])
const [item, setItem] = useState("")
const submitHandler = (e) => {
e.preventDefault();
let newList = list.concat(item)
setList(newList);
console.log(item)
e.target.reset();
}
const deleteHandler = (index) => {
console.log(list[index])
// console.log(list)
const newList = list.filter((item) => list.indexOf(item) !== item[index])
console.log(newList)
setList(newList)
}
return(
<div>
<h1>To Do List:</h1>
<form onSubmit={submitHandler}>
<input type='text' onChange={(e)=>setItem(e.target.value)}></input>
<input type='submit' value='Add'></input>
</form>
{list.map((listItem, index) => (
<div key={index} >
<p className="toDoItem" onClick={()=>deleteHandler(index)}>{listItem}</p>
</div>
))}
</div>
)
}
export default ToDoList;
You can solve this by filtering out all the items that don't have that index:
const newList = list.filter((i, itemIndex) => index !== itemIndex)
Note that in a more robust situation, you might want to assign each to do item an ID. Then, you could filter based on that idea. But, for this basic scenario, doing the filter based on index works fine.
Here is the logic that you should try
const deleteHandler = (index) => {
// Assuming that the index means the index of an item which is to be deleted.
const newList = list.filter((item) => list.indexOf(item) !== index);
setList(newList);
}
Change your deleteHandler to something like below:
const deleteHandler = (index) => {
setList(list.filter((i,Id)=> Id !== index))
}

How to update the UI when the value of the array changes

I am displaying the name of each skid by iterating the skidList array. I have also provided a button "copy" which calls insertSkid when clicked and "delete" which calls deleteSkid. In insertSkid I am updating the skidList array. However, the Modal.Body doesn't update the skid names on changes to the skidList array.
Here's my code.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
let newSkid = {};
newSkid[name] = "PEN";
let newSkidList = [];
newSkidList.push(newSkid);
setSkidList(newSkidList);
const insertSkid = skid => {
let newSkidList = skidList;
newSkidList.push(skid);
setSkidList(newSkidList);
console.log("Added New Skid" + skidList.length);
};
const deleteSkid = (index) => { setSkidList([...skidList.splice(index, 1)]); }
return (
<Modal
backdrop="static"
show={true}
centered
dialogClassName={"createnewskid-modal"}
>
<Modal.Body>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<Button onClick={insertSkid.bind(this,skid)}>copy</Button>
<Button onClick={()=> deleteSkid(index)}>delete</Button>
<Divider />
</div>
))}
</Modal.Body>
</Modal>
)
}
There are several issues with the code your presenting. As previously stated, the useEffect() hook would be useful here to handle the initialization of the skid array:
useEffect(()=>{
let newSkid = {name: "PEN"};
setSkidList([...skidList, newSkid])
},[])
// The empty array second param tells the component to only execute the above code once on mount
Note: The spread operator (...) is used here to generate a new list based on skidList to which we are appending newSkid.
Next, you should use the same method just described to update your list in the insertSkid function:
const insertSkid = (skid) =>{
setSkidList([...skidList, skid])
}
Finally, I would suggest instead of binding you function, use a anonymous function inside the onclick prop:
onClick = {() => insertSkid(skid)}
if you want to setSkidList(newSkidList), do it inside useEffect.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(()=> {
let newSkid = {};
newSkid[name] = "PEN";
setSkidList([newSkid]);
}, [])
//...
}
Call setSkidList out of the useEffect block will cause multiple state updates
It because an array is available by link, and for React skidList after push was not change. Try:
const insertSkid = useCallback((skid) => {
setSkidList([...skidList, skird]);
}, [skidList]);
you can use useEffect for inital data load, and for update you can use prev to get last state value and update yours and do the return, its not needed you need to add more variable assignment.
const CreateNewTab = () => {
const [skidList, setSkidList] = useState([]);
useEffect(() => {
const newSkid = {[name] : "PEN"};
setSkidList([newSkid]);
}, [])
const insertSkid = skid => {
setSkidList(prev => [...prev, skid]);
};
return (
<div>
{skidList.flatMap((skid, index) => (
<div>
{skid[name]}
<button onClick={insertSkid.bind(this,skid)}>copy</button>
<hr />
</div>
))}
</div>
)
}

Recommendation on how to update an array if the index changes

I want to update array elements and I´m using the index to reference the position. the problem is that the index change while searching for names (keyword) it basically sets the name to the wrong element in the users array (because is taking the indes from the filtered users array)
const [users, setUsers] = useState(["John", "Marty", "Mary", "Johanna"]);
const [keyword, setKeyword] = useState("")
const updateName = (index, name) => {
const newState = [...users];
newState[index] = name;
setNames(newState);
};
I have an input field to search for names
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="search"/>
and then I render a child component with each name and I pass a prop to update the name
users
.filter((user) =>
user.toLowerCase().includes(keyword.toLowerCase())
)
.map((user, index) => (
<User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))
My User component
const User (props) => <button onClick={props.updateName(index, "some name")}>{props.user}</button>
this works perfectly fine. except when keyword changes. because the users.map will change and obviously will change the index. but the problem is that I´m using the index to update the array elements.
for example if I search for the "Ma" keyword. 2 names match so the index of the filtered users will change to 0, 1 but the users array is still the same.
How can I solve this problem? thank you.
If you want to keep your current data structure, you could just forgo the filter and do the filtering within your map function by only conditionally rendering the User component. This way, you don't lose accounting of your indexes.
users
.map((user, index) => (
user.toLowerCase().includes(keyword.toLowerCase()) && <User
user={user}
updateName={updateName}
index={index}
key={index}
/>
))
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const names = ["John", "Marty", "Mary", "Johnna"];
// create a map of user objs to hold the position and name of each user
const mappedUsers = names.map((name, i) => {
return { position: i, name };
});
const [users, setUsers] = useState(mappedUsers);
const [keyword, setKeyword] = useState("");
const updateName = (position, name) => {
setUsers(prevUsers => {
prevUsers[position].name = name;
// return a new merged array of users
return [...prevUsers];
});
};
const User = ({ updateName, user }) => (
<div>
<button onClick={() => updateName(user.position, "someName")}>
{user.name}
</button>
</div>
);
const UserList = ({ users, keyword }) => {
return users
.filter(user => user.name.toLowerCase().includes(keyword.toLowerCase()))
.map(user => (
<User key={user.position} updateName={updateName} user={user} />
));
};
return (
<>
<input
value={keyword}
onChange={e => setKeyword(e.target.value)}
placeholder="search"
/>
<UserList users={users} keyword={keyword} />
</>
);
}
I suggest you stop using index for keeping track of positions of items in an array as this will most likely cause issues when items get deleted or added. Instead, you should create a unique identifier for each item and keep track of them that way.
Here's an example of one way you could go about this:
function MyComponent() {
const initialUsers = ["John", "Marty", "Mary", "Johanna"].map(
(name, idx) => {
return { id: idx + 1, name };
}
);
const [users, setUsers] = React.useState(initialUsers);
const [keyword, setKeyword] = React.useState("");
const handleOnChange = event => {
const { value } = event.target;
const nextUsers = initialUsers.filter(user =>
user.name.toLowerCase().includes(value)
);
setUsers(nextUsers);
setKeyword(value);
};
return (
<div>
<p>Search for a name:</p>
<input
type="text"
onChange={handleOnChange}
value={keyword}
placeholder="Type..."
/>
<ul>
{users.map(user => (
<li>
[{user.id}] {user.name}
</li>
))}
</ul>
</div>
);
}
Here's a working example so that you can test it:
CodeSandbox
Hope this helps.

Categories

Resources