I cant figure out why my setStock function is not updating the state and not causing a re-render, while I have several other functions working just fine.
const addToStockOperation = async (addOperation) => {
const payload = {
...
};
const jwtToken = {
...
};
const addToStockOperationResult = await axios.put(`${apiEndpoint}/stock/addtoitem`, payload, jwtToken);
setStock((prevStock) => {
const indexOfModifiedStock = prevStock.findIndex((stock) => stock._id === addOperation.id);
console.log(prevStock[indexOfModifiedStock].operations.added.length);
prevStock[indexOfModifiedStock].operations.added = addToStockOperationResult.data.operations.added;
console.log(prevStock[indexOfModifiedStock].operations.added.length);
return prevStock;
});
};
Both console logs confirm that the modification of prevStock did happen, as the second console.log shows a length of +1 compared to the previous length, so that indicates that the desired part of prevStock was indeed updated, however, a re-render is not caused.
I have also tried making a copy of prevStock const stockCopy = {...prevStock}; and modifying the copy and returning the copy, but no change.
I have also tried simply to return 1; just to see if a re-render will get triggered, still nothing.
I have a few other similar functions that are working just fine and are causing a re-render as expected:
This one is working just fine for setting products:
const setProductsWrapper = async (product) => {
const addProductResult = await axios.post(
`${apiEndpoint}/product/one`,
payload,
token
);
addProductResult.data.name === product.name &&
setProducts((prevProducts) => [addProductResult.data, ...prevProducts]);
};
EDIT: I found the issue, silly me, stock is an array return [...stockCopy]; after modifying the copy, worked.
Returning prevStock is never going to work because it is the current state array (i.e. has reference equality with it) - you need to return a new array for a new render to be triggered. However, it seems likely that an issue is also arising with mutated state.
You're on the way there when you create the copy const stockCopy = [...prevStock], but the problem is that this only copies the state array to one level of depth. Any objects nested inside it, like .operations, will retain their reference equality to the objects in the original state array.
Mutating them directly means that when you return your copy, any effects which rely on a difference in reference equality between these sub-objects will not run because they are already equal. There is no diff-ing to be done.
To fix this you will have to deeply copy the relevant parts of the tree:
setStock((prevStock) => {
const stockCopy = [...prevStock];
const stockIndex = stockCopy.findIndex((stock) => stock._id === addOperation.id);
stockCopy[stockIndex] = {
...stockCopy[stockIndex],
operations: {
...stockCopy[stockIndex].operations,
added: addToStockOperationResult.data.operations.added
}
};
return stockCopy;
});
State mutation sandbox
This can get quite annoying (and potentially expensive) when the data structure is large enough. It's always better to avoid structures like this in immutable state if you can help it. Of course that's often not the case and there are tools to help deal with immutability that can cut down on bloated code if it starts to become an issue.
Related
export default function Raffle(){
/*......*/
setCandidates(() => {
candidates.splice(winnerIdx,1) //(*)
return [...candidates];
})
}
}
at (*) line, setCandidates Function remove 2 candidates. but below is run as expected.
setCandidates(() => {
let remain = [...candidates];
remain.splice(winnerIdx,1);
return [...remain];
})
I don't know why the splice method activate twice in first case.
what happened when i modified state in setState function?
You should provide a callback to your set state call, and then copy the argument, not mutate it. Mutating state is generally not good.
setCandidates((oldCandidates) => {
let remain = [...oldCandidates];
remain.splice(winnerIdx,1);
return [...remain];
})
I'm not sure why exactly you were having the problem you were having, but dealing with state this way is much safer and will avoid any weird behavior.
I have run into this weird behavior which I don't know how to figure out. It involves array destructuring. I know that react renders changes on state only when a new object is passed into the setLocations function, even though it doesn't render the state it still changes the data on the state which you can see by refreshing, but here, I have made an entirely new array newLocation and have populated it with new data but it does not store the data to locations at all while destructuring the array inside setLocations works.
I do not understand what makes this happen. Can someone please provide me with a response.
Thank you and the code example is below.
const searchGeoLocation = async (event) => {
event.preventDefault();
const fetchedData = await fetch(url);
const data = await fetchedData.json();
const newLocation = [];
// This works without the for each
// newLocation.push(...data);
// setLocations(newLocation);
data.forEach(element => {
newLocation.push(element)
});
// Has the right array
console.log(newLocation);
// does not work and prints an empty array
setLocations(newLocation);
console.log(locations);
// Does Work
setLocations(...newLocation);
console.log(locations);
}
I understand why this behavior happens with the comments I got, and I am going to answer my question myself just so that people who stumble upon the same issue in the future can understand as well.
It seems changes on the state are only reflected when a re-render happens. The console.log I put in the function shows the state before the re-render takes place, so when I put the console.log function in the body, the changes are being reflected in the state.
I have a context/provider that has a websocket as a state variable. Once the socket is initialized, the onMessage callback is set. The callback is something as follows:
const wsOnMessage = (message: any) => {
const data = JSON.parse(message.data);
setProgress(merge(progress, data.progress));
};
Then in the component I have something like this:
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress[pvc.metadata.uid]])
return (
{/* stuff */}
);
}
However, the effect isn't triggering when the progress variable gets updated.
The data structure of the progress variable is something like
{
"uid-here": 0.25,
"another-uid-here": 0.72,
...etc,
}
How can I get the useEffect to trigger when the property that matches pvc.metadata.uid gets updated?
Or, how can I get the component to re-render when that value gets updated?
Quoting the docs:
The function passed to useEffect will run after the render is
committed to the screen.
And that's the key part (that many seem to miss): one uses dependency list supplied to useEffect to limit its invokations, but not to set up some conditions extra to that 'after the render is committed'.
In other words, if your component is not considered updated by React, useEffect hooks just won't be called!
Now, it's not clear from your question how exactly your context (progress) looks like, but this line:
setProgress(merge(progress, data.progress));
... is highly suspicious.
See, for React to track the change in object the reference of this object should change. Now, there's a big chance setProgress just assignes value (passed as its parameter) to a variable, and doesn't do any cloning, shallow or deep.
Yet if merge in your code is similar to lodash.merge (and, again, there's a huge chance it actually is lodash.merge; JS ecosystem is not that big these days), it doesn't return a new object; instead it reassigns values from data.progress to progress and returns the latter.
It's pretty easy to check: replace the aforementioned line with...
setProgress({ ...merge(progress, data.progress) });
Now, in this case a new object will be created and its value will be passed to setProgress. I strongly suggest moving this cloning inside setProgress though; sure, you can do some checks there whether or not you should actually force value update, but even without those checks it should be performant enough.
There seems to be no problem... are you sure pvc.metadata.uid key is in the progress object?
another point: move that dependency into a separate variable after that, put it in the dependency array.
Spread operator create a new reference, so it will trigger the render
let updated = {...property};
updated[propertyname] =value;
setProperty(()=>updated);
If you use only the below code snippet, it will not re-render
let updated = property; //here property is the base object
updated[propertyname] = value;
setProperty(()=>updated);
Try [progress['pvc.metadata.uid']]
function PVCListTableRow(props: any) {
const { pvc } = props;
const { progress } = useMyContext();
useEffect(() => {
console.log('Progress', progress[pvc.metadata.uid])
}, [progress['pvc.metadata.uid']])
return (
{/* stuff */}
);
}
I want to wait to apply state updates from the back-end if a certain animation is currently running. This animation could run multiple times depending on the game scenario. I'm using react-native with hooks and firestore.
My plan was to make an array that would store objects of the incoming snapshot and the function which would use that data to update the state. When the animation ended it would set that the animation was running to false and remove the first item of the array. I'd also write a useEffect, which would remove the first item from the array if the length of the array had changed.
I was going to implement this function by checking whether this animation is running or whether there's an item in the array of future updates when the latest snapshot arrives. If that condition was true I'd add the snapshot and the update function to my array, otherwise I'd apply the state update immediately. I need to access that piece of state in all 3 of my firestore listeners.
However, in onSnapshot if I try to access my state it'll give me the initial state from when the function rendered. The one exception is I can access the state if I use the function to set the state, in this case setPlayerIsBetting and access the previous state through the function passed in as a callback to setPlayerIsBetting.
I can think of a few possible solutions, but all of them feel hacky besides the first one, which I'm having trouble implementing.
Would I get the future state updates if I modify the useEffect for the snapshots to not just run when the component is mounted? I briefly tried this, but it seems to be breaking the snapshots. Would anyone know how to implement this?
access the state through calling setPlayerIsBetting in all 3 listeners and just set setPlayerIsBetting to the previous state 99% of the time when its not supposed to be updated. Would it even re-render if nothing is actually changed? Could this cause any other problems?
Throughout the component lifecycle add snapshots and the update functions to the queue instead of just when the animation is running. This might not be optimal for performance right? I wouldn't have needed to worry about it for my initial plan to make a few state updates after an animation runs since i needed to take time to wait for the animation anyway.
I could add the state I need everywhere on the back-end so it would come in with the snapshot.
Some sort of method that removes and then adds the listeners. This feels like a bad idea.
Could redux or some sort of state management tool solve this problem? It would be a lot of work to implement it for this one issue, but maybe my apps at the point where it'd be useful anyway?
Here's my relevant code:
const Game = ({ route }) => {
const [playerIsBetting, setPlayerIsBetting] = useState({
isBetting: false,
display: false,
step: Infinity,
minimumValue: -1000000,
maximumValue: -5000,
});
const [updatesAfterAnimations, setUpdatesAfterAnimations] = useState([]);
// updatesAfterAnimations is currently always empty because I can't access the updated playerIsBetting state easily
const chipsAnimationRunningOrItemsInQueue = (snapshot, updateFunction) => {
console.log(
"in chipsAnimationRunningOrItemsInQueue playerIsBetting is: ",
playerIsBetting
); // always logs the initial state since its called from the snapshots.
// So it doesn't know when runChipsAnimation is added to the state and becomes true.
// So playerIsBetting.runChipsAnimation is undefined
const addToQueue =
playerIsBetting.runChipsAnimation || updatesAfterAnimations.length;
if (addToQueue) {
setUpdatesAfterAnimations((prevState) => {
const nextState = cloneDeep(prevState);
nextState.push({ snapshot, updateFunction });
return nextState;
});
console.log("chipsAnimationRunningOrItemsInQueue returns true!");
return true;
}
console.log("chipsAnimationRunningOrItemsInQueue returns false!");
return false;
};
// listener 1
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
const unsubscribeFromPlayerCards = db
.collection("tables")
.doc(tableId)
.collection("players")
.doc(player.uniqueId)
.collection("playerCards")
.doc(player.uniqueId)
.onSnapshot(
function (cardsSnapshot) {
if (!chipsAnimationRunningOrItemsInQueue(cardsSnapshot, updatePlayerCards)) {
updatePlayerCards(cardsSnapshot);
}
},
function (err) {
// console.log('error is: ', err);
}
);
return unsubscribeFromPlayerCards;
}, []);
};
// listener 2
useEffect(() => {
const tableId = route.params.tableId;
const db = firebase.firestore();
const unsubscribeFromPlayers = db
.collection("tables")
.doc(tableId)
.collection("players")
.onSnapshot(
function (playersSnapshot) {
console.log("in playerSnapshot playerIsBetting is: ", playerIsBetting); // also logs the initial state
console.log("in playerSnapshot playerIsBetting.runChipsAnimation is: "playerIsBetting.runChipsAnimation); // logs undefined
if (!chipsAnimationRunningOrItemsInQueue(playersSnapshot, updatePlayers)) {
updatePlayers(playersSnapshot);
}
},
(err) => {
console.log("error is: ", err);
}
);
return unsubscribeFromPlayers;
}, []);
// listener 3
useEffect(() => {
const db = firebase.firestore();
const tableId = route.params.tableId;
// console.log('tableId is: ', tableId);
const unsubscribeFromTable = db
.collection("tables")
.doc(tableId)
.onSnapshot(
(tableSnapshot) => {
if (!chipsAnimationRunningOrItemsInQueue(tableSnapshot, updateTable)) {
updateTable(tableSnapshot);
}
},
(err) => {
throw new err();
}
);
return unsubscribeFromTable;
}, []);
I ended up not going with any of the solutions I proposed.
I realized that I could access the up to date state by using a ref. How to do it is explained here: (https://medium.com/geographit/accessing-react-state-in-event-listeners-with-usestate-and-useref-hooks-8cceee73c559) And this is the relevant code sample from that post: (https://codesandbox.io/s/event-handler-use-ref-4hvxt?from-embed)
Solution #1 could've worked, but it would be difficult because I'd have to work around the cleanup function running when the animation state changes. (Why is the cleanup function from `useEffect` called on every render?)
I could work around this by having the cleanup function not call the function to unsubscribe from the listener and store the unsubscribe functions in state and put them all in a useEffect after the component mounts with a 2nd parameter that confirmed all 3 unsubscribe functions had been added to state.
But if a user went offline before those functions were in state I think there could be memory leaks.
I would go with solution #1: In the UseEffect() hooks you could put a boolean flag in so the snapshot listener is only set once per hook. Then put the animation state property in the useEffect dependency array so that each useEffect hook is triggered when the animation state changes and you can then run whatever logic you want from that condition.
I've been working on a SPA for a while and managing my global state with a custom context API, but it's been causing headaches with undesired rerenders down the tree so I thought I'd give react-easy-state a try. So far it's been great, but I'm starting to run into some issues which I assume has to do with the mutability of the global state, something which was easily solved with the custom context api implementation using a lib like immer.
Here's a simplified version of the issue I'm running into: I have a global state for managing orders. The order object primaryOrder has an array of addons into which additional items are added to the order - the list of available addons is stored in a separate store that is responsible for fetching the list from my API. The orderStore looks something like this:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
}
})
When a user selects to increases the quantity of an addon item, it's added to the addons array if it isn't already present, and if it is the qty prop of the addon is increased. The same logic applies when the quantity is reduced, except if it reaches 0 then the addon is removed from the array. This is done using the following methods on the orderStore:
const orderStore = store({
initialized: false,
isVisible: false,
primaryOrder: {
addons: [],
},
get orderAddons() {
return orderStore.primaryOrder.addons;
},
increaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
let updatedItem = {
...item,
qty: 1,
};
orderStore.primaryOrder.addons = [
...orderStore.primaryOrder.addons,
updatedItem,
];
} else {
orderStore.primaryOrder.addons[index].qty += 1;
}
console.log(orderStore.primaryOrder.addons);
},
decreaseAddonItemQty(item) {
let index = orderStore.primaryOrder.addons.findIndex(
(i) => i.id === item.id
);
if (index === -1) {
return;
} else {
// remove the item from the array if value goes 1->0
if (orderStore.primaryOrder.addons[index].qty === 1) {
console.log("removing item from array");
orderStore.primaryOrder.addons = _remove(
orderStore.primaryOrder.addons,
(i) => i.id !== item.id
);
console.log(orderStore.primaryOrder.addons);
return;
}
orderStore.primaryOrder.addons[index].qty -= 1;
}
}
})
The issue I'm running into has to do with the fact that one of my views consuming the orderStore.addons. My Product component is the consumer in this case:
const Product = (item) => {
const [qty, setQty] = useState(0);
const { id, label, thumbnailUrl, unitCost } = item;
autoEffect(() => {
if (orderStore.orderAddons.length === 0) {
setQty(0);
return;
}
console.log({ addons: orderStore.orderAddons });
let index = orderStore.orderAddons.findIndex((addon) => addon.id === id);
console.log({ index });
if (index !== -1) setQty(orderStore.findAddon(index).qty);
});
const Adder = () => {
return (
<div
className="flex"
style={{ flexDirection: "row", justifyContent: "space-between" }}
>
<div onClick={() => orderStore.decreaseAddonItemQty(item)}>-</div>
<div>{qty}</div>
<div onClick={() => orderStore.increaseAddonItemQty(item)}>+</div>
</div>
);
}
return (
<div>
<div>{label} {unitCost}</div>
<Adder />
</div>
)
}
export default view(Product)
The issue occurs when I call decreaseAddonItemQty and the item is removed from the addons array. The error is thrown in the Product component, stating that Uncaught TypeError: Cannot read property 'id' of undefined due to the fact that the array length reads as 2, despite the fact that the item has been removed ( see image below)
My assumption is that the consumer Product is reading the global store before it's completed updating, though of course I could be wrong.
What is the correct approach to use with react-easy-state to avoid this problem?
Seems like you found an auto batching bug. Just wrap your erroneous mutating code in batch until it is fixed to make it work correctly.
import { batch, store } from '#risingstack/react-easy-state'
const orderStore = store({
decreaseAddonItemQty(item) {
batch(() => {
// put your code here ...
})
}
})
Read the "Reactive renders are batched. Multiple synchronous store mutations won't result in multiple re-renders of the same component." section of the repo readme for more info about batching.
And some insight:
React updates are synchronous (as opposed to Angular and Vue) and Easy State (and all other state managers) use React setState behind the scenes to trigger re-renders. This means they are all synchronous too.
setState usually applies a big update at once while Easy State calls a dummy setState whenever you mutate a store property. This means Easy State would unnecessarily re-render way too often. To prevent this we have a batch method that blocks re-rendering until the whole contained code block is executed. This batch is automatically applied to most task sources so you don't have to worry about it, but if you call some mutating code from some exotic task source it won't be batched automatically.
We don't speak about batch a lot because it will (finally) become obsolete once Concurrent React is released. In the meantime, we are adding auto batching to as many places as possible. In the next update (in a few days) store methods will get auto batching, which will solve your issue.
You may wonder how could the absence of batching mess things up so badly. Older transparent reactivity systems (like MobX 4) would simply render the component a few times unnecessarily but they would work fine. This is because they use getters and setters to intercept get and set operations. Easy State (and MobX 5) however use Proxies which 'see a lot more'. In your case part of your browser's array.splice implementation is implemented in JS and Proxies intercept get/set operations inside array.splice. Probably array.splice is doing an array[2] = undefined before running array.length = 2 (this is just pseudo code of course). Without batching this results in exactly what you see.
I hope this helps and solves your issue until it is fixed (:
Edit: in the short term we plan to add a strict mode which will throw when store data is mutated outside store methods. This - combined with auto store method batching - will be the most complete solution to this issue until Concurrent React arrives.
Edit2: I would love to know why this was not properly batched by the auto-batch logic to cover this case with some tests. Is you repo public by any chance?