I have an array of objects. I need to add a function to remove an object from my array without using the "this" keyword.
I tried using updateList(list.slice(list.indexOf(e.target.name, 1))). This removes everything but the last item in the array and I'm not certain why.
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = e => {
updateList(list.slice(list.indexOf(e.target.name, 1)))
}
return (
{list.map(item => {
return (
<>
<span onClick={handleRemoveItem}>x </span>
<span>{item.name}</span>
</>
)}
}
)
Expected behaviour: The clicked item will be removed from the list.
Actual behaviour: The entire list gets removed, minus the last item in the array.
First of all, the span element with the click event needs to have a name property otherwise, there will be no name to find within the e.target. With that said, e.target.name is reserved for form elements (input, select, etc). So to actually tap into the name property you'll have to use e.target.getAttribute("name")
Additionally, because you have an array of objects, it would not be effective to use list.indexOf(e.target.name) since that is looking for a string when you are iterating over objects. That's like saying find "dog" within [{}, {}, {}]
Lastly, array.slice() returns a new array starting with the item at the index you passed to it. So if you clicked the last-item, you would only be getting back the last item.
Try something like this instead using .filter(): codesandbox
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (e) => {
const name = e.target.getAttribute("name")
updateList(list.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span name={item.name} onClick={handleRemoveItem}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
You can use Array.filter to do this in a one-liner:
const handleRemoveItem = name => {
updateList(list.filter(item => item.name !== name))
}
Eta: you'll also need to pass the name of your item in your onClick handler:
{list.map(item => {
return (
<>
<span onClick={() =>handleRemoveItem(item.name)}>x </span>
<span>{item.name}</span>
</>
)}
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]
const [list, updateList] = useState(defaultList);
const handleRemoveItem = idx => {
// assigning the list to temp variable
const temp = [...list];
// removing the element using splice
temp.splice(idx, 1);
// updating the list
updateList(temp);
}
return (
{list.map((item, idx) => (
<div key={idx}>
<button onClick={() => handleRemoveItem(idx)}>x </button>
<span>{item.name}</span>
</div>
))}
)
Small improvement in my opinion to the best answer so far
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const App = () => {
const defaultList = [
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
];
const [list, updateList] = useState(defaultList);
const handleRemoveItem = (item) => {
updateList(list.filter(item => item.name !== name));
};
return (
<div>
{list.map(item => {
return (
<>
<span onClick={()=>{handleRemoveItem(item)}}>
x
</span>
<span>{item.name}</span>
</>
);
})}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Instead of giving a name attribute we just send it to the handle function
I think this code will do
let targetIndex = list.findIndex((each) => {each.name == e.target.name});
list.splice(targetIndex-1, 1);
We need to check name value inside object so use findIndex instead. then cut the object start from target index to 1 array after target index.
Codepen
From your comment your problem came from another part.
Change this view section
return (
<>
<span onClick={() => handleRemoveItem(item) }>x </span>
<span>{item.name}</span>
</>
)}
change function handleRemoveItem format
const handleRemoveItem = item => {
list.splice(list.indexOf(item)-1, 1)
updateList(list);
}
Redundant one liner - would not recommend as hard to test / type / expand / repeat / reason with
<button onClick={() => setList(list.slice(item.id - 1))}
A version without exports:
const handleDeleteItem = id => {
const remainingItems = list.slice(id - 1)
setList(remainingItems);
}
However I would consider expanding the structure of your logic differently by using helper functions in another file.
With that in mind, I made one example for filter and another for slice. I personally like the slice option in this particular use-case as it makes it easy to reason with. Apparently, it is also slightly more performant on larger lists if scaling (see references).
If using slice, always use slice not splice unless you have good reason not to do so as it adheres to a functional style (pure functions with no side effects)
// use slice instead of splice (slice creates a shallow copy, i.e., 'mutates' )
export const excludeItemFromArray = (idx, array) => array.slice(idx-1)
// alternatively, you could use filter (also a shallow copy)
export const filterItemFromArray = (idx, array) => array.filter(item => item.idx !== idx)
Example (with both options filter and slice options as imports)
import {excludeItemFromArray, filterItemFromArray} from 'utils/arrayHelpers.js'
const exampleList = [
{ id: 1, name: "ItemOne" },
{ id: 2, name: "ItemTwo" },
{ id: 3, name: "ItemThree" }
]
const [list, setList] = useState(exampleList);
const handleDeleteItem = id => {
//excluding the item (returning mutated list with excluded item)
const remainingItems = excludeItemFromArray(id, list)
//alternatively, filter item (returning mutated list with filtered out item)
const remainingItems = filterItemFromArray(id, list)
// updating the list state
setList(remainingItems);
}
return (
{list.map((item) => (
<div key={item.id}>
<button onClick={() => handleDeleteItem(item.id)}>x</button>
<span>{item.name}</span>
</div>
))}
)
References:
Don't use index keys in maps: https://robinpokorny.com/blog/index-as-a-key-is-an-anti-pattern/
Performance of slice vs filter: https://medium.com/#justintulk/javascript-performance-array-slice-vs-array-filter-4573d726aacb
Slice documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
Functional programming style: https://blog.logrocket.com/fundamentals-functional-programming-react/#:~:text=Functional%20programming%20codes%20are%20meant,computations%20are%20called%20side%20effects.
Using this pattern, the array does not jump, but we take the previous data and create new data and return it.
const [list, updateList] = useState([
{ name: "ItemOne" },
{ name: "ItemTwo" },
{ name: "ItemThree" }
]);
updateList((prev) => {
return [
...prev.filter((item, i) => item.name !== 'ItemTwo')
]
})
This is because both slice and splice return an array containing the removed elements.
You need to apply a splice to the array, and then update the state using the method provided by the hook
const handleRemoveItem = e => {
const newArr = [...list];
newArr.splice(newArr.findIndex(item => item.name === e.target.name), 1)
updateList(newArr)
}
Related
I have an application using React Hookstate.
For one state I am using an Array. I want to update a single element in this array.
But the updated element always is wrapped inside a proxy tag.
import React from 'react';
import { useState, none, State } from '#hookstate/core';
const Test: React.FC = () => {
const books: State<Array<any>> = useState([
'Harry Potter',
'Sherlock Holmes',
'Swiss Family Robinson',
'Tarzon',
]);
const SwapItems = () => {
// books.merge((p) => ({ 1: p[0], 0: p[1] })); // this way it works.
const reOrderedItem = books.nested(0);
books.nested(0).set(none);
books.set((p) => {
p.splice(0, 0, reOrderedItem);
return p;
});
};
return (
<div>
<ol>
{books.map((book) => {
return <li>{book.get()}</li>;
})}
</ol>
<br />
<button onClick={SwapItems}>Swap Books</button>
</div>
);
};
export default Test;
//error
react-dom.development.js:11393 Uncaught RangeError: Maximum call stack size exceeded.
I'm pretty sure you just need to add .value as follows:
const reOrderedItem = books.nested(0).value;
I'm not sure why you don't prefer the update version that works though -- that seems much simpler.
Alternatively, if you want to use set, why not work with the items directly?
const reOrderedItem = books.nested(0).value;
books.nested(0).set(books.nested(1).value);
books.nested(1).set(reOrderedItem);
Alternatively, you could do the manipulation inside set like so (but you're right that this is inefficient for large arrays):
const SwapItems = () => {
books.set((p) => {
const [a, b, ...rest] = p;
return [b, a, ...rest];
});
};
Actually, I wanted to reorder a certain item on the list.
import React from 'react';
import { useState, State } from '#hookstate/core';
const Test: React.FC = () => {
const books: State<Array<any>> = useState([
'Harry Potter',
'Sherlock Holmes',
'Swiss Family Robinson',
'Tarzon',
]);
const ReorderItems = () => {
books.set((p) => {
const reOrderedItem = p[2];
p.splice(2, 1);
p.splice(0, 0, reOrderedItem);
return p;
});
};
return (
<div>
<ol>
{books.map((book) => {
return <li>{book.get()}</li>;
})}
</ol>
<br />
<button onClick={ReorderItems}>Swap Books</button>
</div>
);
};
export default Test;
I have a collection of my lists in an array called allLists. When I create a new list I add the list to allLists array.
I can only add, update, delete elements from the selectedList.
My question is how do keep allLists updated with the changes made to selectedList.
How do I update a single list in a collection of lists?
Should I update using index of the array or by listName?
Is it best to do this in a useeffect or should i trigger this in a react lifecycle event or is there other ways of doing this?
Codesandbox link:
https://codesandbox.io/s/react-handle-lists-of-lists-92lez?file=/src/App.tsx
import { useEffect, useState } from "react";
import "./styles.css";
export interface ListInterface {
listName: string;
list: string[];
}
export default function App() {
const pokemonList = {
listName: "Pokemon",
list: ["Pikachu", "Onix", "Mew"]
};
const fruitsList = {
listName: "Fruits",
list: ["Apple", "Orange", "Banana"]
};
const numbersList = {
listName: "Numbers",
list: ["One", "Two", "Three"]
};
const [selectedList, setSelectedList] = useState<ListInterface>(numbersList);
const [allLists, setAllLists] = useState<ListInterface[]>([
pokemonList,
fruitsList
]);
const addListToAllLists = () => {
//Add new list to allList
if (selectedList?.listName !== "") {
setAllLists([...allLists, selectedList]);
}
};
const updateAllList = (listName: string) => {
allLists.forEach((list) => {
if (list.listName === listName) {
//set updated list
}
});
};
useEffect(() => {
if (selectedList) {
//addListToAllLists();
}
}, [selectedList]);
const addMultiElementToList = () => {
let newList = ["Four", "Five", "Six"];
setSelectedList({
...selectedList,
list: selectedList.list.concat(newList)
});
};
const addElementToList = () => {
let newElement = "New Element";
setSelectedList({
listName: selectedList.listName,
list: [...selectedList.list, newElement]
});
};
const changeSelectedList = (listName: string) => {
console.log("List", listName);
allLists.forEach((list) => {
if (list.listName === listName) {
console.log("Found List", listName);
setSelectedList(list);
}
});
};
return (
<div className="App">
<h2>Selected List [{selectedList.listName}]</h2>
{selectedList?.list?.map((element) => {
return <p>{element}</p>;
})}
<button onClick={() => addElementToList()}>Add Single Element</button>
<button onClick={() => addMultiElementToList()}>
Add Multiple Element
</button>
<button onClick={() => setSelectedList(numbersList)}>Clear List</button>
<hr></hr>
<h2>All Lists</h2>
<h4>Change Selected List</h4>
{allLists?.map((list) => {
return (
<button onClick={() => changeSelectedList(list.listName)}>
{list.listName}
</button>
);
})}
</div>
);
}
My question is how do keep allLists updated with the changes made to selectedList.
You always want a "single source of truth" for data. selectedList should not be its own state. It is something that is derived from the lists in allLists and the name or id of the current list.
const [allLists, setAllLists] = useState<ListInterface[]>([
pokemonList,
fruitsList,
numbersList
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const selectedList = allLists[selectedIndex];
How do I update a single list in a collection of lists?
You would copy all of the other lists and replace the single list that are modified with a new version. So you need a copy within a copy. The Redux guide to Immutable Update Patterns is a good resource on handling nested updates.
Since you will have multiple similar functions to update the current list, we can remove a lot of repeated code by creating a helper function.
const updateCurrentList = (newList: string[]) => {
setAllLists((lists) =>
lists.map((current, i) =>
i === selectedIndex ? { ...current, list: newList } : current
)
);
};
const addElementToList = () => {
let newElement = "New Element";
updateCurrentList([...selectedList.list, newElement]);
};
const addMultiElementToList = () => {
let newList = ["Four", "Five", "Six"];
updateCurrentList(selectedList.list.concat(newList));
};
Should I update using index of the array or by listName?
I don't feel strongly on this one. Accessing is definitely faster by index as you avoid the need to find(). When you update you always have to do a map() to copy the other lists, so there's not much difference. Technically the number comparison of the indexes is faster than the string comparison of the names, but this is negligible.
The listName would be better if lists are going to be added or removed because the array index might change but the listName won't.
Is it best to do this in a useEffect or should I trigger this in a react lifecycle event or is there other ways of doing this?
If you follow my advice about keeping data in one place only then there are no dependent updates which need to be done in a useEffect. The primary update of the allLists data is triggered by the event.
import { useState } from "react";
import "./styles.css";
export interface ListInterface {
listName: string;
list: string[];
}
export default function App() {
const pokemonList = {
listName: "Pokemon",
list: ["Pikachu", "Onix", "Mew"]
};
const fruitsList = {
listName: "Fruits",
list: ["Apple", "Orange", "Banana"]
};
const numbersList = {
listName: "Numbers",
list: ["One", "Two", "Three"]
};
const [allLists, setAllLists] = useState<ListInterface[]>([
pokemonList,
fruitsList,
numbersList
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const selectedList = allLists[selectedIndex];
// you could make this take a function instead of an array
// to ensure that it always gets the current value from state
const updateCurrentList = (newList: string[]) => {
setAllLists((lists) =>
lists.map((current, i) =>
i === selectedIndex ? { ...current, list: newList } : current
)
);
};
const addElementToList = () => {
let newElement = "New Element";
updateCurrentList([...selectedList.list, newElement]);
};
const addMultiElementToList = () => {
let newList = ["Four", "Five", "Six"];
updateCurrentList(selectedList.list.concat(newList));
};
const clearList = () => {
updateCurrentList([]);
};
return (
<div className="App">
<h2>Selected List [{selectedList.listName}]</h2>
{selectedList?.list?.map((element) => {
return <p>{element}</p>;
})}
<button onClick={addElementToList}>Add Single Element</button>
<button onClick={addMultiElementToList}>Add Multiple Element</button>
<button onClick={clearList}>Clear List</button>
<hr></hr>
<h2>All Lists</h2>
<h4>Change Selected List</h4>
{allLists.map((list, i) => {
return (
<button onClick={() => setSelectedIndex(i)}>{list.listName}</button>
);
})}
</div>
);
}
Code Sandbox Link
Think you can get the answer through this.
https://www.robinwieruch.de/react-state-array-add-update-remove
I am having a onChange function i was trying to update the array options by index wise and i had passed the index to the function.
Suppose if i am updating the options array index 0 value so only that value should be update rest should remain as it is.
Demo
Here is what i tried:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const handleOptionInputChange = (event, idx) => {
setPostData({
...postdata,
LEVEL: {
...postdata.LEVEL.options,
[event.target.name]: event.target.value
}
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
Suppose if i want to add the objects in another useState variable for all the updated options only, will this work?
const posting = {
"optionUpdates": [],
}
const [sentdata , setSentData] = useState(posting);
setSentData({
...sentdata,
optionUpdates: [{
...sentdata.optionUpdates,
displayOrder: event.target.value
}]
})
Basically, you need to spread properly, use callback approach to set state etc.
Change your handler to like this.
Working demo
const handleOptionInputChange = (event, idx) => {
const target = event.target; // with callback approach of state, you can't use event inside callback, so first extract the target from event.
setPostData(prev => ({ // prev state
...prev, // spread prev state
LEVEL: { //update Level object
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => { // you need to loop thru options and find the one which you need to update.
if (id === idx) {
return { ...item, [target.name]: target.value }; //spread all values and update only orderStatus
}
return item;
})
}
}));
};
Edit Added some comments to code and providing some explanation.
You were spreading postdata.LEVEL.options for LEVEL which is incorrect. For nested object you need to spread each level.
Apparently, your event.target.name is "orderStatus", so it will add an "orderStatus" key to your postData.
You might want to do something like this:
const handleOptionInputChange = (value, idx) => {
setPostData(oldValue => {
const options = oldValue.LEVEL.options;
options[idx].orderStatus = value;
return {
...oldValue,
LEVEL: {
...oldValue.LEVEL,
options
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e.target.value, idx)}
/>
);
})}
</div>
);
See this demo
I am trying to map over an array of objects in state, conditionally returning one of two react components from that state. I then change that state at some point and would expect the component to re-render when it's object's state changed. I understand my issue is something to do with React not recognizing the change in the diff, but I'm not sure why and what pattern I need to change to in order to get this working.
Here's a codepen:
https://codepen.io/steven-harlow/pen/KKPLXRO
And the code from it:
const App = (props) => {
const [todos, setTodos] = React.useState([
{name: 'A', done: false},
{name: 'B', done: false},
{name: 'C', done: false},
])
React.useEffect(() => {
}, [todos])
const handleClick = (name) => {
const index = todos.find(todo => todo.name == name)
let tempTodos = todos;
tempTodos[index].done = true;
setTodos(tempTodos);
}
return (
<div>
<h1>Hello, world!</h1>
<div>
{todos.map(todo => {
return todo.done ? (<div key={'done' + todo.name}>{todo.name} : done</div>) : (<div onClick={() => handleClick(todo.name)} key={'notdone' + todo.name}>{todo.name} : not done</div>)
})}
</div>
</div>
)
}
Here you go, this here should work for you now. I added some notes in there.
const App = (props) => {
const [todos, setTodos] = React.useState([
{name: 'A', done: false},
{name: 'B', done: false},
{name: 'C', done: false},
])
const handleClick = (name) => {
/*
Here you were using todos.find which was returning the object. I switched
over to todos.findIndex to give you the index in the todos array.
*/
const index = todos.findIndex(todo => todo.name === name)
/*
In your code you are just setting tempTodos equal to todos. This isn't
making a copy of the original array but rather a reference. In order to create
a copy I am adding the .slice() at the end. This will create a copy.
This one used to get me all of the time.
*/
let tempTodos = todos.slice();
tempTodos[index].done = true;
setTodos(tempTodos);
}
console.log(todos)
return (
<div>
<h1>Hello, world!</h1>
<div>
{todos.map((todo,index) => {
return todo.done ? (<div key={index}>{todo.name} : done</div>) : (<div onClick={() => handleClick(todo.name)} key={index}>{todo.name} : not done</div>)
})}
</div>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
Another thing I did was simplify the keys for the divs created by the map. I just added the index to the map and used that for the key, a lot cleaner that way.
Hope this helps!
React won't see the state as changed unless you create a new array.
const handleClick = n => setTodos(todos.map(t => t.name === n ? {...t, done: true} : t));
I'm trying to set state as array of objects, but it fails.
I created project using CRA, and using react-hooks for states.
I get data from graphql server using react-apollo-hooks.
I just declared data object in codesandbox, but it doesn't affect my problem.
For every click, set state(array of object) with data(array of object).
const data = {
lists: [
{
id: "1"
},
{
id: "2"
},
{
id: "3"
}
]
};
const Sample = () => {
const [sample, setSample] = useState([]);
const Updator = async () => {
try {
await data.lists.map(list => {
setSample([
...sample,
{
label: list.id,
value: list.id
}
]);
return true;
});
console.log(sample);
} catch (err) {
throw err;
}
};
return (
<div>
<React.Fragment>
<button
onClick={e => {
Updator();
}}
>
Click me
</button>
<p>
<strong>
{sample.map(single => {
return <div>{single.label}</div>;
})}
</strong>
</p>
</React.Fragment>
</div>
);
};
I attached all test code on below.
Here is codesandbox link.
https://codesandbox.io/s/zr50rv7qjp
I expect result of
123
by click, but result is
3
Also, for additional click, expected result is
123
123
And I get
3
3.
When I use setSample(), I expect function something like Array.push(). But it ignores all the previous data expect the last one.
Any helps will be thankful!
state updater does batching and since you are calling the setSample method in map, only your last value is being written in state.
The best solution here is to return data from map and then update the state once like below.
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const data = {
lists: [
{
id: "1"
},
{
id: "2"
},
{
id: "3"
}
]
};
const Sample = () => {
const [sample, setSample] = useState([]);
const Updator = async () => {
try {
const newData = data.lists.map(list => {
return {
label: list.id,
value: list.id
};
});
setSample([...sample, ...newData]);
} catch (err) {
throw err;
}
};
return (
<div>
<React.Fragment>
<button
onClick={e => {
Updator();
}}
>
Click me
</button>
<p>
<strong>
{sample.map((single, index) => {
return <div key={index}>{single.label}</div>;
})}
</strong>
</p>
</React.Fragment>
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<Sample />, rootElement);
Working Demo
Another solution is to use the callback method to update state but you must avoid calling state updates multiple times.
You're destructing sample which will not have the latest version of itself when you're looping and calling setSample. This is why it only puts 3 in the list of samples, because the last iteration of the map will destruct an empty sample list and add 3.
To make sure you have the newest value of sample you should pass a function to setSample. This function will get the latest version of your state var from react.
setSample((latest) => {
return [
...latest,
{
label: list.id,
value: list.id
}
]
});