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

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.

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

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

react redux duplicate keys even when setting a unique key

I have an array of 6 objects which have a uid and nothing else. This is so I can repeat over them and have some placeholder content until an object is ready to be added into the array. I set a unique key when a new object is selected. However if I select the same object twice, even though I'm setting a unique key. It seems to update the unique key on the duplicate item (even though the unique key is different).
Might be easier to see the code/app in action here, an example of the problem would be clicking squirtle then blastoise, take a note of the uid's shown. Then click squirtle again and for some reason it updates the old squirtle with the new squirtles uid causing a duplicate key error. https://codesandbox.io/s/l75m9z1xwq or see code below. Math.random is just placeholder until I can get this working correctly.
const initState = {
party: [
{ uid: 0 },
{ uid: 1 },
{ uid: 2 },
{ uid: 3 },
{ uid: 4 },
{ uid: 5 }
]
};
When I click on something this is triggered:
handleClick = pokemon => {
// setup a uid, will need a better method than math.random later
pokemon.uid = Math.random();
this.props.addToParty(pokemon);
};
This then calls a dispatch which triggers the following reducer. Which essentially just checks if the object has no normal ID then replace the content with the payload sent over. It does this but also somehow updates any previous objects with the same uid even though the if statement does not run against them.
const rootReducer = (state = initState, action) => {
if (action.type === "ADD_POKEMON") {
let foundFirstEmptyPoke = false;
const newArray = state.party.map((pokemon, index) => {
if (typeof pokemon.id === "undefined" && foundFirstEmptyPoke === false) {
foundFirstEmptyPoke = true;
pokemon = action.payload; // set the data to the first object that ios empty
}
// if we get to the last pokemon and it's not empty
if (index === 5 && foundFirstEmptyPoke === false) {
pokemon = action.payload; // replace the last pokemon with the new one
}
return pokemon;
});
return {
party: newArray
};
}
return state;
};
The problem here is that, when you click to select a pokemon, you mutate the data you retrieved from the API:
handleClick = pokemon => {
pokemon.uid = Math.random(); // HERE
this.props.addToParty(pokemon);
};
You actually mutate the react state. What you should do is clone your pokemon data object, add an uid to the clone you just generated and update your redux state with it:
handleClick = pokemon => {
this.props.addToParty({
...pokemon,
uid: Math.random()
});
};
That way, no references to the actual react state are kept. Because that was what was happening when you say it updates the old squirtle with the new squirtles uid. When you tried to add another pokemon, you updated the data you retrieved from your API which was also referenced from your first pokemon slot (from your redux state).
In react/redux it's always better to not mutate objects:
this.props.addToParty({...pokemon, uid: Math.random()});
You are mutating the state. Use spread syntax *** to copy the state before updating.
return {
...state,
party: newArray
}

How do I loop all Firebase children in React Native?

ANSWER AT BOTTOM OF POST
(ALSO SEE #soutot ANSWER)
I have successfully gained text output from my Firebase code and so I know it works. Now I need to loop all the children in Firebase. Coming from swift, the way to do it is to store each item on a tempObject then append that to an array. Then you can simply use that array any way you like. This doesn't seem to work with JavaScript, or I'm doing something wrong. The fully functional FB code I now have is:
componentDidMount(){
let child = ""
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
child = snapshot.child("test1/testHeader").val()
this.setState( {
child
})
}.bind(this));
}
I can then successfully print this in console or in <Text>. Now, the problem I'm having is looping all children, adding them to an array, then using that array to display the data. In my head, this is how it should work:
componentDidMount(){
let child = ""
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
snapshot.forEach(function(childSnapshot){
let tempObject = MyFirebase
tempObject.testHeader =
childSnapshot.val()
myArray.append(tempObject) //or .push???
})
this.setState( { //PASSING VARIABLE TO STATE
child
})
}.bind(this)); //BINDING TO SET STATE
}
I know that this is obviously wrong. Creating an object like that doesn't even work... MyFirebase -class looks like this:
render() {
let testHeader = ""
let testText = ""
)
}
My Firebase database looks like this:
(I ignored subText for now)
All suggestions are much appreciated :)
WORING CODE
//FIREBASE CODE
componentDidMount(){
const ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
const categories = []
//LOOPING EACH CHILD AND PUSHING TO ARRAY
snapshot.forEach(item => {
const temp = item.val();
categories.push(temp);
return false;
});
this.setState( { //PASSING VARIABLE TO STATE
child,
categories
})
}.bind(this)); //BINDING TO SET STATE
}
According to the code you provided, it looks like it works until this point
var ref = firebase.database().ref("testCategory");
ref.once("value")
.then(function(snapshot) {
Am I correct?
From this point, if you add a console.log(snapshot.val()) it might print an array of objects, something like this:
[ { test1: { testHeader: 'FirstHeader', testText: 'FirstText' } }, { test2: { testHeader: 'SecondSub', testText: 'SecondSub } }]
Right?
If so, you can for example store this snapshot into your state and then consume this state in your render method. Something like this:
const ref = firebase.database().ref('testCategory');
ref.once('value').then((snapshot) => {
this.setState({ categories: snapshot.val() });
});
Then in your render method:
const { categories } = this.state;
categories.map(category => <Text>{category.testHeader}</Text>)
The result in your screen should be:
FirstHeader
SecondSub
Let me know if this helped
Some links that might explain more about es6 codes I used in this example:
array map categories.map(...): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
object destructuring const { categories } = this.state: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
const instead of var const ref = ...: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
setState: https://reactjs.org/docs/state-and-lifecycle.html
arrow function (snapshot) => ...: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
Some about firebase snapshots: https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot
Hope it helps

Categories

Resources