updating object inside array inside object using prevState and the useState hook - reactjs

I'd like to remove a nested object based on the id is equal to a passed prop. At the moment, the entire object is replaced. I'm missing something, when trying to update the state using useState probably with the way I'm looping my object?
UPDATE: The question was closed in response to available answers for updating nested objects. This question involves arrays which I believe are part of the issue at hand. Please note the difference in nature in this question with the forEach. Perhaps a return statement is required, or a different approach to the filtering on id..
my initial object looks like this:
{
"some_id1": [
{
"id": 93979,
// MORE STUFF
},
{
"id": 93978,
// MORE STUFF
}
],
"some_id2": [
{
"id": 93961,
// MORE STUFF
},
{
"id": 93960,
// MORE STUFF
}
]
}
and I go through each item as such:
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
[key]: {
...prevState[key],
[x.id]: undefined
}
}))
}
})
}

There are 3 problems in your code:
You are setting the value of key to an object while the items is expected to have an array to ids.
// current
[key]: {
...prevState[key],
[x.id]: undefined
}
// expected
[key]: prevState[key].filter(item => item.id === matchingId)
If you intend to remove an object from an array based on some condition, you should be using filter as pointed out in Owen's answer because what you are doing is something else:
const a = { xyz: 123, xyz: undefined };
console.log(a); // { xyz: undefined} - it did not remove the key
To make your code more readable, it is expected to manipulate the entire object items and then, set it to the state once using setItems - in contrast to calling setItems multiple times inside a loop and based on some condition.
This makes your code more predictable and leads to fewer re-renders.
Also, the solution to your problem:
// Define this somewhere
const INITIAL_STATE = {
"some_id1": [
{
"id": 93979
},
{
"id": 93978
}
],
"some_id2": [
{
"id": 93961
},
{
"id": 93960
}
]
};
// State initialization
const [items, setItems] = React.useState(INITIAL_STATE);
// Handler to remove the nested object with matching `id`
const removeByNestedId = (id, items) => {
const keys = Object.keys(items);
const updatedItems = keys.reduce((result, key) => {
const values = items[key];
// Since, we want to remove the object with matching id, we filter out the ones for which the id did not match. This way, the new values will not include the object with id as `id` argument passed.
result[key] = values.filter(nestedItem => nestedItem.id !== id)
return result;
}, {});
setItems(updatedItems);
}
// Usage
removeByNestedId(93961, items);

Probably a simple reduce function would work, Loop over the entries and return back an object
const data = {"some_id1": [{"id": 93979},{"id": 93978}],"some_id2": [{"id": 93961},{"id": 93960}]}
const remove = ({id, data}) => {
return Object.entries(data).reduce((prev, [nextKey, nextValue]) => {
return {...prev, [nextKey]: nextValue.filter(({id: itemId}) => id !== itemId)}
}, {})
}
console.log(remove({id: 93961, data}))

your way solution
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
//filter will remove the x item
[key]: element.filter(i => i.id !== x.id),
}))
}
})
}
}
short solution.
for(const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
const items = {
"some_id1": [
{
"id": 93979,
},
{
"id": 93978,
}
],
"some_id2": [
{
"id": 93961,
},
{
"id": 93960,
}
]
}
const singleItemId = 93979;
for (const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
console.log(items);
//setItems(items)

You could try using the functional update.
const [data, setData] = [{id:1},{id:2},{id:3}...]
Once you know the id which you need to remove.
setData(d=>d.filter(item=>item.id !== id));

Related

after sort, the ui not render with new data

im trying to list out the list after sort by status. This is my code, it can be sort in order.But the problem is, at the ui screen, it still previous value which it not render after do sorting.
const [listData, setListData] = useState(null)
let statusOrder={
'Active':1,
'In progress'
}
let list=[
{
sample: '1',
data:[
{
name:'name1',
status:'In Progress'
},
{
name:'name2',
status:'Active'
}
]
},
{
sample:'2',
data:[
{
name:'name1',
status:'In Progress'
},
{
name:'name2',
status:'Active'
}
]
}
]
const function = () => {
setListData(list)
for(let i=0; i<listData.length; i++){
listData[i].data.sort(a,b) => statusOrder[a.status] - statusOrder[b.status]
if((index+1) == listData.length)
setData(listData)
}
}
useEffect(() => {
sort()
},[])
I already try this approach, but still not render.
setListData([].concat(listData[i].data).sort((a, b) => statusOrder[a.status] - statusOrder[b.status]))
Like mentioned in the comments you're not really setting a new state since the "new" state is the same object as the old one.
Your function is called function which is a reserved keyword in the JS. So I assume you mean sort like you're using in your useEffect.
To handle the sort or your list you can try this
const sort = () => {
// give a callback to the setState which receives the prevState
setListData((prevListData) => {
// map over the prevState which returns a new array
const newListData = prevListData.map((item) => {
// return a new object with the item and sorted data
return {
...item,
data: item.data.sort(
(a, b) => statusOrder[a.status] - statusOrder[b.status]
),
};
});
return newListData;
});
};

How to map over useState array of objects and edit the needed object?

const [Questions, setQuestions] = React.useState(props.QuestionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) => {
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
});
} else {
return;
}
};
This is the code im doing and what I want to do it loop over the Questions (which has a list of objects that look like this.
{
"category": "Entertainment: Video Games",
"type": "boolean",
"difficulty": "hard",
"question": "The first "Metal Gear" game was released for the PlayStation 1.",
"correct_answer": "False",
"incorrect_answers": [
"True"
],
"Answers": {
"correct": {
"MasterId": "MXTOfKnKw7dU7QP0UP0td",
"id": 0,
"answer": "False",
"selected": false,
"correct": true,
"userChosen": true
},
"wrong": {
"id": 1,
"answer": "True",
"selected": false
}
}
}
I tried to do everything but it returns undefined. I even tried to do the map alone with the value and logged out the needed value and it worked.
If someone can tell me a good way to loop over usestate array of objects and only edit the objects that satisfy the condition that would be great.
Remove the curly braces so that the value is actually returned.
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
return {
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
};
} else {
return item;
}
});
);
I don't recommend using the capitalization convention for variable names. Use camelCase and save those for Components, Classes, and Types.
For the sake of readability and clean code, I recommend you write data processing code outside of setSomeState()
Here's an example.
import { useState } from "react";
...
const [questionList, setQuestionList] = useState(props.questionsData);
const handleClick = (id, isCorrect) => {
if (isCorrect) {
const updatedList = questionList.map((item, index) => {
if (index === id) {
return {
...item,
[item.answers.correct.selected]: !item.answers.correct.selected,
};
} else {
return item;
}
});
// no messy prev state stuff, it's now clean and easy to maintain
setQuestionList(updatedList);
} else {
return;
}
};
Further, I see that you use props.questionData as a default value of the setState. It's not a good approach since one component's state's default value depends on the other component's value and can potentially cause errors.
I personally recommend you to keep the questions default state to an empty array, and use useEffect and do setQuestions inside the useEffect with props.questionData.
Can you imagine writing this handler every time you have array-based stated? Also map doesn't make sense because we only want to update one item, however map iterates over the entire array.
Write a generic function to update an arr at a specific index using a user-supplied func -
function update(arr, index, func) {
return [
...arr.slice(0, index),
func(arr[index]),
...arr.slice(index + 1)
]
}
Now in your component -
function MyComponent({ questions = [] }) {
const [questions, setQuestions] = useState(questions)
const updateQuestion = index => event =>
setQuestions(arr => update(arr, index, q => ({
/* update q here */
})))
return questions.map((q, index) =>
<Question key={index} question={q} onClick={updateQuestion(index)} />
)
}
Developing your own utility functions to operate on arrays and objects can be a fun exercise, but maybe challenging to get right. Look to popular libraries like Immutable.js and Immer for inspiration.
See these Q&A for live examples you can run in your browser -
Add Numbers in ReactJS without button
How to change Active when it is clicked
How to remove an element from an array passing the key using hooks?
how to add class and remove class from buttons in react?
You forgot to return the result of the map. If a function doesn't return anything in JS, undefined will be returned. In addition, the computed property isn't used correctly here:
{
...item,
[item.Answers.correct.selected]: !item.Answers.correct.selected,
}
Try this:
const handleClick = (id, isCorrect) => {
if (isCorrect) {
setQuestions((prev) =>
prev.map((item, index) => {
if (index === id) {
item.Answers.correct.selected = !item.Answers.correct.selected
}
return item
}));
}
};

How to create multiple object in Reactjs?

I am having a onChange function i was trying to update the array optionUpdates which is inside of sentdata by index wise as i had passed the index to the onChange function.
Suppose i update any two values of the input field from option which is inside of postdata therefore the input name i.e. orderStatus with changed value and with order should be saved inside of optionUpdates
For example: Suppose i update the option 1 and option 3 of my postdata further inside of options of orderStatus values so my optionUpdates which is inside of sentdata should look like this
optionUpdates: [
{
Order: 1,
orderStatus: "NEW1"
},
{
Order: 3,
orderStatus: "New2"
}
]
here is what i tried
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
Demo
complete code:
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 posting = {
optionUpdates: []
};
const [sentdata, setSentData] = React.useState(posting);
const handleOptionInputChange = (event, idx) => {
const target = event.target;
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => {
if (id === idx) {
return { ...item, [target.name]: target.value };
}
return item;
})
}
}));
setSentData(oldValue => {
const curoptions = oldValue.sentdata.optionUpdates[idx];
console.log(curoptions);
curoptions.event.target.name = event.target.value;
return {
...oldValue,
sentdata: {
...oldValue.sentdata.optionUpdates,
curoptions
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
key={idx}
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
If I've understood correctly then what you're looking to do is save a copy of the relevant options object in sentdata every time one changes. I think the best way to approach this is by doing all your state modification outside of setPostData, which then makes the results immediately available to both setPostData and setSentData. It will also make the setters easier to read, which is good because you have some quite deeply nested and complicated state here.
A few other things worth noting first:
Trying to use synchronous event results directly inside the asynchronous setter functions will throw warnings. If you do need to use them inside setters, then it is best to destructure them from the event object first. This implementation uses destructuring although it didn't end up being necessary in the end.
You seem to have got a bit muddled up with setSentData. The oldValue parameter returns the whole state, as prev in setPostData does. For oldValue.sentdata you just wanted oldValue. You also wanted curoptions[event.target.name], not curoptions.event.target.name.
So, on to your code. I would suggest that you change the way that your input is rendered so that you are using a stable value rather than just the index. This makes it possible to reference the object no matter which array it is in. I have rewritten it using the Order property - if this value is not stable then you should assign it one. Ideally you would use a long uuid.
{postdata.LEVEL.options.map(item => {
return (
<input
key={item.Order}
type="text"
name="orderStatus"
value={item.orderStatus}
onChange={e => handleOptionInputChange(e, item.Order)}
/>
);
})}
The handleOptionInputChange function will now use this Order property to find the correct objects in both postdata and sentdata and update them, or if it does not exist in sentdata then push it there. You would do this by cloning, modifying, and returning the relevant array each time, as I explained before. Here is the function again with comments:
const handleOptionInputChange = (event, orderNum) => {
const { name, value } = event.target;
/* Clone the options array and all objects
inside so we can mutate them without
modifying the state object */
const optionsClone = postdata.LEVEL.options
.slice()
.map(obj => Object.assign({}, obj));
/* Find index of the changed object */
const optionIdx = optionsClone.findIndex(obj => obj.Order === orderNum);
/* If the orderNum points to an existing object...*/
if (optionIdx >= 0) {
/* Change the value of object in clone */
optionsClone[optionIdx][name] = value;
/* Set postdata with the modified optionsClone */
setPostData(prev => ({
...prev,
LEVEL: {
...prev.LEVEL,
options: optionsClone
}
}));
/* Clone the optionUpates array and all
contained objects from sentdata */
const updatesClone = sentdata.optionUpdates
.slice()
.map(obj => Object.assign({}, obj));
/* Find the index of the changed object */
const updateIdx = updatesClone.findIndex(obj => obj.Order === orderNum);
/* If the changed object has already been
changed before, alter it again, otherwise push
a new object onto the stack*/
if (updateIdx >= 0) {
updatesClone[updateIdx][name] = value;
} else {
updatesClone.push({ Order: orderNum, [name]: value });
}
/* Set sentdata with modified updatesClone */
setSentData(prev => ({
...prev,
optionUpdates: updatesClone
}));
}
};

How to push item into array of object in array with useState?

I have an array with objects that also contain array. How can I push element to that array inside object using useState?
const [list, updateList] = useState([defaultList]);
const defaultList = [
{
"name":"Element 1",
"subList": [{"name": "Element 1"}]
}
]
How can I update subList in that case?
If you have one item in your array, you must recreate your array with new/modified data in it.
updateList(state => {
const stateCopy = [...state]
stateCopy[index] = {
...stateCopy[index],
subList: [...state[index].subList, newItem]
}
return stateCopy
})
Update:
If you want to update based on the item name.
updateList(state => {
const stateCopy = [...state]
const indexOfName = stateCopy.findxIndex(item => item.name === name)
stateCopy[indexOfName] = {
...stateCopy[indexOfName],
subList: [...state[indexOfName].subList, newItem]
}
return stateCopy
})
Assuming you want to append an item to the sublist of a specific list item, based on index:
function appendSublistItemForItemAtIndex(newItem, index) {
updateList([
...list,
[index]: {
...list[index],
subList: [
...list[index].subList,
newItem
]
}
])
}
Then call this function wherever needed for your component (some callback, in a useEffect hook, etc.)

change of one value in the state react

In the state I have data array that look like this
{
"offers" : [
{
"model": "shoes",
"availability": false,
"id": 1
},
{
"model": "t-shirt",
"availability": true,
"id": 2
},
{
"make": "belt",
"availability": false,
"id": 3
}
]
}
My task is to change the accessibility field. I have a button - change the availability. After clicking it I want to make the availability of a single field changed
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
this.setState({
data: newData.map(x => x.id === id ? x.ailability = !ailability : ailability )
})
}
I have created a function that does not work.
My idea was: I click on the element, pass to the element's id and accessibility. In this function, I create a copy of the state. In the right place, I change the availability and I am sending everything to the state.
This function does not work, nothing happens, I have no idea how to fix it
Seems like the map function is returning nothing ...
Could be something like this
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
const updateItem = (x) => {
if (x.id === id) {
x.ailability = !ailability;
}
return x;
};
this.setState({
data: newData.map(updateItem)
})
}
I am assuming you want to click on an element which passes an id and availability of an offer, find a matching entry (on the passed id) in the offers array in the state and change the availability of the found object to what you passed in the function.
I recommend using the callback that this.setState() allows you to use, providing a reference to a copy of the previous state, before the setState was called.
changeAvailability = (passedAvail, passedID) => {
this.setState((prevState) => {
// Make a copy of the offers array in the state
const offersArray = prevState.offers
// Find index of matching obj by id
const matchObjIndex = offersArray.findIndex(
(offerObj) => (
offerObj.id === passedID
)
)
// If a match was indeed found
if (matchObjIndex > -1) {
// Find matching obj from index obtained
const matchObj = offersArray[matchObjIndex]
// Change the availibility of the matched obj and obtain a modified obj
const modmatchObj = Object.assign(matchObj, {
availability: passedAvail
})
// Replace the offer obj by index in the offers array.
offersArray.splice(matchObjIndex, 1, modmatchObj);
}
// Finally set state with the updated offers lists
// If no match was found, it will return the old offers array
return {offers: offersArray }
})
}
You can try something like this to make it work:
changeAvailability = (availability, id) =>
this.setState(prevState => {
return {
offers: prevState.offers.map((e, i) =>
e.id !== id ? e : { ...prevState.offers[i], availability }
)
};
});
The issue with your solution is that you shouldn't modify the items you're iterating while doing a map, but you should just return whatever new data you wanna add.

Resources