useMemo function running during rendering - javascript

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 []

Related

Ensuring React set hook runs synchronously in order

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

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.

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();
}, [])

How to create multiple copies of a deeply nested immutable Object?

I'm trying to get data from firebase/firestore using javascript so i made a function where i get my products collection and passing this data to reactjs state.products by setState() method
My goal is to pass these products to my react state but also keeping the original data and not changing it by manipulating it. I understand that in JavaScript whenever we are assigning the objects to a variable we are actually passing them as a reference and not copying them, so that' why i used the 3 dots (spread syntax) to copy firestore data into tempProducts[] same way to copy it in virgins[]
getData = () => {
let tempProducts = [];
let virgins = [];
db.collection("products")
.get()
.then(querySnapshot => {
querySnapshot.forEach(item => {
const singleItem = { ...item.data() };
virgins = [...virgins, singleItem];
tempProducts = [...tempProducts, singleItem];
});
tempProducts[0].inCart = true;
this.setState(
() => {
return { products: tempProducts };
},
() => {
console.log(
"this.state.products[0].inCart: " + this.state.products[0].inCart
);
console.log("tempProducts[0].inCart: " + tempProducts[0].inCart);
console.log("virgins[0].inCart: " + virgins[0].inCart);
}
);
});
};
then calling this method in componentDidMount
componentDidMount() {
this.getData();
}
So I changed the first product inCart value in tempProducts to true when I console log tempProducts value and state.products value it gives me true all fine but I'm expecting to get false when i console log virgins value but i did not. I should also mention that all inCart values are false in firestore data.
I solved the issue by passing the original firestore data to virgins[] instead of the singleItem as it is an object referenced by tempProducts[] like so virgins = [...virgins, item.data()]; also it works if i copied the singleItem object instead of referencing it like so virgins = [...virgins, { ...singleItem }]; keep in mind that i have no idea if this solutions are in fact efficient (not "memory waste") or not.

Updating json with lodash

Actually I need to handle mysite frontend fully with json objects(React and lodash).
I am getting the initial data via an ajax call we say,
starred[] //returns empty array from server
and am adding new json when user clicks on star buton it,
starred.push({'id':10,'starred':1});
if the user clicks again the starred should be 0
current_star=_findWhere(starred,{'id':10});
_.set(curren_star,'starred',0);
but when doing console.log
console.log(starred); //returns
[object{'id':10,'starred':0}]
but actually when it is repeated the global json is not updating,while am performing some other operations the json is like,
console.log(starred); //returns
[object{'id':10,'starred':1}]
How to update the global , i want once i changed the json, it should be changed ever.Should I get any idea of suggesting some better frameworks to handle json much easier.
Thanks before!
Working with arrays is complicated and usually messy. Creating an index with an object is usually much easier. You could try a basic state manager like the following:
// This is your "global" store. Could be in a file called store.js
// lodash/fp not necessary but it's what I always use.
// https://github.com/lodash/lodash/wiki/FP-Guide
import { flow, get, set } from 'lodash/fp'
// Most basic store creator.
function createStore() {
let state = {}
return {
get: path => get(path, state),
set: (path, value) => { state = set(path, value, state) },
}
}
// Create a new store instance. Only once per "app".
export const store = createStore()
// STARRED STATE HANDLERS
// Send it an id and get back the path where starred objects will be placed.
// Objects keyed with numbers can get confusing. Creating a string key.
const starPath = id => ['starred', `s_${id}`]
// Send it an id and fieldId and return back path where object will be placed.
const starField = (id, field) => starPath(id).concat(field)
// import to other files as needed
// Add or replace a star entry.
export const addStar = item => store.set(starPath(item.id), item)
// Get a star entry by id.
export const getStar = flow(starPath, store.get)
// Get all stars. Could wrap in _.values() if you want an array returned.
export const getStars = () => store.get('starred')
// Unstar by id. Sets 'starred' field to 0.
export const unStar = id => store.set(starField(id, 'starred'), 0)
// This could be in a different file.
// import { addStar, getStar, getStars } from './store'
console.log('all stars before any entries added:', getStars()) // => undefined
const newItem = { id: 10, starred: 1 }
addStar(newItem)
const star10a = getStar(10)
console.log('return newItem:', newItem === star10a) // => exact match true
console.log('star 10 after unstar:', star10a) // => { id: 10, starred: 1 }
console.log('all stars after new:', getStars())
// Each request of getStar(10) will return same object until it is edited.
const star10b = getStar(10)
console.log('return same object:', star10a === star10b) // => exact match true
console.log('return same object:', newItem === star10b) // => exact match true
unStar(10)
const star10c = getStar(10)
console.log('new object after mutate:', newItem !== star10c) // => no match true
console.log('star 10 after unstar:', getStar(10)) // => { id: 10, starred: 0 }
console.log('all stars after unstar:', getStars())
I think the problem is in mutating original state.
Instead of making push, you need to do the following f.e.:
var state = {
starred: []
};
//perform push
var newItem = {id:10, starred:1};
state.starred = state.starred.concat(newItem);
console.log(state.starred);
//{ id: 10, starred: 1 }]
var newStarred = _.extend({}, state.starred);
var curr = _.findWhere(newStarred, {id: 10});
curr.starred = 0;
state = _.extend({}, state, {starred: newStarred});
console.log(state.starred)
//{ id: 10, starred: 0 }]
To solve this in a more nice looking fashion, you need to use either React's immutability helper, or ES6 stuff, like: {...state, {starred: []}} instead of extending new object every time. Or just use react-redux =)

Categories

Resources