I am trying to update a state value w/ a multidimensional array but I can't seem to figure how to update one of the arrays object key values without effecting the previous state value which I use later in the process after the dispatch call.
I the code below the payload carries an array of ids (nodes) that I loop through and change the only those objects within the state object. Rather straight forward, but updating a multidimensional array of objects and not effecting the state has me confused.
UPDATE_RESTRICTION: (curState, payload) => {
const updatedNodes = {...curState.layout}
const accessProfile = BpUAE.accessProfileID
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node].settings.bp_uae_restrictions = {};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node].settings.bp_uae_restrictions[accessProfile] = {};
}
updatedNodes[node].settings.bp_uae_restrictions[accessProfile].is_node_restricted = JSON.parse(payload.isRestricted);
})
return {layout: updatedNodes}
}
Please let me know if you need more information and thanks for any help you can provide.
You are not applying immutable update pattern correctly, you are mutating the nested references pointing into the current state object. You need to create new object references and shallow copy all state that is being updated.
UPDATE_RESTRICTION: (curState, payload) => {
const updatedNodes = { ...curState.layout }
const accessProfile = BpUAE.accessProfileID;
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {},
},
};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {},
},
},
};
}
// now all the new references have been created and previous
// state shallow copied, you can update the deeply nested
// `is_node_restricted` property.
updatedNodes[node].settings.bp_uae_restrictions[accessProfile].is_node_restricted = JSON.parse(payload.isRestricted);
});
return {
...curState,
layout: updatedNodes,
};
}
UPDATE: Added the last immutable pattern
const updatedNodes = { ...curState.layout }
const accessProfile = BpUAE.accessProfileID;
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {},
},
};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {},
},
},
};
}
// now all the new references have been created and previous
// state shallow copied, you can update the deeply nested
// `is_node_restricted` property.
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {
...updatedNodes[node].settings.bp_uae_restrictions[accessProfile],
is_node_restricted: JSON.parse(payload.isRestricted)
},
},
},
};
});
return {
...curState,
layout: updatedNodes,
};
}```
Check your return statement, you have to include your previous state and override layout like shown below.
return {...curState, layout: updatedNodes}
Related
I have here a map function for an array of object
and also added some condition
userList.map((item) => {
const newFilter = dailyData.filter((value) => {
return value.author == item.MSM;
});
let obj_idx = userList.findIndex(
(obj) => obj.MSM == newFilter[0]?.author
);
const newArr = userList?.map((obj, idx) => {
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0]?.store,
timeIn: newFilter[0]?.date_posted,
storeTimeOut: newFilter[newFilter.length - 1]?.store,
timeOut: newFilter[newFilter.length - 1]?.date_posted,
};
} else {
return obj;
}
});
console.log(newArr);
setAttendanceData(newArr);
});
that just check if the Item exist in the array before updating it.
and this condition here works fine
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0]?.store,
timeIn: newFilter[0]?.date_posted,
storeTimeOut: newFilter[newFilter.length - 1]?.store,
timeOut: newFilter[newFilter.length - 1]?.date_posted,
};
}
as seen in this picture
but when my condition becomes false the whole array of object becomes empty again
my hunch is I'm setting the state wrongly . which appear in the setAttendanceData(newArr)
this state is just an empty array state const [attendanceData, setAttendanceData] = useState([]);. is there a way to not update the whole array of object when the condition gets false like how can I use spread operator in this situation. TIA
I feel you need to add a little more description including the current react component and little bit about your inputs/outputs for a more refined answer.
userList.map((item, index, self) => {
const newFilter = dailyData.find((value) => {
return value.author === item.MSM;
});
// this is only needed if there can be multiple
// occurrence of same obj.MSM in 'userList' array
let obj_idx = self.findIndex((obj) => obj.MSM === newFilter[0] ? .author);
// why are you doing this
const newArr = self ? .map((obj, idx) => {
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0] ? .store,
timeIn: newFilter[0] ? .date_posted,
storeTimeOut: newFilter[newFilter.length - 1] ? .store,
timeOut: newFilter[newFilter.length - 1] ? .date_posted,
};
} else {
return obj;
}
});
console.log(newArr);
// why are you doing this , updating state on every iteration
// after user.map attendanceData will only reflect for the last index in userList
setAttendanceData(newArr);
});
But if my assumptions are correct, this following code snippet may help you in some way.
setAttendanceData(
// make sure to run setAttendanceData only once
userList.map((item, index, self) => {
const newFilter = dailyData.find((value) => {
return value.author === item.MSM;
});
const obj_idx = self.findIndex((obj) => obj.MSM === newFilter[0] ? .author);
// there can be refinement around this but i feel this is what you need
if (index === obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0] ? .store,
timeIn: newFilter[0] ? .date_posted,
storeTimeOut: newFilter[newFilter.length - 1] ? .store,
timeOut: newFilter[newFilter.length - 1] ? .date_posted,
};
} else {
return obj;
}
})
);
First, I would re-examine this logic. You're calling map inside of map on the same array, which is confusing. The top level map is used more like a forEach than a map because nothing is done with the output. You're also updating state on each iteration of the loop. I'm not sure how React handles this situation.
Try to only set state once the data is in the shape you want. Ideally it would only require one pass through the array. Here is an example:
const [attendanceData, setAttendanceData] = useState([]);
const [userList, setUserList] = useState([
{ MSM: "Edmond" },
{ MSM: "Dantes" },
{ MSM: "Conan" },
]);
const [dailyData, setDailyData] = useState([
{ author: "No one", store: "store", date_posted: "a date" },
{ author: "Edmond", store: "store", date_posted: "a date" },
{ author: "Conan", store: "store", date_posted: "a date" },
]);
useEffect(() => {
const mappedUserList = userList.map((user) => {
// Find the daily data author that matches the current user.
const day = dailyData.find((value) => {
return value.author == user.MSM;
});
if (!day) {
// If no author matched in daily data, return the user unchanged.
return user;
} else {
return {
...user,
storeTimeIn: day.store,
timeIn: day.date_posted,
// I'll leave these lines for you to figure out
// since I don't understand why you're getting the last element of the filtered data.
// storeTimeOut: filteredDailyData[filteredDailyData.length - 1]?.store,
// timeOut: filteredDailyData[filteredDailyData.length - 1]?.date_posted,
};
}
});
setAttendanceData(mappedUserList);
}, [userList, dailyData]);
console.log(attendanceData);
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));
I'm setting the data from api in two state variables both values are same.But when I am updating one state variable the another state variable also changing.
Api.get("url")
.then((response) => {
this.setState(
{
deliveryInfoSection2: Object.assign({}, response.data),
deliveryInfoSection2copy: Object.assign({}, response.data),
}
);
})
updateState() {
try {
newVal = { ...this.state.deliveryInfoSection2 };
newVal.orderDetails[index].bo = value.replace(global.REG_NUMBER_ONLY, '');
//After this state variable deliveryInfoSection2copy is also updating.
this.setState({ deliveryInfoSection2: newVal }, () => {
if (this.state.deliveryInfoSection2.orderDetails[index].bo != '') {
}
catch (e) {
alert("error" + e)
}
}
This is a issue with respect to shallow copy of variables while using spread operator in javascript. It has nothing to do with react's setState. The spread operator creates a shallow copy for the object.
response = {
orderDetails: [
{
bo: "tempData1"
},
{
bo: "tempData2"
}
]
}
deliveryInfoSection2 = Object.assign({}, response)
deliveryInfoSection2Copy = Object.assign({}, response)
//Here spread operator will create shallow copy and so, the references are copied and hence any update to one will update other.
newVar = { ...deliveryInfoSection2 }
newVar.orderDetails[0].bo = "newValue"
deliveryInfoSection2 = newVar
console.log("deliveryInfoSection2", deliveryInfoSection2)
console.log("deliveryInfoSection2Copy", deliveryInfoSection2Copy)
To fix this, you need to create a deep copy of your object.
You can use JSON.parse(JSON.stringify(object)) for the same.
response = {
orderDetails: [
{
bo: "tempData1"
},
{
bo: "tempData2"
}
]
}
deliveryInfoSection2 = Object.assign({}, response)
deliveryInfoSection2Copy = Object.assign({}, response)
//This will create a deep copy for the variable
newVar = JSON.parse(JSON.stringify(deliveryInfoSection2))
newVar.orderDetails[0].bo = "newValue"
deliveryInfoSection2 = newVar
console.log("deliveryInfoSection2", deliveryInfoSection2)
console.log("deliveryInfoSection2Copy", deliveryInfoSection2Copy)
Hope it helps. Revert for any doubts/confusions.
I have a unit test that is producing something I didn't expect:
Background: I'm making a simple todo list with Angular/test driven development.
Problem: When I call editTask on an item in the array, it changes the item's value. But, I don't see how it's changed in the original array because the original array is never accessed in the method I'm testing. Please help me connect HOW the original array is being changed? It seems Object.assign is doing this, but why?
describe('editTask', () => {
it('should update the task by id', () => {
const dummyTask1 = { id: 1, name: 'test', status: false };
service.tasks.push(dummyTask1); //refers to TestBed.get(TaskService)
const id = 1;
const values = { name: 'cat', status: false };
service.editTask(id, values);
console.log(service.tasks); // why does this log this object? [Object{id: 1, name: 'cat', status: false}]
expect(service.tasks[0].name).toEqual(values.name); // Test passes
});
});
Here is the method I'm testing:
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values); //How does this line change the array?
return task;
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop(); //is this altering the original array somehow?
}
If needed, here's the full Angular service:
export class TaskService {
tasks: any = [];
lastId = 0;
constructor() { }
addTask(task) {
if (!task.id) {
task.id = this.lastId + 1;
}
this.tasks.push(task);
}
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values);
return task;
}
deleteTask(id: number) {
this.tasks = this.tasks.filter(task => task.id !== id);
}
toggleStatus(task) {
const updatedTask = this.editTask(task.id, { status: !task.status});
return updatedTask;
}
getTasks() {
return of(this.tasks);
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop();
}
}
Here is the github repo: https://github.com/capozzic1/todo-tdd
The getTask() method is getting a reference to the item in the array using the array filter() method.
It then uses Object.assign() to change the properties of the item. The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
So now the values of the reference in memory of the item is changed. Because it is a reference in memory you will see the original item being changed.
I don't think this is difficult, I just can't figure out the best way to do it. This function is creating an array, from a group of checkboxes. I then want to break up the array and create an array of objects, because each object can have corresponding data. How do I filter out existing rolesInterestedIn.roleType.
handleTypeOfWorkSelection(event) {
const newSelection = event.target.value;
let newSelectionArray;
if(this.state.typeOfWork.indexOf(newSelection) > -1) {
newSelectionArray = this.state.typeOfWork.filter(s => s !== newSelection)
} else {
newSelectionArray = [...this.state.typeOfWork, newSelection];
}
this.setState({ typeOfWork: newSelectionArray }, function() {
this.state.typeOfWork.map((type) => {
this.setState({
rolesInterestedIn: this.state.rolesInterestedIn.concat([
{
roleType: type,
}
])
}, function() {
console.log(this.state.rolesInterestedIn);
});
})
});
}
UDPATE
rolesInterestedIn: [
{
roleType: '',
experienceYears: ''
}
],
Because each time you do setState you are concatenating the new value to the prev one in rolesInterestedIn array. Add new value only when you are adding new item, otherwise remove the object from both the state variable typeOfWork and rolesInterestedIn.
Try this:
handleTypeOfWorkSelection(event) {
const newSelection = event.target.value;
let newSelectionArray, rolesInterestedIn = this.state.rolesInterestedIn.slice(0);
if(this.state.typeOfWork.indexOf(newSelection) > -1) {
newSelectionArray = this.state.typeOfWork.filter(s => s !== newSelection);
rolesInterestedIn = rolesInterestedIn.filter(s => s.roleType !== newSelection)
} else {
newSelectionArray = [...this.state.typeOfWork, newSelection];
rolesInterestedIn = newSelectionArray.map((workType) => {
return {
roleType: workType,
experienceYears: '',
}
});
}
this.setState({
typeOfWork: newSelectionArray,
rolesInterestedIn: rolesInterestedIn
});
}
Suggestion: Don't use multiple setState within a function, do all the calculation then use setState once to update all the values in the last.