React setstate not updating object array - reactjs

I am trying to update an object array but even though the correct values are passed to the function, the array remains blank. If I try add a second object, then somehow the first one gets added and I'm not sure what I'm doing wrong.
const [qualifications, setQualifications] = useState([{}]);
function handleAddQualification(values: any) {
console.log(values);
console.log(qualifications);
setQualifications((prev) => {
return [...prev, values];
});
console.log(qualifications);
}
The values that I'm passing get logged correctly and the 2 subsequent logs of qualifications both show an empty array of objects.
I simplified the object so in my screen shot I added 'one' and the value logs correctly, but the qualifications array remains blank. If I add a second entry of 'two' then for some reason it adds 'one' to the array.
Please share some insight as to what is going on here?

Here is example how event loop works :)
In your case:
Calling handleAddQualification
Log values, qualifications
Adding setQualifications to queue as async operation
Log qualifications again with the same result as from step 3
Here can works tasks from queue which was added before setQualifications
setQualifications updates qualifications
Take a look here for better understanding https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

The state update calls are asynchronous and you can't see them by logging just after update call.
The first object is empty because you have defined it in your default state and you update your new state using the previous state because of this the empty object always stays as first element in the array. To fix this set your qualifications to
interface Qual {
subject?: string;
level?: string;
other?: string
}
const [qualifications, setQualifications] = useState<Qual[]>([]);
if you want to log state whenever it updates use an effect hook, something like this
useEffect(() => {
console.log({qualifications});
}, [qualifications])

Related

React, conditional setState with the click of a button

So, I don't know if how I want this to work is possible at all, but in my head I think it could I just can't seem to get it to work..
Basically I have an empty array of objects:
this.state = {
orders: [{}]
}
Then when I click on a button I want an order to be added (custom event because cart has no parent-child relations to the button clicked, for reasons):
pubSub.on("addToCart", (data) =>
this.setState((prevState => ({
orders: [...prevState.orders, data.order]
})
);
This works. I end up with:
orders: [
{[sku]: sku, quantity: quantity}
]
Now the problem I am stuck with is that if an order is to be added with an SKU that is already in the array, I want just the quantity to be updated.
If the SKU isn't already found in the array, I want a new order added.
So I want to check the array for the existence of a key (key SKU is the same as the value of key SKU).
So, to check an array for the existence of a key I would use:
this.state.orders.map(obj =>{
if (!(data.sku in obj)){
**setState here with order added**
}
})
I know how the conditional should look like if it finds that the key exists, and then only update the quantity.
the problem I face is that it iterates over all the orders in the array, and every time the key doesn't exist, it adds a new order.
How could I make this, so it checks for the existence of a key, and only adds a new order 1 time if that's the case, and not every time?
I want actually to first check all the keys, and then decide what to do.
With this approach, I have to decide every round of the map what should be done.
Also keep in mind that we are in the scope of a custom event handler, where I want to use data.
inside this handler, I cannot assign variables outside the scope of the map function. So basically it's or I use just setState, or I use the map function and inside there the setState. I can't use them both next to each other as data is only available in the first method call.
This is also the first time I try to use this custom event handler thing, and the scoping has tied my hands a bit.
If this makes sense, and someone can point me in the right direction, that would be greatly appreciated.
Thanks in advance!
You can pass function in setState. This function will have current state and, if you use classes, current props. In this function you create state change - part of state object that you want to update. In your case it might look like this (it's barebones and dirty, to show how you can do it):
// data is here, somwhere
this.setState(function(state, props) {
let index = state.orders.findIndex(obj => data.sku in obj);
if (index > -1) {
// update existing
return {orders: state.orders.slice(0, index).concat(
{...state.orders[index], quantity: state.orders[index].quantitiy + 1},
state.orders.slice(index+1))}
} else {
// create new
return {orders: state.orders.concat(data)}
}
})

Why does the first iteration of my update to an array in state does not appear? It will only appear after the second iteration?

I have an array postArray defined in state on Main.js.
this.state ={
step: 1,
// welcome
qNumber:1,
accountNumber:'',
amount:'',
txNumber:1,
postArray : []
}
I also have a function on Main.js which inserts new array element into postArray:
insertTx =() => {
// save transaction to array state
// create copy of the array
const copyPostArray = Object.assign([],this.state.postArray)
// insert one element into the array
copyPostArray.push({
txNumber: this.state.txNumber+"-"+this.state.accountNumber,
qNumber : this.state.qNumber,
accountNumber : this.state.accountNumber,
amount : this.state.amount
})
// save the values back to array state
this.setState({
postArray:copyPostArray
})
console.log(this.state.postArray)
console.log(this.state.txNumber)
console.log(this.state.qNumber)
console.log(this.state.accountNumber)
console.log(this.state.amount)
}
On CashDeposit.js, postArray is being updated whenever I call InsertTx function below:
continue = e => {
e.preventDefault();
this.props.nextStep();
//increment the txNumber
// this.props.incTxNumber();
this.props.insertTx();
Viewing the postArray on the console.log, it shows an empty array on first iteration. But for the second iteration, it will show the value for the first, on the third iteration will show value for the second and so on. Why does it not update current values?
setState does not happen right away. The state will always be the same values until the next render happens. If you update state, then in the same cycle reference state, you will get the old state. This would make it appear that you are one behind if you run something like:
this.setState(newValues)
console.log(this.state) // old values
Make sure that when you are referencing state you don't rely on a setState from another function. This is where hooks and useEffect come in handy.
The issue you're seeing is caused by the fact that setState does not set the state immediately, you can think of it like an asynchronous operation. So when you try to log the state values, you are getting old values because the state hasn't changed yet.
In order to get access to the new state value, you can pass a callback to setState as a second parameter: this.setState(newState, updatedState => console.log(updatedState))
This is because setState() does not immediately update state. You will not see the updated state until the next time render() is called. Because of how React reconciles, this is pretty fast, because React won't try to build the DOM until all the setState() calls have been shaken out. But it also means that, while you can't see the new state immediately in the console, you can rest assured that you will see it eventually, before it appears in the browser.
It does, however, mean you need to be sure you've got your initial state condition handled in your code. If you don't set up your state in your constructor, you'll have at least one go-around where you'll need to render without undefined state throwing errors, for example.

Store checkbox values as array in React

I have created the following demo to help me describe my question: https://codesandbox.io/s/dazzling-https-6ztj2
I have a form where I submit information and store it in a database. On another page, I retrieve this data, and set the checked property for the checkbox accordingly. This part works, in the demo this is represented by the dataFromAPI variable.
Now, the problem is that when I'd like to update the checkboxes, I get all sorts of errors and I don't know how to solve this. The ultimate goal is that I modify the form (either uncheck a checked box or vice versa) and send it off to the database - essentially this is an UPDATE operation, but again that's not visible in the demo.
Any suggestions?
Also note that I have simplified the demo, in the real app I'm working on I have multiple form elements and multiple values in the state.
I recommend you to work with an array of all the id's or whatever you want it to be your list's keys and "map" on the array like here https://reactjs.org/docs/lists-and-keys.html.
It also helps you to control each checkbox element as an item.
Neither your add or delete will work as it is.
Array.push returns the length of the new array, not the new array.
Array.splice returns a new array of the deleted items. And it mutates the original which you shouldn't do. We'll use filter instead.
Change your state setter to this:
// Since we are using the updater form of setState now, we need to persist the event.
e.persist();
setQuestion(prev => ({
...prev,
[e.target.name]: prev.topics.includes(e.target.value)
// Return false to remove the part of the array we don't want anymore
? prev.topics.filter((value) => value != e.target.value)
// Create a new array instead of mutating state
: [...prev.topics, e.target.value]
}));
As regard your example in the codesandbox you can get the expected result using the following snippet
//the idea here is if it exists then remove it otherwise add it to the array.
const handleChange = e => {
let x = data.topics.includes(e.target.value) ? data.topics.filter(item => item !== e.target.value): [...data.topics, e.target.value]
setQuestion({topics:x})
};
So you can get the idea and implement it in your actual application.
I noticed the problem with your code was that you changed the nature of question stored in state which makes it difficult to get the attribute topics when next react re-renders Also you were directly mutating the state. its best to alway use functional array manipulating methods are free from side effects like map, filter and reduce where possible.

useState React hook always returning initial value

locationHistory is always an empty array in the following code:
export function LocationHistoryProvider({ history, children }) {
const [locationHistory, setLocationHistory] = useState([])
useEffect(() => history.listen((location, action) => {
console.log('old state:', locationHistory)
const newLocationHistory = locationHistory ? [...locationHistory, location.pathname] : [location.pathname]
setLocationHistory(newLocationHistory)
}), [history])
return <LocationHistoryContext.Provider value={locationHistory}>{children}</LocationHistoryContext.Provider>
}
console.log always logs []. I have tried doing exactly the same thing in a regular react class and it works fine, which leads me to think I am using hooks wrong.
Any advice would be much appreciated.
UPDATE: Removing the second argument to useEffect ([history]) fixes it. But why? The intention is that this effect will not need to be rerun on every rerender. Becuase it shouldn't need to be. I thought that was the way effects worked.
Adding an empty array also breaks it. It seems [locationHistory] must be added as the 2nd argument to useEffect which stops it from breaking (or no 2nd argument at all). But I am confused why this stops it from breaking? history.listen should run any time the location changes. Why does useEffect need to run again every time locationHistory changes, in order to avoid the aforementioned problem?
P.S. Play around with it here: https://codesandbox.io/s/react-router-ur4d3?fontsize=14 (thanks to lissitz for doing most the leg work there)
You're setting up a listener for the history object, right?
Assuming your history object will remain the same (the very same object reference) across multiple render, this is want you should do:
Set up the listener, after 1st render (i.e: after mounting)
Remove the listener, after unmount
For this you could do it like this:
useEffect(()=>{
history.listen(()=>{//DO WHATEVER});
return () => history.unsubscribe(); // PSEUDO CODE. YOU CAN RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[]); // THIS EMPTY ARRAY MAKES SURE YOUR EFFECT WILL ONLY RUN AFTER 1ST RENDER
But if your history object will change on every render, you'll need to:
cancel the last listener (from the previous render) and
set up a new listener every time your history object changes.
useEffect(()=>{
history.listen(()=>{//DO SOMETHING});
return () => history.unsubscribe(); // PSEUDO CODE. IN THIS CASE, YOU SHOULD RETURN A FUNCTION TO CANCEL YOUR LISTENER
},[history]); // THIS ARRAY MAKES SURE YOUR EFFECT WILL RUN AFTER EVERY RENDER WITH A DIFFERENT `history` OBJECT
NOTE: setState functions are guaranteed to be the same instance across every render. So they don't need to be in the dependency array.
But if you want to access the current state inside of your useEffect. You shouldn't use it directly like you did with the locationHistory (you can, but if you do, you'll need to add it to the dependency array and your effect will run every time it changes). To avoid accessing it directly and adding it to the dependency array, you can do it like this, by using the functional form of the setState method.
setLocationHistory((prevState) => {
if (prevState.length > 0) {
// DO WHATEVER
}
return SOMETHING; // I.E.: SOMETHING WILL BE YOUR NEW STATE
});

Passing Array as Props in vue Modal

I want to pass array from one component to the other in vueJs, which i am able to do with
<add-new-admin-modal
:permissions = "permissions"
</add-new-admin-modal>
In my other component which is a modal actually,
I am receiving the props as,
props: {
permissions: {
type: Array,
default: () => []
}
}
Here when i try to change the permissions array, it reflects the parent data, As mentioned in the documentation.
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
so i tried with spread operator
data () {
return {
statePermissions: [...this.permissions]
}
}
The statePermissions array is still empty when i try above method,
I even tried with Object.assign
data () {
return {
statePermissions: Object.assign([], this.permissions)
}
}
Still it doesn't work.
In my Modal, I am accessing it as
<div v-for = "permission in statePermissions" :key = "permission.id">
...someHtml here.
</div>
The main idea is, I have a component which gets the data through an api, then i have a modal which takes this data and updates it accordingly and submit it to an api.
when the modal is closed, the parent component should need to have the unedited data, so that if modal is reopened it should get unedited data.
In the process of using Modal, My parent component remains in the same state (It neither gets mounted nor changed), so their is no point in making the request for default data again from parent.
Your problem is probably that the default value for you prop is an empty array and you're assigning it to a local variable before the property is properly populated by the parent (or it might even be a lifecycle issue).
Try moving your assignment to the local variable to the mounted() hook or even better if you wan't it to be reactive watch your property:
watch: {
permissions(newValue) {
this.statePermissions = newValue
}
}
You also don't need to ...spread an array to assign it to an array.
Since permissions is an array of objects, when you make a copy of it, the resulting array is a shallow copy meaning it will contain the same references to objects. That's why modifying the new array's values update the old array's values as well. You need to create copies of the objects inside permissions.
If permissions only contains primitives, you can do something like this:
this.statePermissions = JSON.parse(JSON.stringify(this.permissions));
If permissions is of a certain type (i.e. new Permission()), you can map it (I think this is cleaner):
this.statePermissions = this.permissions.map(x => new Permission(x.propA, x.propB, etc.));
This way, each cloned object in statePermissions will have the same properties as the object it's copied from in permissions, but it's independent so modifications won't affect the parent it was created from.
There's a few other ways in this post.

Resources