I would like to remove an item from array onClick by using the prevState syntax.
Here is an example code that i use:
export default function App() {
const [selected, setSelected] = useState(false);
const [selectedItems, setSelectedItems] = useState([]);
const [cards, setCards] = useState(["card1", "card2", "card3", "card4"]);
function handleButtonClick(event) {
setSelected(true);
let key = event.target.id;
key = parseInt(key, 10);
if (selectedItems.indexOf(key) === -1) {
setSelectedItems([...selectedItems, key]);
} else {
// HOW do i remove the item from the array ????
let index = selectedItems.indexOf(key);
}
}
return (
<div className="App">
{cards.map((card, index) => (
<button
className={
selected && selectedItems.indexOf(index) !== -1
? "button1 active"
: "button1"
}
onClick={handleButtonClick}
id={index}
>
{card}
</button>
))}
</div>
);
}
link to sandbox as well : https://codesandbox.io/s/patient-thunder-mb1vx?file=/src/App.js
Once you've found the index of the key in selectedItems, set selectedItems's state by slicing the array before and after that index:
const index = selectedItems.indexOf(key);
setSelectedItems([
...selectedItems.slice(0, index),
...selectedItems.slice(index + 1)
]);
const { useState } = React;
const App = () => {
const [selected, setSelected] = useState(false);
const [selectedItems, setSelectedItems] = useState([]);
const [cards, setCards] = useState(["card1", "card2", "card3", "card4"]);
function handleButtonClick(event) {
setSelected(true);
let key = event.target.id;
key = parseInt(key, 10);
if (selectedItems.indexOf(key) === -1) {
let index = selectedItems.indexOf(key);
setSelectedItems([...selectedItems, key]);
} else {
const index = selectedItems.indexOf(key);
setSelectedItems([
...selectedItems.slice(0, index),
...selectedItems.slice(index + 1)
]);
}
}
return (
<div className="App">
{cards.map((card, index) => (
<button
className={
selected && selectedItems.indexOf(index) !== -1
? "button1 active"
: "button1"
}
onClick={handleButtonClick}
id={index}
key={index}
>
{card}
</button>
))}
</div>
);
}
ReactDOM.render(<App />, document.querySelector('.react'));
.App {
font-family: sans-serif;
text-align: center;
}
.active {
border: 2px solid green;
}
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class="react"></div>
Also remember to use a unique key prop for lists, and to prefer const over let whenever possible.
Use this one. Hope it will work.
if (selectedItems.indexOf(key) === -1) {
setSelectedItems([...selectedItems, key]);
} else {
// HOW do i remove the item from the array ????
let index = selectedItems.indexOf(key);
const arr = selectedItems.filter((item, i) => i !== index);
setSelectedItems(arr);
}
Check this link https://codesandbox.io/s/stupefied-greider-m25le
You can make a copy of your array, splice the index from the copy then set the state with the new array.
const newArray = [...oldArray]
newArray.splice(index, 1)
setSelectedItems(newArray)
An alternative to #CertainPerformance using splice:
const copy = [...selectedItems];
const index = copy.indexOf(key);
copy.splice(index, 1);
setSelectedItems(copy);
An option using Array#splice that uses the function form of setState:
const [items, setItems] = React.useState([]);
const deleteItemAtIndex = (index) => setItems(items => {
const newItems = [...items];
newItems.splice(index, 1);
return newItems;
});
This has the advantage that it can be memoized (useCallback) without needing items as a dependency (since setItems is always stable and the function itself doesn't use items, only the callback does):
const deleteItemAtIndex = React.useCallback((index) => setItems(items => {
const newItems = [...items];
newItems.splice(index, 1);
return newItems;
}), []);
I find it odd that the simple, standard answer hasn't been posted:
function handleButtonClick(event) {
setSelected(true);
const key = parseInt(event.target.id, 10);
setSelectedItems(selectedItems => {
const itemIndex = selectedItems.indexOf(key);
if (itemIndex === -1) {
// New item, add it
selectedItems = [...selectedItems, key];
} else {
// New item, add it
selectedItems = selectedItems.filter((_, index) => index !== itemIndex); // ***
}
return selectedItems;
});
}
Notice that since this sets state based on existing state, it's best to use the callback form of the state setter.
Related
I have a problem with the localStorage in my application. When I add items to a list of "favorites" they are stored without any problem in the localStorage, they can even be deleted by clicking them again.
But when I refresh the page, my application doesn't read that these items are in the favorites list and therefore doesn't mark them. Also, when I add a new item to the favorites list it causes it to delete everything from localStorage and start over.
Here's a gif of the localStorage view
Here's the code:
import React, { useState, useEffect } from 'react';
import SearchBar from '../../SearchBar/SearchBar.js';
import FiltersBox from '../FiltersBox/FiltersBox.js';
import { getItems } from '../../../Database/Database.js';
import './ItemsContainer.css';
function ItemsContainer() {
const [items, setItems] = useState([]);
const [search, setSearch] = useState('');
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
const [sortPrice, setSortPrice] = useState('');
const [filterCategory, setFilterCategory] = useState('');
const addItemToFavorites = item => {
let existentItem = favoriteItems.find(favItem => favItem.id === item.id);
if (existentItem) {
let filterTheExistentItem = favoriteItems.filter(
favItem => item.title !== favItem.title
);
setFavoriteItems(filterTheExistentItem);
let stringItems = JSON.stringify(filterTheExistentItem);
localStorage.setItem('Favorite Items', stringItems);
} else {
setFavoriteItems([...favoriteItems, item]);
let stringItems = JSON.stringify([...favoriteItems, item]);
localStorage.setItem('Favorite Items', stringItems);
}
};
const filteredItemsList = () => {
let newItemList = [];
newItemList = items.filter(item => {
if (filterCategory !== '' && filterCategory !== 'none') {
return item.category === filterCategory;
} else {
return item;
}
});
if (sortPrice === 'ascending') {
return newItemList.sort((a, b) => (a.price > b.price ? 1 : -1));
} else if (sortPrice === 'descending') {
return newItemList.sort((a, b) => (b.price > a.price ? 1 : -1));
} else {
return newItemList;
}
};
function onSortSelected(sortValue) {
setSortPrice(sortValue);
}
function onCategorySelected(categoryValue) {
setFilterCategory(categoryValue);
}
useEffect(() => {
getItems().then(res => setItems(res));
}, []);
useEffect(() => {
let xd = JSON.parse(localItems);
console.log(xd);
}, [localItems]);
return (
<div>
<SearchBar setSearch={setSearch} />
<FiltersBox
items={items}
setItems={setItems}
onSortSelected={onSortSelected}
onCategorySelected={onCategorySelected}
/>
<div>
{filteredItemsList()
.filter(item =>
search.toLowerCase() === ''
? item
: item.title.toLowerCase().includes(search)
)
.map(item => (
<div key={item.id}>
<div>{item.title}</div>
<button
className={favoriteItems.includes(item) ? 'si' : 'no'}
onClick={() => addItemToFavorites(item)}>
Add to favorites
</button>
</div>
))}
</div>
</div>
);
}
export default ItemsContainer;
And here I leave a GIF with a continuous console.log of the localStorage:
I tried everyrhing, and I don't know what is happening.
You're retrieving your items in localItems and... you do nothing with this variable. You should initialize your state favoritesItems with your local storage
const getItemsFromLocalStorage = () => {
const items = localStorage.getItem('Favorite Items');
return items ? JSON.parse(items) : [];
}
const [favoriteItems, setFavoriteItems] = useState(getItemsFromLocalStorage())
This is where the culprit is:
const [favoriteItems, setFavoriteItems] = useState([]);
let localItems = localStorage.getItem('Favorite Items');
You load localStorage into localItems, but you expect it to be in favoriteItems, where you have never assigned it. You would need to specify the item of localStorage as the initial state, like:
let localItems = localStorage.getItem('Favorite Items');
const [favoriteItems, setFavoriteItems] = useState(localItems ? localItems : []);
what my issue is
Scenario 1: It is not opening sub-lists after searching or removing any list name from the search bar
Scenario 2: After searching any list name in the search bar that is already selected, then after searching that selected list it is showing that list but its checkbox is not selected.
so what do I need after searching list name in the search bar if that list has a sub-list for example if I New Watchlists then I want to show that sub-list also that present under this list after searching in the search bar but right it coming with empty list you can see image below..
const hasSearchTerm = (n, searchTerm) =>
n.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1;
const filterData = (arr, searchTerm) =>
arr?.filter(
(n) =>
hasSearchTerm(n.title, searchTerm) ||
filterData(n.children, searchTerm)?.length > 0
);
function filteredTreeData(data, searchString, checkedKeys, setExpandedTree) {
let keysToExpand = [];
const filteredData = searchString
? filterData(data, searchString).map((n) => {
keysToExpand.push(n.key);
return {
...n,
children: filterData(n.children, searchString, checkedKeys)
};
})
: data;
setExpandedTree([...keysToExpand]);
return filteredData;
}
const Demo = () => {
const [expandedKeys, setExpandedKeys] = useState([]);
const [checkedKeys, setCheckedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [searchValue, setSearchValue] = useState("");
const [tree, setTree] = useState(treeData);
const onExpand = (expandedKeysValue) => {
console.log("onExpand", expandedKeysValue); // if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
setExpandedKeys(expandedKeysValue);
setAutoExpandParent(false);
};
const onCheck = React.useCallback(
(checkedKeysValue, e) => {
if (e.checked) {
if (e.node?.children?.length) {
setCheckedKeys(
_.union(
checkedKeys,
_.cloneDeep([
...e.node.key,
...e.node.children.map((child) => child.key)
])
)
);
} else {
setCheckedKeys(_.union(checkedKeys, [e.node.key]));
}
} else {
if (e.node?.children?.length) {
setCheckedKeys(
_.union(
checkedKeys.filter((item) => {
return (
item !== e.node.key &&
!e.node.children.filter((child) => child.key === item).length
);
})
)
);
} else {
setCheckedKeys(
_.cloneDeep(checkedKeys.filter((item) => item !== e.node.key))
);
}
}
},
[checkedKeys, setCheckedKeys]
);
const onSelect = (selectedKeysValue, info) => {
console.log("onSelect", info);
setSelectedKeys(selectedKeysValue);
};
React.useEffect(() => {
const checked = [];
treeData.forEach((data) => {
data.children.forEach((item) => {
if (item.checked) {
checked.push(item.key);
}
});
});
setCheckedKeys(checked);
}, []);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
React.useEffect(() => {
if (searchValue) {
const filteredData = filteredTreeData(
treeData,
searchValue,
checkedKeys,
setExpandedKeys
);
setTree([...filteredData]);
} else {
setTree(treeData);
// setExpandedKeys([]);
}
}, [searchValue, checkedKeys]);
return (
<div>
<Search
style={{ marginBottom: 8 }}
placeholder="Search"
onChange={(e) => {
setSearchValue(e.target.value);
}}
/>
<Tree
checkable
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onCheck={onCheck}
checkedKeys={checkedKeys}
onSelect={onSelect}
selectedKeys={selectedKeys}
treeData={tree}
/>
</div>
);
};
CodeSandBox Link
When you run the filterTreeData function, you assign the children to only the tree that contains the search term. So, you're searching n levels deep for the term. Notice that it works like I'd expect it to if you only search one level deep. That is, if the node has children, you return the node as such rather than filtering down all the children that don't also contain the term. when you filter with the string "new" in your example, you'll notice that none of the children in the collection "New Watchlists" contain the term and they are therefore filtered out leaving you with an empty array. My naive solution is to just return the children as in my following example:
function filteredTreeData(data, searchString, checkedKeys, setExpandedTree) {
let keysToExpand = [];
const filteredData = searchString
? filterData(data, searchString).map((n) => {
keysToExpand.push(n.key);
return {
...n,
children: n.children
};
})
: data;
setExpandedTree([...keysToExpand]);
return filteredData;
}
I call this solution naive, because I'm not exactly sure what your use case is. There might be more conditional logic you need to render in there for first searching n levels deep for the term and then and returning the whole node if the term is discovered anywhere inside the node or children.
Im doing a onclick that changes the individual numbers colors, its working fine in useState(the colors change):
const [totalNumbers, setTotalNumbers]: any = useState([1, 2, 3]);
But when i push to useState: totalNumbers.push(event.target.id), only the array is increased but the colors didnt render
const newBet: React.FC = () => {
const [range, setRange] = useState(gamesJson[whichLoteriaIsVar].range);
const numbersList = Array.from(Array(25).keys()).map((num) => num + 1);
const [totalNumbers, setTotalNumbers]: any = useState([]);
const changeButtonColor = (event: any) => {
totalNumbers.push(Number(event.target.id));
console.log(totalNumbers);
setTotalNumbers(totalNumbers);
};
return(
<NumbersContainer>
{numbersList.map((num) => (
<Numbers
onClick={changeButtonColor}
id={num.toString()}
className={
totalNumbers.indexOf(num) === -1 ? 'desactive' : 'active'
}
key={num}
>
{formatNumber(num)}
</Numbers>
))}
</NumbersContainer>
)
}
Try this:
const changeButtonColor = (event: any) => {
const newtotalNumbers = totalNumbers.slice()
newtotalNumbers.push(Number(event.target.id));
setSelectedNumbers(newtotalNumbers );
};
const changeButtonColor = (event: any) => {
totalNumbers.push(Number(event.target.id));
console.log(totalNumbers);
setTotalNumbers([...totalNumbers]);
};
Solved my problem
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))
}
I have a set of card objects that I map over.
When I click on a card it adds the selected class which in turn gives it a border to show the user it is selected, it also adds the id of the card to the selectedCards useState array.
WHAT I WANT TO HAPPEN:
Each card object has a creditAvailable key state which is equal to a figure.
On selection (click) of the card, in addition to selecting the card I would also like to add up the creditAvailable and display it on the screen. and when I unselect the card I would like the figure to go down.
WHAT I HAVE TRIED:
I thought it would be as simple as calling the function to add up the credit inside the first function which selects the card, however when console logging inside the first function I see that the state has not yet updated. (scope).
I then tried to call the function outside of the first function but it gave me an infinite loop. Here is my code.
Any ideas? Thanks
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const [total, setTotal] = useState();
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
// addUpCreditAvailable(); // nothing happens
console.log(selectedCards); // []
};
console.log(selectedCards) // [1] for example. This is in the global scope
const addUpCreditAvailable = () => {
console.log("inside add up credit");
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
setTotal(result);
return result;
};
return (
<div className="Container">
<UserInputForm submitData={handleSubmitData} />
<h1> Cards available to you displayed are below!</h1>
{cards.map(
({
id,
name,
number,
apr,
balanceTransfer,
purchaseDuration,
creditAvailable,
expiry,
}) => (
<CreditCard
key={id}
name={name}
number={number}
apr={apr}
balanceTransferDuration={balanceTransfer}
purchaseOfferDuration={purchaseDuration}
creditAvailable={creditAvailable}
expiry={expiry}
onClickCard={() => handleSelectCard(id)}
selected={selectedCards.includes(id)}
/>
)
)}
<span> £{total}</span>
)}
I figured it out with the help from above. As Wilms said i had to return the result of the handleSelectCard function and return the result of the addUpCredit function. Then I called the addUpCreditAvailable with the selectedCards state and stored the result in a variable which i then displayed in my render method.
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
return selectedCards;
};
const addUpCreditAvailable = (selectedCards) => {
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
return result;
};
const totalCredit = addUpCreditAvailable(selectedCards);
render method:
render (
[...]
{selectedCards.length && (
<div className={bem(baseClass, "total-credit")}>
Total Credit available: £{totalCredit}
</div>
)}
[...]
)