How to change object in state without making a copy - reactjs

I have a products state object that is just for front-end ease, and I want to decrement a field, but when I do, it creates a copy of the object rather than mutate the actual object. What is the React-y way to manipulate the fields without duplicates?
My download function:
const getDownload = async (id, uid, bucket_prefix, variation) => {
const getPresignedUrl = functions.httpsCallable('getPresignedUrl')
await getPresignedUrl({id: id, uid: uid, bucket_prefix: bucket_prefix ? bucket_prefix : '', variation: variation})
.then(res => {
if (Object.keys(res.data).length > 0) window.open(res.data, '_blank')
const intendedObj = ownedProds[id]
setOwnedProds({
...ownedProds,
})
})
.catch(err => console.error(err))
}
The shape of ownedProds:
ownedProds = {
{id}:
{
uid:
id:
downloads_remaining:
...
}
}
}
I basically want to target ownedProds[id] in my setState, and just make the ownedProds[id].downloads_remaining to be one less. I can do it, but it just adds another object with the same id as a key in the object.
I can move getData outside of the useEffect and just call it again, but then it's pinging my server again and getting all data all over again, and the order gets messed up so there's a shift, and it's a PITA to sort objects just because I want to mutate one property of one object in the state object. Am I being silly? Should I just call the server again for a superfluous number?

you can achieve that by passing a function to setState. The function will receive the previous value, and return an updated value. So according to what you want to do something like this should work:
setOwnedProds(ownedProds => {
ownedProds[id].downloads_remaining--;
// Or any other state manipulation you want to achieve.
return ownedProds;
});

Related

How to make a variable to remember the previous length of the list?

I'm getting API data from the backend using axios and in .then().
What I'm trying to do is when another client is added and I get it to the list, I want to make a statement that notices the new client on the list, and after it to complete the statement.
My idea is to compare the length of the list, and if in the list is added a client, the length would be added + 1.
But the thing is that every time the page is rendered, the list is re-updated as well since I'm doing it with useEffect.
So my idea is to make a variable that remembers the previous length on a list in a state, and if a new client comes to the list the condition might be clientList.length > prev.clinetListlength
useEffect(() => {
const clientParams =
"userName=" +
currentUser
"clientId=" +
currentClient
setClientList([]);
axios
.get(API + clientParams)
.then((response) => {
let newListClient = response.data.map(function (client) {
return {
...client,
id: client.cleint_id,
name: client.client_name,
created: client.created,
};
});
setClientList(newListClient);
//the logic...:
if(newListClient.length > prev.newListClient) {
console.log("test")
})
.catch((error) => console.log(error));
}, []);
What makes it hard, on my behalf is the useEffect that re-renders the list every time, so it might be hard to remember a previous length bc it is updated as a new one every time, which in this case the console.log("test") is shown every time.
You can use window.localStorage to store previous values. here is an example of some code
const prevLen = window.localStorage.getItem('prevLen')
window.localStorage.setItem('prevLen' , newListClient.length)
// you can make you're logic like so
Probably, LocalStorage will be enough for your issue. The idea is to put your length to browser level and update it, write additional useEffect
useEffect(() => {
const getSavedList = localStorage.getItem("myList");
setClientList(getSavedList);
}, [])
So, in your useEffect you should
get value from local store
compare with value from backend
set new list to strage

Is this correct way to render according to the state data using async await?

I got multiple buttons that render different modals.
The modals may render different results according to the data provided in the state.
I got 3 state brackets I need to consider
const [cartChangeStoreShow, setCartChangeStoreShow] = useState(false);
const [orderLineId, setOrderLineId] = useState('');
const [storeId, setStoreId] = useState('');
cartChangeStoreShow is for controlling the visible state of the modal
What I want to do is I wanna change OrderLineId and storeId before rendering the component.
The data will change according to the orderlineId and storeId.
The component is like this
<CartChangeStorePopUp
visibility={cartChangeStoreShow}
setCartChangeStoreShow={setCartChangeStoreShow}
orderLineId={orderLineId}
storeId={storeId}
/>
I am calling api inside CartChangeStorePopUp component according to prop data.
So I am handing the user press button like this.
<TouchableOpacity
onPress={() => renderCartChangeStore(cartItem)}>
<Text>
Change Store
</Text>
</TouchableOpacity>
const renderCartChangeStore = async cartItem => {
try {
await setOrderLineId(cartItem.orderLineId);
await setStoreId(cartItem.storeId);
} catch (err) {
console.log(err);
} finally {
setCartChangeStoreShow(true);
}
};
the code is working now but from what I read before
Async Await doesn't work properly with setState,So I wanna know if there is potential error with the code written here
To me, it does not make sense both the async/await presence and the try/catch/finally.
Async/await is useful when the function you're calling is dealing with something like I/O, time-consuming, where you cannot do anything than "wait" for the completion. Since "to wait" might be something not desirable in a UI context, the async/await pattern helps you to keep track to the "slow function", but even leave the CPU free to serve other useful tasks.
That being said, the "setXXX" functions of React.useState are not time-consuming: no I/O or similar task involves. Hence, the async/await is not applicable.
Going further, the "setXXX" functions of React.useState throw no error on setting. They're much like setting a variable like so:
var storeId = "";
function setStoreId(value) {
storeId = value;
}
That is, the try/catch/finally is quite useless.
If you want, you might optimize the code by grouping the three variables as a single immutable object. However, that's up to your real code.
const [storeState, setStoreState] = useState({
cartChangeStoreShow: false,
storeId: "",
orderLineId: ""
});
const renderCartChangeStore = cartItem => {
setStoreState({
cartChangeStoreShow: true,
storeId: cartItem.storeId,
orderLineId: cartItem.orderLineId,
});
};
Here is a more compact way to achieve the same behavior:
const renderCartChangeStore = cartItem => {
setStoreState({
cartChangeStoreShow: true,
...cartItem,
});
};
Bear in mind that is very important that you treat the storeState as immutable. That is, never ever change a field of the object, rather create a brand new object with the new field value.
At that point, the component should be called like so:
const handleCartChangeStoreShow = value => {
setStoreState({
...storeState,
cartChangeStoreShow: value,
});
}
<CartChangeStorePopUp
visibility={storeState.cartChangeStoreShow}
setCartChangeStoreShow={handleCartChangeStoreShow}
orderLineId={storeState.orderLineId}
storeId={storeState.storeId}
/>
Notice the handler to correctly alter the storeState object. Worthwhile mention how the new value is set. First, all the current storeState is copied to a fresh new object, then the new show value is also copied on the same object. However, since that happens after, it'll have an override-effect.

Trying to reset a list before filtering, but having trouble with the asynchronous states

So i have a const with an array of objects, named "data", a state that receive that data so I can update the screen when I change it, and I have a function that filter that data.
Every time that the user change the state of an HTML select, I trigger a function named handleChange that calls the filter function, so the user user can see the filtered content.
The problem is that I want to reset the state that receives the data, with the original data before filtering it, so if the user change one of the selects, it filter based on the original data, not the previous changed one, but when I try to update the state with the const data value, it doesn't work.
Here is my code
const [list, setList] = useState<IData[]>([...data]);
const [filter, setFilter] = useState<IFilter>({
name: "",
color: "",
date: "",
});
function handleChange(
key: keyof IData,
event: React.ChangeEvent<{ value: string }>
): void {
const newFilter = { ...filter };
newFilter[key as keyof IData] = event.target.value;
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
}
function filterList(): void {
const keys = Object.keys(filter) as Array<keyof IData>;
keys.forEach((key: keyof IData) => {
if (filter[key]) {
const newList = list.filter((item: IData) => item[key] === filter[key]);
setList([...newList]);
}
});
}
the problem is here
setList([...data]); // reset the data
setFilter({ ...newFilter }); // set the filter value
filterList();
apparently, when the filterList happens, the list state is not yet restarted with the data value, since if I console log it inside filterList(), it return me only the list that was previous filtered. Is there a way for me to make sure that the setList happens before filtering, so I'm sure that the list that is filtered is always based on the initial value?
You can access the updated values inside useEffect (more about useEffect), so instead of calling filterList() directly inside the handler you move it inside the hook:
React.useEffect(()=>{filterList();},[list,filter])
Alternatively, you can pass the values as parameters to the function, after you updated the state with them
filterList(newList, newFilter)
UPDATE
As you update list inside filterList you'll need some sort of a flag to indicate if you need to filter of not (I'd use useRef). Note that it would be preferable to pass parameters into filterList instead, because this way there will be one less rendering cycle. Here is a simplified example of how both can work, let me know if they make sense : https://jsfiddle.net/6xoeqkr3/

setState override existing state

I have the following function to set state:
const [study, setStudy] = useState(defaultState)
const setValue = (section, key, value) => {
setStudy({...study, [section]: {...study[section], [key]: value}})
}
But for some reason it keeps overriding the existing state, what am I doing wrong?
I call the function like this:
setValue('company', 'name', 'test')
setValue('property', 'state', 'test2')
setValue('property', 'address', 'test3')
Structure of data:
const defaultState: StudyData = {
client: {},
company: {},
property: {},
study: {}
}
I think the problem is that the study value you destruct is still an old version (a memoized one). It is the same between all setValue calls and thus your first two setValue calls do essentially nothing. If this is what's happening to you, the best way to fix it would be to give a callback function to setStudy. Any function given to a setter will get the absolute latest version of the state as parameter. Try it like this:
setStudy((latestStudy) => {
return {
...latestStudy,
[section]: {...latestStudy[section], [key]: value}
}
});
When dealing with multiple state-updating events, the standard behavior for React is to batch them. The state-updating method does not immediately update the state of the component, React just puts the update in queue to be processed later when event handling is complete. Changes are all flushed together at the end of the event and you don't see the intermediate state.
With batching, when the final set-state event executes, it has no recollection that the prevState has been updated (asynchrounous), which is why the final state appears to only reflect changes made by the last event.
Using a call-back function as the first argument for your setStudy method will help you return the intermediate states and avoid the issue of batching. The updater function will loop through and shallowly merge all updated states with the previous component state. This ensures that we receive all state-updates and use all intermediate states before ultimately arriving at the final update.
const setValue = (section, key, value) => {
setStudy(study => {
return {
...study,
[section]: { ...study[section], [key]: value }
};
});
};
Also see working sandbox: https://codesandbox.io/s/delicate-tree-vqrz7

Delete an object inside of array with firestore

I want to query the specific object by the property "id" inside the array and remove/delete the whole object. Is this possible through firestore?
My firestore structure looks like this:
For now I have function called removeCard() that takes in the docId which is the document id in firestore, and the cardId which is property "id" value inside the object.
removeCard() function:
export const removeCard = (retroId, cardId) => {
return (dispatch, getState, { getFirestore }) => {
// make async call to database
const firestore = getFirestore();
firestore.collection('retros').doc(retroId).update({
// firestore query to remove/delete this card.
}).then(() => {
dispatch({ type: 'DELETE_CARD_SUCCESS' });
}).catch(err => {
dispatch({ type: 'DELETE_CARD_ERROR' }, err);
});
}
};
But I cant figure out how to do this query or if it's even possible to do with Firestore. Any help is appreciated. Just tell me if i should provide with any more information to make my question more clear.
Actually, at the time of writing, it is not possible, with array-contains, to query for a specific property of an object stored in an array.
A possible workaround exists, which is to query for the entire object.
In your case it would be like
db.collection('retros').where("cardsAction", "array-contains", {id: "65....", content: "this is...", time: ....})
but obviously this workaround will not be useful in your case as both the time and content values may vary.
So in other words, you will probably need to change your data model.
One possible approach would be to duplicate your data (quite common in NoSQL world) and have another array with only the ids of the cardAction, like:
cardActionIds = ["65....", "5789", ....]
This way you can use array-contains and once you have found the docs corresponding to the cardActionIds you manipulate the other array (i.e. cardAction)

Resources