suppose i am having an array of like
[
{ id: 1, name: "John" , selected: true}
{ id: 2, name: "Jim" , selected: false }
{ id: 3, name: "James" , selected: false }
]
I am having function which get id and on the basis of that id I want to change the selected property of object to true and other selected property object to false
here is my function what i have tried but its throwing error
const handleOnClick = (id) => {
const temps = state.findIndex((sub) => sub.id === id)
setState(prevState => ({
...prevState,
[prevState[temps].selected]: !prevState[temps].selected
}))
Use a .map() to iterate everything. You shouldn't change values within the array without recreating the tree down to the changed object. See below example.
Below you will:
map over every element
Spread their current values
Overwrite the selected depending on if the id is the newly "selected" id
I also supplied an additional function to update using the index of the item in the array instead of by id since your title is "how to update object using index in react state?", although your question also mentions changing by id. I would say go by id if you have the choice, array indices can change.
const App = () => {
const [state, setState] = React.useState([
{ id: 1, name: "John" , selected: true},
{ id: 2, name: "Jim" , selected: false },
{ id: 3, name: "James" , selected: false }
]);
// This takes the ID of the item
const handleOnClick = (id) => {
const newState = state.map(item => {
return {
...item,
selected: item.id === id
};
});
setState(newState);
};
// This takes the index of the item in the array
const handleOnClickByIndex = (indexToSelect) => {
const newState = state.map((item, idx) => {
return {
...item,
selected: indexToSelect === idx
};
});
setState(newState);
};
return (
<div>
{state.map((item, mapIndex) => (
<div key={item.id}>
<div>
<span>{item.name} is selected: {item.selected ? "yes" : "no"}</span>
{/* This will update by ID*/}
<button onClick={() => handleOnClick(item.id)}>Select using ID</button>
{/* This will update by the index of the array (supplied by map) */}
<button onClick={() => handleOnClickByIndex(mapIndex)}>Select using index</button>
</div>
</div>
))}
</div>
);
};
ReactDOM.render(
<App/>,
document.getElementById("app")
);
<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 id="app"></div>
Related
since DataGrid follows react-table, I try to show subRows in DataGrid like this exmple but the subRows or getLeafRows array properties array in each row of Table stays empty array[]. I think there is a problem in DataGrid component!
my code is:
const items= useMemo(
() =>
data?.map((item) => ({
id: item.id,
name: item.name,
time: item.time,
subRows: parseStringToArray(data.children).map((child) => ({
id: child.id,
name: child.name,
time: child.time,
})),
})),
[data]
);
const onRow = useCallback<(row: Row<any>) => HTMLAttributes<HTMLTableRowElement>>((row) => {
return row.getCanExpand()
? {
onClick: row.getToggleExpandedHandler(),
style: { cursor: 'pointer' },
}
: {};
}, []);
const getRowCanExpand = useCallback<(row: Row<any>) => boolean>((row) => !!row.original.subRows?.length, []);
return(
<DataGrid
data: items || [],
columns: gridCols || [],
withRowExpanding: true,
getRowCanExpand,
renderSubComponent,
onRow,
withColumnFilters
withSorting
withFixedHeader
withPagination
highlightOnHover
height={1000}
pageSizes={['50', '100', '200']}
/>
)
I'll be happy if you help me in this concept.
I expect to show my subRows in datagrid like another rows in datagrid
Try this
const items = useMemo(
() =>
data?.map((item) => ({
id: item.id,
name: item.name,
time: item.time,
subRows: parseStringToArray(item.children)?.map((child) => ({
id: child.id,
name: child.name,
time: child.time,
})) || [],
})),
[data]
);
const onRow = useCallback<(row: Row<any>) => HTMLAttributes<HTMLTableRowElement>>((row) => {
return row.getCanExpand()
? {
onClick: row.getToggleExpandedHandler(),
style: { cursor: 'pointer' },
}
: {};
}, []);
const getRowCanExpand = useCallback<(row: Row<any>) => boolean>((row) => {
return !!row.original.subRows?.length;
}, []);
return (
<DataGrid
data={items || []}
columns={gridCols || []}
withRowExpanding={true}
getRowCanExpand={getRowCanExpand}
renderSubComponent={renderSubComponent}
onRow={onRow}
withColumnFilters={true}
withSorting={true}
withFixedHeader={true}
withPagination={true}
highlightOnHover={true}
height={1000}
pageSizes={['50', '100', '200']}
/>
);
In the useMemo hook, I changed the data.children property to item.children. This is because the data array does not have a children property, but each item in the array does.
I added a null check to the parseStringToArray function in case it returns null or undefined.
I added a default value of an empty array for the subRows property in case the parseStringToArray function returns null or undefined.
In the getRowCanExpand function, I added a null check for the subRows property using the optional chaining operator (?.). This is because the subRows property may be null or undefined if the parseStringToArray function returns null or undefined.
const listItems = items.map((item) =>
item.id === id ? { ...item, checked: !item.checked } : item
)
Especially what is the difference between item.id and id? and why use : item at the end?
Full code below, Note that the code isn't something I'm working on, it is a tutorial and there is still more to code
import React from "react";
import { useState } from "react";
import { FaTrashAlt } from "react-icons/fa";
const Content = () => {
const [items, setItems] = useState([
{
id: 1,
checked: false,
item: "One half bag of cocoa covered",
},
{
id: 2,
checked: false,
item: "Item 2",
},
{
id: 3,
checked: false,
item: "Item 3",
},
]);
const handleCheck = (id) => {
console.log(`key: ${id}`);
const listItems = items.map((item) =>
item.id === id ? { ...item, checked: !item.checked } : item
);
setItems(listItems);
localStorage.setItem("shoppinglist", JSON.stringify(listItems));
};
return (
<main>
<main>
<ul>
{items.map((item) => (
<li className="item" key={item.id}>
<input
type="checkbox"
onChange={() => handleCheck(item.id)}
checked={item.checked}
/>
<label
style={item.checked ? { color: "red" } : { color: "green" }}
onClick={() => handleCheck(item.id)}
>
{item.item}
</label>
{/* <button>Delete</button> */}
<FaTrashAlt
onClick={() => handleCheck(item.id)}
role="button"
tabIndex="0"
/>
</li>
))}
</ul>
</main>
</main>
);
};
export default Content;
items.map(item => ) is mapping over an array of items, item is the object of the current item being mapped
id is the id that comes from handleCheck function(triggered onClick), which is the id of the clicked item, while item.id is the id of the current item from the array of objects
item.id === id ? { ...item, checked: !item.checked } : item uses the ternary operator which is a shorthand if/else to check if the id of the current item is the same as the id of the clicked item
if it is(true) it adds to the current item object checked: !item.checked, if it isn't(false) it returns the item object as it is
Here this code trying to keep items array immutable,if you change item.checked value it without using
It will modify items array too
{ ...item, checked: !item.checked }
And Id is used to collect only particular item and modifications of it.
The map function iterates through all the items in a list. Each item has an id, which is the list.id variable. The function takes id as a param.
Then if item.id equals the passed in id, a modified item will be returned. If not, the iten is returned unmodified. That's what :item at the end is for.
A few basics to start since I'm not sure where the confusion lies:
map() takes the contents of a list and returns another list / array
based on those concepts. e.g. [1,2,3].map(x => x *2) will return [ 2, 4, 6 ]
The ternary ?: operator takes three args (hence the name) If the
first is true, returns the second, otherwise the third
e.g. true ? 'yes' : 'no' returns 'yes'
The spread operator populates one object with the contents of
another. e.g. If a = { foo: 'Hello' } then
{ ...a, bar: 'World' } means { foo: 'Hello', bar: 'World' }
So your example takes the first list and If the ID matches the checkbox ID being targetted, it returns a new object with the state flipped, otherwise it returns the original object.
The net effect is a list of IDs with the "checked" status flipped on one of the items. Make sense?
I have following part of React code:
This is handler for adding new object item to the array.
So I am checking if object exist in whole array of objects. If yes I want to increase quantity of this object by 1. Else I want to add object and set quantity to 1.
I am getting error when I want to add 2nd same product (same id)
Uncaught TypeError: Cannot assign to read only property 'quantity' of object '#<Object>'
export const Component = ({ book }: { book: Book }) => {
const [basket, setBasket] = useRecoilState(Basket);
const handleAddBookToBasket = () => {
const find = basket.findIndex((product: any) => product.id === book.id)
if (find !== -1) {
setBasket((basket: any) => [...basket, basket[find].quantity = basket[find].quantity + 1])
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }])
}
}
EDIT:
if (find !== -1) {
setBasket((basket: any) =>
basket.map((product: any) => ({
...product,
quantity: product.quantity + 1,
}))
);
} else {
setBasket((basket: any) => [...basket, { ...book, quantity: 1 }]);
}
data structures
I'd say the root of your "problem" is that you chose the wrong data structure for your basket. Using Array<Item> means each time you need to update a specific item, you have to perform O(n) linear search.
If you change the basket to { [ItemId]: Item } you can perform instant O(1) updates. See this complete example below. Click on some products to add them to your basket. Check the updated basket quantity in the output.
function App({ products = [] }) {
const [basket, setBasket] = React.useState({})
function addToBasket(product) {
return event => setBasket({
...basket,
[product.id]: {
...product,
quantity: basket[product.id] == null
? 1
: basket[product.id].quantity + 1
}
})
}
return <div>
{products.map((p, key) =>
<button key={key} onClick={addToBasket(p)} children={p.name} />
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
const products = [
{ id: 1, name: "ginger" },
{ id: 2, name: "garlic" },
{ id: 3, name: "turmeric" }
]
ReactDOM.render(<App products={products}/>, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
object update
As a good practice, you can make a function for immutable object updates.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
export { update }
Then import it where this behavior is needed. Notice it's possible to use on nested object updates as well.
// App.js
import * as Obj from "./Obj"
function App({ products = [] }) {
// ...
function addToBasket(product) {
return event => setBasket(
Obj.update(basket, product.id, (p = product) => // ✅
Obj.update(p, "quantity", (n = 0) => n + 1) // ✅
)
)
}
// ...
}
object remove
You can use a similar technique to remove an item from the basket. Instead of coupling the behavior directly in the component that needs removal, add it to the Obj module.
// Obj.js
const update = (o, key, func) =>
({ ...o, [key]: func(o[key]) })
const remove: (o, key) => { // ✅
const r = { ...o }
delete r[key]
return r
}
export { update, remove } // ✅
Now you can import the remove behaviour in any component that needs it.
function App() {
const [basket, setBasket] = React.useState({
1: { id: 1, name: "ginger", quantity: 5 },
2: { id: 2, name: "garlic", quantity: 6 },
3: { id: 3, name: "tumeric", quantity: 7 },
})
function removeFromBasket(product) {
return event => {
setBasket(
Obj.remove(basket, product.id) // ✅
)
}
}
return <div>
{Object.values(basket).map((p, key) =>
<div key={key}>
{p.name} ({p.quantity})
<button onClick={removeFromBasket(p)} children="❌" />
</div>
)}
<pre>{JSON.stringify(basket, null, 2)}</pre>
</div>
}
// inline module for demo
const Obj = {
remove: (o, key) => {
const r = {...o}
delete r[key]
return r
}
}
ReactDOM.render(<App />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
In your setBasket your should create a new object instead of updating current one
your code should look like these one :
{...basket, ...{
quantity : basket.quantity + 1
}}
I am working on a project where I have 4 options, but I should only be able to select one option per screen. The option pushes an object with an id and a score (based on the screen) to an array in the state. So if I make one selection and change to another, I'd like to able to remove the previous object and append the new one in an array in the state (for each screen). I have script to append and remove the object if I'm clicking on the same option, but I'm not sure what I would do to replace the object with a new one if I make a selection then immediately go to another.
The array looks as follows:
scores: [
[{id: "G1", score: "4"}, {id: "B1", score: "3"}, {id: "O1", score: "2"}, {id: "R1", score: "1"}],
[{id: "B2", score: "4"}, {id: "G2", score: "3"}, {id: "R2", score: "1"}, {id: "O2", score: "4"}]
]
Here is the code I have now:
handleScore = (id, score, idx) => {
const {
statements,
scores
} = this.state;
if (statements[0][idx].isSelected) {
this.setState({
scores: scores.map((item, index) => index === 0 ? [...item, {
'id': id,
'score': score
}] : item)
});
} else {
this.setState({
scores: scores.map((item, index) => index === 0 ? item.filter(i => i.id !== item[0].id) : item)
});
}
}
and in the render method I have:
<div
className={`statement ${(this.state.statements[0][index].isSelected) ? 'selected' : ''}`} key={item.id} onClick={e => {
e.preventDefault();
this.handleScore(item.id, match.params.score, index)
}}>
{item.statement}
</div>
Thank you in advance!
I don't know what you are trying to do, but you can use the splice() method to remove an item from an array. Check this repro on Stackblitz to see the result, and here is the code in case it does not works:
import React, { Component } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const App = () => {
const [data, setData] = React.useState([
{id: 0, score:10},
{id: 1, score:20},
{id: 2, score:30},
{id: 3, score:40},
]);
const displayItems = () => {
return data.map((item, index) => {
return (
<div>
<span>id: {item.id}, score: {item.score}</span>{' '}
<button onClick={() => replaceItem(item, index)}>Replace item</button>
</div>
)});
}
const replaceItem = (item, index) => {
const newItem = {id:4, score: 50};
let newData = [...data];
// -----Replace ONE ('1') item at specific index by the newItem
// newData.splice(index, 1, newItem);
// -----OR remove an item and place another at the first index
newData.splice(index, 1); // remove item from array
newData.unshift(item); // Put the item at the first index of the array
//------
setData(newData); // set your newData array as the new state
}
return (
<div>
{displayItems()}
</div>
);
};
render(<App />, document.getElementById("root"));
I placed two use case of splice so you can coment/uncomment to see the result.
checkCard(cardName, index) {
const item = {
name: cardName,
indexNumber: index
};
// check the part where you should look at
const newFlipStatus = this.state.flipped[index];
this.setState({
tempArray: [...this.state.tempArray, item],
flipped: !newFlipStatus
}, () => {
this.cardCounter(this.state.tempArray)
});
}
render() {
return (
<ul>
<button onClick={() => this.shuffleCard()}>Click</button>
{this.state.cardArray.map((items, index) => (
<li className={this.state.flipped ? 'card' : 'card true'} key={index} onClick={() => this.checkCard(items, index)}>{items}</li>
))}
</ul>
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
When the user clicks on a specific card in this case a specific element, but the problem I have now is that it is looping through all the element and change them all.
https://codeshare.io/5O0M7j
this.state = {
memoryCards: ['mario', 'luigi', 'browser', 'peach', 'wario', 'toad', 'yoshi', 'bowser-junior', 'waluigi', 'diddy-kong-jr' ],
cardArray: [],
newArray: [],
tempArray: [],
arrayHere: [],
count: 0,
score: 0,
flipped: false
}
You only have a single flipped property which is representing the whole deck of cards. This is the same property you are checking in:
<li className={this.state.flipped ? 'card' : 'card true'}
So when you click on it, you update the state, but this state property is used on all the cards, so all the li elements will get the card classname.
Your check:
const newFlipStatus = this.state.flipped[index];
Will also return false, as you defined flipped as a boolean at the start. Then you assign it, which will be true.
flipped: !newFlipStatus
If you update your flipped state to an Object:
this.state = {
flipped: {}
...
}
You can update your Render function to check the property, on the Flipped Object, which related to the Index of the card:
<li className={this.state.flipped[index] ? 'card' : 'card true'}....
And then within your checkCard Function, you can check can flip the value and reassign it to your Flipped Object:
this.setState(state => ({
tempArray: [...state.tempArray, item],
flipped: {
...state.flipped,
[index]: !state.flipped[index]
}
}), () => {
this.cardCounter(this.state.tempArray)
});