SetState creating duplicate entries - reactjs

I am trying to generate an array of UI controls as they're being rendered and the resulting state contains duplicate entries. Is there a good way to protect against this?
This code is fired each time a component is rendered. There are multiple tables with similar controls and I'm using this to get the maximum number of rows per control:
const newcontrols: LoadedCtrls = {
"itemCtrlType": controlType,
"rowsCount": numberRows,
};
if (this.state.loadedcontrols.length == 0) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
for (var i = 0; i < this.state.loadedcontrols.length; i++) {
if (this.state.loadedcontrols[i].itemCtrlType == controlType) {
if (this.state.loadedcontrols[i].rowsCount < numberRows) {
this.setState({
loadedcontrols: [...this.state.loadedcontrols.slice(0, i), newcontrols, ...this.state.loadedcontrols.slice(i)]
})
}
}
else {
this.setState({
loadedcontrols: [...this.state.loadedcontrols, newcontrols]
})
}
}
The result in the console is the following:
[{"itemCtrlType":"map1","rowsCount":2},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"location","rowsCount":5},{"itemCtrlType":"map2","rowsCount":3},{"itemCtrlType":"monitor","rowsCount":7},{"itemCtrlType":"monitor","rowsCount":7}]
I've attempted to use good practices with immutability but it still seems like setState is firing off with duplicates, and I know it is an asynchronous operation. So is there a way to prevent this?
EDIT: The function is triggered each time a component's fetch function outputs data, and the state is set in the component:
.then(data => {
this.setState({
results: data,
loading: false,
}, () => {
this.finishLoad(this.state.controlType, this.state.customerId, data.length);
});
});

The use of setState() can lead to very uncertain behavior; for two reasons:
setState() should not be used as a synchronous call (i.e., you rely on state being updated immediately).
Calling setState() as you are iterating over the state may not have the intended behavior (because of 1.).
You might consider a more general pattern:
let localState=this.state;
// Rely on and modify localState, as desired
this.setState(localState) // Set final state and trigger potential re-render
You may not need to specify the callback in your initial setState() call since state changes are batched and the next render is implicitly invoked.
The following read may be helpful setState() State Mutation Operation May Be Synchronous In ReactJS

Related

Issues accessing react state in firestore onSnapshot listener

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.

React useEffect forces me to add dependencies that trigger an infinite loop

Why is React forcing me with their linter plugin to add dependencies that I don't want?
For example, I want my effect to trigger only when a certan value changes, yet the linter tells me to add even functions to the dependencies, and I don't want that.
Why it forces me to do that? What do I gain from that?
/**
* Gets all items, pages, until 250th.
*/
useEffect(() => {
let mounted = true;
if (loadUntil250th && !paginationProps.complete) {
mounted && setLoading(true);
let limit = 250 - paginationProps.page * BATCH_LIMIT;
fetchListItems(paginationProps, limit, paginationProps.page * BATCH_LIMIT)
.then((results) => {
if (mounted) {
setPaginationProps({
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
setListItems(results.listItems);
setLoading(false);
}
})
.catch((err) => {
logger.log('LOADMORE FAILED:', err);
mounted && setPaginationProps({ ...paginationProps, complete: true });
mounted && setLoading(false);
});
}
return () => {
mounted = false;
};
}, [loadUntil250th]);
It wants this array of dependencies, which result in a infinite loop
[loadUntil250th, logger, paginationProps, setListItems]);
I want to understand why it is required, if I don't want them.
The 'exhaustive-deps' lint rule is designed to protect against stale closures, where useEffect references props or state used in the callback but not present in the dependency array. Since logger, paginationProps, and setListItems can theoretically change between renders, it's not safe to reference them inside useEffect without also including them in the dependency array to ensure you're always receiving and acting on up-to-date data. You can think of useEffect as essentially generating a snapshot of all state and props when it gets created and only updating that if one of its dependencies changes.
For instance, without including paginationProps in the dependencies list, if fetchListItems ever modifies the value of paginationProps then useEffect won't have access to that updated value until loadUntil250th changes.
As referenced in this answer, part of the issue is that your usage of useEffect() is unidiomatic. If all you're doing is subscribing to changes to loadUntil250th, you'd be better off moving this function elsewhere and calling it with your code that modifies loadUntil250th.
If you want to keep your code in the useEffect hook, you have a few options:
Assuming that paginationProps and setPaginationProps are derived from a useState hook, you can eliminate the dependency on paginationProps by passing a function to setPaginationProps instead of an object. So your code would become:
setPaginationProps(paginationProps => {
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
Move setListItems inside the useEffect hook if possible. This ensures that you can control whatever props/state that function depends on. If that's not possible, you have a few options. You can move the function outside the component altogether to guarantee that it doesn't depend on props or state. Alternatively, you can use the useCallback hook along with the function to control its dependencies and then list setListItems as another dependency.
It's unlikely that the logger function changes between renders, so you're probably safe keeping that inside your dependency array (although it's odd that the linter would expect that).
If you're still curious, this article is helpful at detailing how useEffect and the dependency array actually works.

Mutable global state causing issues with array length

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?

injecting a url parameter into an an API call in react native

I'm passing some params from a page to the other to make an API call. What i've notice is when i hard code the value i'm suppose to get into the http call, i get the desired results but when i inject the value from the params into the call, if fails to work. so i thought the params wasn't able to pass till i did an alert in the render function and what i realized was the alert prompts twice, the first one is empty and the second prompt brings the value from the previous page, so then meaning in my componentDidMount, the call
state = {
modalVisible: false,
posts : [],
itemID: ''
}
componentDidMount = () => {
const { navigation } = this.props;
this.setState({itemID : navigation.getParam('itemID')})
api.get('/items/'+`${this.state.itemID}`+'/test_items_details/'+`${userID}`+'/posts').then((response) => {
let data = response.data.data
this.setState({posts: data})
console.log(JSON.stringify(this.state.posts))
})
}
As per the docs, it states
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
Your code snippet is trying to access the state variable before it has in fact been updated.
You can do two things here:
Simply use navigation.getParam('itemID') so now your URL becomes /items/${navigation.getParam('itemID')}/test_item_details/${userID}/posts.
Use the setState callback, the snippet is added below.
Snippet:
this.setState({ itemID: navigation.getParam('itemID') }, () => {
// this.state.itemID is now set, you can make your API call.
});
If your only intention to use this.state.itemID is for the URL, I would opt for the first option.

When changing one specific state setting, is it necessary to restate all the other ones?

Let's say, I have a state that looks as follows.
constructor(props) {
super(props);
this.state = {
setting_a: "value-1",
setting_b: "color-green"
}
}
When I change the state of a specific setting (e.g. setting_a), I don't want other settings (e.g. setting_b) to disappear. So I also specify the other settings while changing the state. (which is easy using the spread operator ...state).
this.setState( {...this.state, setting_a: "value-2"});
I noticed though, that some tutorials restate them, and others only specify the changed key-values.
Things got just a little bit more complicated since the introduction of the Component#getDerivedStateFromProps method, ( since React 16.3 ).
static getDerivedStateFromProps(props, state) {
const oldSetting = state.setting_a;
const newSetting = props.setting_a;
if (oldSetting !== newSetting) {
// this is a very similar situation.
return ({ ...state, state.setting_a: props.setting_a});
}
return null;
}
Again, in the above example, I add all previous settings (i.e. ...state), because I don't want the other settings to be removed.
In both these cases, the same question: do I need to specifically repeat values which are already in the state ? Or are the states always merged incrementally, without removing ?
You don't need to copy the state (using spread operator or any idea) when updating the state with setState. The setState method updates the required state only:
this.setState( {setting_a: "value-2"});
So, now you will still get:
state = {
setting_a: "value-2",
setting_b: "color-green"
}
Similarly, it works like that when you return the object in getDerivedStateFromProps. The returned value is applied in the state without mutation.
You only need to copy the state when you want to update the property of state. For eg.:
// initial state
this.state = {
settings: {
a: 'value-1',
b: 'color-green'
}
}
Now, we have a and b property in settings state. So now, if you wanted to update the a, then you'll need to copy the settings:
this.setState((state) => ({settings: {...state.settings, a: 'value-2' } }))
The preceding example is with settings object. You can think similar with array of state. You may just do a google search for how to update the array without mutation?
It depends.
In your first case you could do:
this.setState( prevState => {
prevState.setting_a = "value-2";
return prevState
});
Or just go with:
this.setState({ setting_a: "value-2" });
As per React Docs State Updates are Merged.

Resources