React does not update nested state using my setter - reactjs

I'd like to update a react state (the "app" variable) object dynamically. The state has several properties, one of them is a nested object ("app.error" object). There is a kind of setter function ("setAppData") which takes any number of [existing key]: new value pairs of the state as parameter, create a new object and it should update the state according to "temp" object.
It works as intended when I want to update "app.error" using hard-coded variable or any other "app" properties, when I try to update the "app.error" using my setter function, it does not update.
const initialAppData: IAppData = {
error: {} as IFrontEndError,
interfaceTheme: UI_THEME.LIGHT,
};
function AppProvider(props: any) {
const [app, setApp] = useState<IAppData>(initialAppData);
useMemo(() => {
window.addEventListener("error", (event) => {
errorHandler({
colno: event.colno,
filename: event.filename,
lineno: event.lineno,
message: event.message,
});
});
}, []);
const errorHandler = (event: IFrontEndError) => setAppData({error: { ...event }});
const setAppData = (newValue: TUpdateAppData) => {
let key: keyof IAppData;
let temp = getAppData();
for (key in newValue) {
if (
!Object.prototype.hasOwnProperty.call(app, key) ||
!Object.prototype.hasOwnProperty.call(newValue, key)
)
continue;
temp = {
...temp,
[key]: newValue[key],
};
}
setApp({ ...temp, error: { ...temp.error } });
};
const getAppData = () => ({ ...app });
}
Using static update (it works as intended):
const setAppData = () => {
let temp = getAppData();
temp.error = {
colno: 1,
lineno: 1,
message: "asd",
filename: "asd"
}
setApp({ ...temp, error: { ...temp.error } });
};
The structure of the "temp" objects before passing the spreaded copy to the state setter are exactly the same. "temp" object before spreading, triggered by event:
{
"error": {
"colno": 11,
"filename": "http://127.0.0.1:5173/src/components/Debug.tsx?t=1667134926865",
"lineno": 29,
"message": "Error: asd"
},
"interfaceTheme": "light-theme"
}
getAppData() after error event:
{
"error": {},
"interfaceTheme": "light-theme"
}
"temp" object before spreading using hard-coded value:
{
"error": {
"colno": 1,
"lineno": 1,
"message": "asd",
"filename": "asd"
},
"interfaceTheme": "light-theme"
}
getAppData() after execution using hard-coded value:
{
"error": {
"colno": 1,
"lineno": 1,
"message": "asd",
"filename": "asd"
},
"interfaceTheme": "light-theme"
}
What is that I don't notice?
edit: sources of the project:
https://github.com/gazsop/magus_shared
https://github.com/gazsop/magus_react

Related

Can't use the data from API when app just starts

My data is undefined when the app is started but after the refresh, the data comes perfectly.
For startup
It gives me [Unhandled promise rejection: TypeError: Object.entries requires that input parameter not be null or undefined]
But after the refresh, the data comes perfectly and everything working.
This is part of my data
Object {
"attributes": Object {
"htmlName": null,
"id": 0,
"items": Array [
Object {
"htmlName": "r_1",
"name": "m2 (Brüt)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "r_2",
"name": "m2 (Net)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "r_164",
"name": "Arsa Alanı (m2)",
"numeric": true,
"options": Object {},
"order": 0,
"required": true,
},
Object {
"htmlName": "a_137",
"name": "Oda Sayısı",
"numeric": false,
"options": Object {
"12": "1+0",
"13": "1+1",
"14": "1.5+1",
"15": "2+0",
"16": "2+1",
"17": "2.5+1",
"18": "2+2",
"19": "3+1",
"20": "3.5+1",
"21": "3+2",
"22": "4+1",
"226": "0+1",
"23": "4.5+1",
"24": "4+2",
"25": "4+3",
"26": "4+4",
"27": "5+1",
"28": "5+2",
"29": "5+3",
"30": "5+4",
"31": "6+1",
"32": "6+2",
"33": "6+3",
"34": "7+1",
"35": "7+2",
"36": "7+3",
"37": "8+1",
"38": "8+2",
"39": "8+3",
"40": "8+4",
"41": "9+1",
"42": "9+2",
"43": "9+3",
"44": "9+4",
"45": "9+5",
"46": "9+6",
"47": "10+1",
"48": "10+2",
"49": "10 Üzeri",
},
"order": 0,
"required": true,
},
api.js
export const getData = function () {
return axios
.get(
"blabla",
{
headers: {
Authorization: `blabla`,
},
}
)
.then((json) => {
if (json && json.status === 200) {
//console.log(json);
return json.data;
}
})
.catch((e) => {
console.log(e);
});
};
App.js
const [data, setData] = useState({});
const [roomValue, setRoomValue] = useState(null);
const [roomCount, setRoomCount] = useState([]);
const [isFocus, setIsFocus] = useState(false);
useEffect(() => {
getDataFunc();
//setDropdown(data.attributes.items[3].options);
}, []);
const getDataFunc = async () => {
const res = await getData();
//console.log(res);
setData(res);
console.log(data);
};
function setDropdown(query) {
const response = query;
try {
const entries = Object.entries(response);
const tempArray = [];
for (let i = 0; i < entries.length; i++) {
var key;
var value;
(key = entries[i][0]), (value = entries[i][1]);
tempArray.push({ key: value, value: key });
}
setRoomCount(tempArray);
//console.log(roomCount);
} catch (error) {
//console.log(error);
}
}
How can I fix that ?
Add a seperate useEffect to check wheather the data has been set and then only set the dropdown values
useEffect(() => {
getDataFunc();
}, []);
useEffect(() => {
if(data && data.attributes?.items[3]){
setDropdown(data.attributes.items[3].options);
}
}, [data]);
const getDataFunc = async () => {
const res = await getData();
//console.log(res);
setData(res);
console.log(data);
};
It seems like the error is caused by the attributes property being empty when you try to access it. But when you assign them one by one then it loads because the data is loaded per nested property before assigning it to the variable. Means it hasn't fully loaded yet
const response = data.attributes.items[3].options;
It outputs an error because attributes is undefined. So it's not an object, therefore, attributes.items is considered invalid
// sample
const data = {
/* attributes: {
items: {
1: {
options: 'option1'
},
2: {
options: 'option2'
},
3: {
options: 'option3'
}
}
} */
}
const specificData = data.attributes.items[3].options
console.log(specificData) //
So one solution would be using the optional chaining operator to avoid the error, it's just basically a question mark (?) after the object you are trying to access.
The response would be then 'undefined'. That way even if the attributes is empty or not, data will be assigned to the response constant then you can just add some more checking outside of that.
// sample
const data = {
/* attributes: {
items: {
1: {
options: 'option1'
},
2: {
options: 'option2'
},
3: {
options: 'option3'
}
}
} */
}
const specificData = data.attributes?.items[3].options
console.log(specificData) // outputs undefined instead of an error
Let me know if this works btw. maybe you could provide the actual api or maybe a sample api endpoint so we could test it directly. Or maybe the full code?
I've encoutered this before though I'm not 100% sure this is all I've done. But for the error I'm sure the optional chaining operator will prevent it
Try calling getData inside an async function and wait for the process to complete like this in your App.js
const [data, setData] = useState([]);
const [roomCount, setRoomCount] = useState([]);
useEffect(() => {
getDataFunc()
}, []);
const getDataFunc = async() => {
await getData(setData);
const response = data;
console.log(response);
const entries = Object.entries(response);
const tempArray = [];
for (let i = 0; i < entries.length; i++) {
var key;
var value;
(key = entries[i][0]), (value = entries[i][1]);
tempArray.push({ key: value, value: key });
}
setRoomCount(tempArray);
console.log(roomCount);
}
note: The best practice is not to directly pass the setData function to getData api call instead return the response from api and assign the response in main code like below
const response = await getData();
setData(response)
From what I see, your data.attributes has undefined value.
Please double-check everything, it is technically impossible to get data directly if data.attributes is undefined

React State Updation Issue

My component's state is as below:
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {},
});
teamMembersOptions are being mapped from the redux state teamMembersList as below:
const teamMembersList = useSelector(state => state.get_all_team_members.team)
useEffect(() => {
if (teamMembersList)
mapTeamMembers();
}, [teamMembersList])
const mapTeamMembers = () => {
const teamMembers = [];
teamMembersList.map(member => {
const memberObject = {
'value': member.id,
'label': member.first_name.charAt(0).toUpperCase() + member.first_name.slice(1) + ' ' + member.last_name.charAt(0).toUpperCase() + member.last_name.slice(1)
}
if (member.is_leader == 1) {
memberObject.label = memberObject.label + ' (owner)'
setState({
...state,
selectedTeamMember: memberObject
})
}
teamMembers.push(memberObject)
})
setState({
...state,
teamMembersOptions: teamMembers
})
}
The state variables of selectedTeamMember and teamMemberOptions are not updating, it keeps consoling empty state. Whenever I console the local array of teamMembers inside mapTeamMembers function, it logs all the values successfully teamMembersList from Redux
also logs successfully that means teamMembersList and teamMembers are not empty. But the state is not updating. Why the setState statement inside mapTeamMembers function is not updating the state?
There are a number of things going on here and lot of them cause renders to trigger more renders which is why you are getting unexpected output.
I have add useMemo() and useCallback() around the data and calculation method respectively, and added their return values to the dependency array for useEffect(). This is to avoid the useEffect dependencies change on every render.
Calling setState() within the .map() function doesn't feel like the right choice either as each time it is called a render might occur, even though you are halfway through the mapping operation. Instead I suggest, and opted for, using .reduce() on the array and returning that result which can then be used to update the state within the useEffect hook.
Have a look at the working code below and a sample output given the defined input from teamMembersList. Note: this doesn't use Redux in the example given that it more setup to prove the concept.
import { useCallback, useEffect, useMemo, useState } from "react";
export default function App() {
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {}
});
const teamMembersList = useMemo(
() => [
{ id: 1, first_name: "John", last_name: "Smith", is_leader: 0 },
{ id: 2, first_name: "Maggie", last_name: "Simpson", is_leader: 1 }
],
[]
);
const mapTeamMembers = useCallback(
() =>
teamMembersList.reduce(
(acc, member) => {
const memberObject = {
value: member.id,
label:
member.first_name.charAt(0).toUpperCase() +
member.first_name.slice(1) +
" " +
member.last_name.charAt(0).toUpperCase() +
member.last_name.slice(1)
};
if (member.is_leader === 1) {
memberObject.label = memberObject.label + " (owner)";
acc.leader = memberObject;
}
acc.teamMembers.push(memberObject);
return acc;
},
{
teamMembers: [],
leader: ""
}
),
[teamMembersList]
);
useEffect(() => {
if (teamMembersList) {
const members = mapTeamMembers();
setState({
selectedTeamMember: members.leader,
teamMembersOptions: members.teamMembers
});
}
}, [teamMembersList, mapTeamMembers, setState]);
return (
<div>
<pre>
<code>{JSON.stringify(state, null, 4)}</code>
</pre>
</div>
);
}
The above will render out:
{
"selectedTeamMember": {
"value": 2,
"label": "Maggie Simpson (owner)"
},
"teamMembersOptions": [
{
"value": 1,
"label": "John Smith"
},
{
"value": 2,
"label": "Maggie Simpson (owner)"
}
]
}
I'd consider splitting the state object into individual state items but that's really up to you and how you want to handle the data.

How can I change the array key value before passing to the state in react?

Question, I have this state coming from the backend It's a array of messages that will be store in state using useState. This state will be pass on in the child component. The problem is I want to change value of a specific key before storing it into the state.
Sample
Messages Array sample data
const messages = [
{
value: 'sample value one',
status: false,
},
{
value: 'sample value two',
status: false,
},
];
UseSelector
const messageGetById = useSelector((state) => state.messageGetById);
const { message } = messageGetById;
UseEffect
useEffect(() => {
if (message) {
setCurrentMessage(message);
}
}, [message]);
The output that I want is before passing the message into setCurrentMessage, all the value of status will be change to true.
Thanks!
You can use map method to map thought the array and change the status to true.
useEffect(() => {
if (message) {
const newMessages = messages?.map((mess) => {
return {...mess, status: true}})
setCurrentMessage(newMessages);
}}, [message]);
Set the mapped state with useEffect
useEffect(() => {
const data = [...message];
if (data.length > 0) {
data.map((ele) => ({
value: "YOUR CHANGED VALUE",
status: ele.status,
}));
setCurrentMessage(data);
}
}, [message]);

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

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));

Recursive function in Reactjs Hooks?

I want to update the state using react Hooks useState(); ?
Here is an example :
I have global state on top of the app:
const [familyTree, setFamilyTree] = useState([
{
fam_id: 1,
name: "No name",
attributes: {
"Key1": "*",
"Key2": "*",
},
children: [
{
fam_id: 2,
name: "No Name2",
attributes: {
"Key1": "*",
"Key2": "*",
},
},
],
},
]);
I have a current object to update the global state:
let res = {
fam_id: 2,
name: "No Name2",
attributes: {
"Key1": "Update this",
"Key2": "*",
},
},
Recursive function in this case helps me to update global state with matched ID, but I have problem now,
const matchAndUpdate = (updater, target) => {
if (updater.fam_id === target.fam_id) {
target.name = updater.name;
target.attributes = updater.attributes;
}
if ("children" in target && Array.isArray(target.children)) {
target.children.forEach((child) => {
matchAndUpdate(updater, child);
});
}
};
familyTree.forEach((g) => {
matchAndUpdate(res, g);
setFamilyTree({ ...g }); // here is my try, this works on start, but on secound update i got error about forEach is not a function...
});
I don't know where to update state on correct way?
Thanks, o/
Because you update state inside of forEach().
Maybe you should use .map and update state then at the end of check array.
This is the solution:
const matchAndUpdate = (updater, children) => {
return children.map(_child => {
if (updater.fam_id === _child.fam_id) {
return {
...updater,
children: _child.children && Array.isArray(_child.children) ? matchAndUpdate(updater, _child.children) : null
};
} else {
return {..._child,children: _child.children && Array.isArray(_child.children) ? matchAndUpdate(updater,_child.children) : null};
}
});
};
This will return and array of children, so you will begin from the initial array:
const finalFamily = matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
finalFamily will be the final updated array.
You can update the state like this:
// Option 1:
setFamilyTree(matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
// Option 2:
const newFamilyTree = matchAndUpdate({ fam_id: 1, name: "Name" }, familyTree);
setFamilyTree(newFamily);
--- NEXT QUESTION-- -
I understand that you want to create a method to push new children to child specified by id.
I developed a method that maintains attributes and old children:
const addChildrenToChild = (parent,numChildren) => {
const arrayChildren = [];
for (let i = 0; i < numChildren; i++) {
arrayChildren.push({
fam_id: Math.floor(Math.random() * 100),
name: "No name",
attributes: {
key1:"",
key2:""
},
});
}
return {...parent,children:parent.children && Array.isArray(parent.children) ? parent.children.concat(arrayChildren) : arrayChildren }
}
And upgrade matchAndUpdate to maintains old children
const matchAndUpdate = (updater, children) => {
return children.map(_child => {
if (updater.fam_id === _child.fam_id) {
return {
...updater,
children: updater.children
//Filter updater children
.filter(_childFiltered =>
_child.children && Array.isArray(_child.children) ?
//check if exists new child in old children
_child.children.some(
_childToCheck => _childToCheck.fam_id !== _childFiltered.fam_id
) : true
)
//concat old children and check to update
.concat(
_child.children && Array.isArray(_child.children)
? matchAndUpdate(updater, _child.children)
: []
)
};
} else {
return {
..._child,
children:
_child.children && Array.isArray(_child.children)
? matchAndUpdate(updater, _child.children)
: []
};
}
});
};
And now. You can use the other method at the same time to add new children:
// Now we are going to add new children to the first element in familyTree array, and maintains old children if it has.
const newFamilyTree = matchAndUpdate(
addChildrenToChild(familyTree[0], 10),
familyTree
);
setFamilyTree(newFamilyTree);

Resources