Ensuring React set hook runs synchronously in order - javascript

I have a react state that contains an array of objects that look like this {ID: 7, LicenseNumber: 'Testing123', LicenseCreator: 7, added: true}
To manipulate the individual values of this object, I have prepared a function that looks like this:
const updateLicenseInfo = (index, licenseInfoKey, licenseInfoVal) => {
const foundLicenseInfo = { ...licenseInfo[index] };
const licenseInfoItems = [...licenseInfo];
foundLicenseInfo[licenseInfoKey] = licenseInfoVal;
licenseInfoItems[index] = foundLicenseInfo;
setLicenseInfo([...licenseInfoItems]);
};
My problem is when I run this function twice in a row, I believe the asynchronous nature of hooks only causes the last function to run.
For example, when I make an API call and then try to update the ID, then the added field, ID will be null but added will have changed. Here is that example:
postData('/api/licenseInfo/createLicenseInfoAndCreateRelationship', {
LicenseNumber,
RequestLineItemID: procurementLine.ID
})
.then((res) => res.json())
.then((r) => {
updateLicenseInfo(licenseIndex, 'ID', r.ID);
updateLicenseInfo(licenseIndex, 'added', true);
})
How Can I ensure that this function runs and then the next function runs synchronously

How about refactoring your initial function to take an array of objects as parameters, something with the structure {key: "", val: ""} and then iterating over that array in your function, adding all the values to the paired keys and calling setState only once, with all the new changes
const updateLicenseInfo = (index, pairs) => {
const foundLicenseInfo = { ...licenseInfo[index] };
const licenseInfoItems = [...licenseInfo];
pairs.forEach((pair)=>{
foundLicenseInfo[pair.key] = pair.value;
});
licenseInfoItems[index] = foundLicenseInfo;
setLicenseInfo([...licenseInfoItems]);
};
and called like this
updateLicenseInfo(licenseIndex, [{key:'ID', value:r.ID},
{key:'added', value:true}]);

Related

useMemo function running during rendering

Basically what I'm trying to do is call this 'myMemoFunction' function in a part of my code. The problem is that from what I've read in the documentation, useMemo() executes this function right on rendering, therefore the myArray parameter is still empty. Then it returns the error below.
const myMemoFunction = useMemo((myArray) => {
const users = myArray.map((a) => a.user)
return users;
})
Error:
myArray is undefined
You should use useCallback in that case
as useMemo memoizes a variable. Also I doubt it can take arguments.
Edit:
const myMemoFunction = useCallback((myArray) => {
// this won't be called on renders
const users = myArray.map((a) => a.user)
return users;
}, [] /* dont forget the dependency you want to evaluate only once */)
// later
myMemoFunction(arr);
Edit 2 with useMemo:
const myMemoVariable = useMemo(() => {
// re-evaluates each time myArray changes
const users = myArray.map((a) => a.user)
return users;
}, [myArray])
// note that we dont use myMemoVariable() to get our variable
console.log(myMemoVariable)
update your code to this
const myMemoFunction = useMemo((myArray) => {
// myArray could be undefined, that was why ? was added
const users = myArray?.map((a) => a.user)
return users;
}, [myArray]) // myArray is in a dependency of the function
your memo function will get updated with the new myArray variable
it is always good to initialize your myArray variable as an array e.g []

Why react state (useState) is updated but not updated when log it?

Hi am trying to create a simple multi file component, But the files state is not behaving as expected.
The FileUpload component works fine and when the user chooses a file and it starts uploading the onStart prop method is called. And then when it finishes successfully the 'onFinish' prop method is called. This code seems fine to the point of consoling the object in the onfinish method. it consoles an old value of the object before it was modified by the onStart Method. I expected the file object value in the console to include the buffer key since it was added when the onStart method was called but it's not there.
Example initial state files should be [] when the use effect is called on the state files should be updated to [{_id:"example_unique_id"}] then a button for upload will appear and when user chooses a file and onStart modifies the object and the state should be updated to [{_id:"example_unique_id", buffer:{}] and finally when it finishes files should be [{_id:"example_unique_id", buffer:{}] but instead here it returns [{_id:"example_unique_id"}].
What could I be missing out on?
Also, I have React Dev tools installed and it seems the state is updated well in the dev tools.
import React, { useState } from 'react'
import { useEffect } from 'react';
import unique_id from 'uniqid'
import FileUpload from "./../../components/FileUpload";
const InlineFileUpload = ({ onFilesChange }) => {
const [files, setFiles] = useState([]);
function onFinish(file, id) {
const old_object = files.filter((file) => file._id == id)[0];
console.log("old object on after upload", old_object);
}
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
};
useEffect(() => {
const new_attachments = files.filter(({ buffer }) => buffer == undefined);
if (new_attachments.length == 0) {
setFiles([...files, { _id: unique_id() }]);
}
const links = files.filter((file) => file.file !== undefined);
if (links.length !== 0) {
onFilesChange(links);
}
}, [files]);
return (
<>
{files.map((file) => {
const { _id } = file;
return ( <FileUpload
key={_id}
id={_id}
onStart={(e) => addFile(e, _id)}
onFinish={(e) => onFinish(e, _id)}
/>
);
})}
</>
);
};
export default InlineFileUpload
I think the problem is caused by the fact that your this code is not updating the state:
const addFile = (file, id) => {
const old_object = files.filter((file) => file._id == id)[0];
const index = files.indexOf(old_object);
const new_files = [...files];
new_files.splice(index, 1, { ...old_object, buffer: file });
setFiles(new_files);
}
files looks like an array of objects.
Spread operator will not do a deep copy of this array. There are a lot of examples on the internet, here is one.
let newArr = [{a : 1, b : 2},
{x : 1, y : 2},
{p: 1, q: 2}];
let arr = [...newArr];
arr[0]['a'] = 22;
console.log(arr);
console.log(newArr);
So your new_files is the same array. Splice must be making some modifications but that is in place. So when you are doing this setFiles(new_files);, you are basically setting the same reference of object as your newState. React will not detect a change, and nothing gets updated.
You have the option to implement a deep copy method for your specific code or use lodash cloneDeep.
Looking at your code, this might work for you : const new_files = JSON.parse(JSON.stringify(files)). It is a little slow, and you might lose out on properties which have values such as functions or symbols. Read
The reason you are getting the old log is because of closures.
When you do setFiles(new_files) inside addFiles function. React updates the state asynchronously, but the new state is available on next render.
The onFinish function that will be called is still from the first render, referencing files of the that render. The new render has the reference to the updated files, so next time when you log again, you will be getting the correct value.
If it's just about logging, wrap it in a useEffect hook,
useEffect(() => {
console.log(files)
}, [files);
If it's about using it in the onFinish handler, there are answers which explore these option.

ReactJS: Updating array inside object state doesn't trigger re-render

I have a react hooks function that has a state object apiDATA. In this state I store an object of structure:
{
name : "MainData", description: "MainData description", id: 6, items: [
{key: "key-1", name : "Frontend-Test", description: "Only used for front end testing", values: ["awd","asd","xad","asdf", "awdr"]},
{key: "key-2", name : "name-2", description: "qleqle", values: ["bbb","aaa","sss","ccc"]},
...
]
}
My front end displays the main data form the object as the headers and then I map each item in items. For each of these items I need to display the valuesand make them editable. I attached a picture below.
Now as you can see I have a plus button that I use to add new values. I'm using a modal for that and when I call the function to update state it does it fine and re-renders properly. Now for each of the words in the valuesI have that chip with the delete button on their side. And the delete function for that button is as follows:
const deleteItemFromConfig = (word, item) => {
const index = apiDATA.items.findIndex((x) => x.key === item.key);
let newValues = item.value.filter((keyWord) => keyWord !== word);
item.value = [...newValues];
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
apiDATA.items = [...apiDataItems];
setApiDATA(apiDATA);
}
});
};
Unfortunately this function does not re-render when I update state. And it only re-renders when I update some other state. I know the code is a bit crappy but I tried a few things to make it re-render and I can't get around it. I know it has something to do with React not seeing this as a proper update so it doesn't re-render but I have no idea why.
It is not updating because you are changing the array items inside apiDATA, and React only re-render if the pointer to apiDATA changes. React does not compare all items inside the apiDATA.
You have to create a new apiDATA to make React updates.
Try this:
if (res.result.status === 200) {
let apiDataItems = [...apiDATA.items];
apiDataItems.splice(index, 1);
apiDataItems.splice(index, 0, item);
setApiDATA(prevState => {
return {
...prevState,
items: apiDataItems
}
});
}
Using splice isn't a good idea, since it mutates the arrays in place and even if you create a copy via let apiDataItems = [...apiDATA.items];, it's still a shallow copy that has original reference to the nested values.
One of the options is to update your data with map:
const deleteItemFromConfig = (word, item) => {
api.updateConfig(item).then((res) => {
if (res.result.status === 200) {
const items = apiDATA.items.map(it => {
if (it.key === item.key) {
return {
...item,
values: item.value.filter((keyWord) => keyWord !== word)
}
}
return item;
})
setApiDATA(apiData => ({...apiData, items});
}
});
}

Accessing index of an array returns undefined

i am building a simple React app where i collect data from a Firebase realtime database and push it as an array (memberArr) into the component state via setState:
this.setState({members: memberArr})
When I log the state in the render() method, I see the contents of the array. In the React Dev Tools the state is also filled as expected.
If I now want to access the contents of the array (e.g. with this.state.members[0]) the console returns undefined.
I initialize the state like this:
constructor(props) {
super(props);
this.state = {
members: [],
};
}
My whole componentDidMount() method looks like this:
componentDidMount() {
const membersRef = firebase.database().ref(`${groupName}/members`);
membersRef.on('value', (data) => {
const memberArr = [];
data.forEach(function(snapshot){
//Gets name of member
var memberName = snapshot.val().name;
//Gets UID of member
var memberKey = snapshot.key;
//Get expenses from fetched member. When ready call function to push everything as array into component state.
this.getExpensesFromUser(memberKey).then(function(data) {
pushArray(data)
});
function pushArray(memberExpense) {
memberArr.push([memberName, memberKey, memberExpense])
};
//This works:
//memberArr.push([memberName, memberKey])
}.bind(this));
this.setState({members: memberArr})
});
}
On a side note: If I avoid calling
this.getExpensesFromUser(memberKey).then(function(data) {
pushArray(data)
});
and only use the following to push the name and key of the members:
memberArr.push([memberName, memberKey])
everything works as expected. console.log(this.state.members[0][0]) returns the name of the first member after the render() method gets triggered due to setState.
Do you have any tips for me?
As Dragos commented, your getExpensesFromUser function returns asynchronous results, so you need to make sure to only call setState once those calls have finished.
const membersRef = firebase.database().ref(`${groupName}/members`);
membersRef.on('value', (data) => {
const promises = [];
data.forEach((snapshot) => {
var memberKey = snapshot.key;
promises.push(this.getExpensesFromUser(memberKey))
});
Promise.all(promises).then((data) => {
const memberArr = data.map((item) => [memberName, memberKey, memberExpense]);
this.setState({members: memberArr})
});
});

Add key/value pair to existing array of objects

I have an array of objects that is saved into a userList useState which is composed of:
[{
firstName: "blah"
lastName: "blah2"
}
{
firstName: "test"
lastName: "test2"
}]
I have a useEffect that calls a function and returns a value. I want to store a new key and value to each user in userList.
useEffect(() => {
userList.forEach((user, index) =>
returnNewValueForNewKeyFunction(user, index).then(newValue => {
userList[index]['newKey'] = newValue
//this console.log shows new field and value
console.log(userList)
//this console.log ALSO shows new field and value
console.log(JSON.stringify(contactList[index]))
})
)
}
}, [])
This is fine if I'm operating out of console.log, but unfortunately I need to render the data onto the page.. in my render I have:
return (
<TableBody>
{userList
.map((user, index) => (
<TableRow>
<TableCell>
{user.newKey}
</TableCell>
)
user.newKey is showing as blank and it seems like the user wasn't updated at all. How can I make it so the value is actually updated and can be read from when rendering?
You shouldnt mutate your list, you should use useState to store your list, so something like this :
const [ state, setState] = useState(userList);
Then when you want to update, do something like this :
const listCopy = [...state];
//Logic to update your list here
listCopy[index][otherindex] = Value;
setState(listCopy)
Hope this helps
You are modifying your userList but not calling your set function on which means React won't know to re-render with the updated state.
Instead of mutating the current state, you should create a new array and then call the set function returned by useState with the updated array after making your changes.
It also looks like your returnNewValueForNewKeyFunction is a promise / async which means each of your item changes are happening async. You'll need to make these synchronous / wait for them all before updating your state to make your state change a single update for the UI.
E.g., putting these both together - if you are doing:
const [userList, setUserList] = useState();
You could do:
useEffect(() => {
// Since can't use an async func directly with useEffect -
// define an async func to handle your updates and call it within the useEffect func
const updateUsers = async () => {
// Create a new array for your updated state
const updatedUserList = [];
// Loop over your values inline so your can await results to make them sync
for (let index = 0; index < userList.length; index ++) {
const user = userList[index];
const newVal = await returnNewValueForNewKeyFunction(user, index);
// Create a shallow copy of the original value and add the newValue
updatedUserList[index] = { ...user, newKey: newValue };
// ... Any other logic you need
}
// Call set with the updated value so React knows to re-render
setUserList(updatedUserList);
};
// Trigger your async update
updateUsers();
}, [])

Categories

Resources