React useEffect runs too often - reactjs

In short:
I have a side effect I want to happen when a value changes, but only if a second value is true. Since both values must be in the dependency array, the side effect is also triggered when the first value is unchanged and the second value is turned 'on', but I don't want it to.
In detail:
I'm creating a game in React. I want to play (short) sound effects at various events, such as winning and losing. I have a simple helper function to play sounds, which works:
function playSound(url) {
new Audio(url).play();
}
All the game logic is in a custom hook useGame. This hook returns, among other things, a click-handler for when the user makes a move (it handles updating all the relevant states in the hook) and a winner boolean (indicates whether the game is won).
I can't call playSound in the click-handler, because that closure only has access to the pre-click states and doesn't know if the game is won after the click. So I put it in a side-effect, with a dependency array to ensure it only happens once when the game is won.
useEffect(() => {
if (winner) {
playSound(winSound);
}, [winner]);
[Note: Internally, winner is stateless; it's calculated directly from the (stateful) board. So there are no setWinner calls I can tag along with.]
This all works fine up to here. But now I want to add a user setting to mute all sounds.
const [soundIsOn, setSoundIsOn] = useState(true);
Now I have to add this as a condition to calling playSound (or pass it as an argument to it).
useEffect(() => {
if (winner && soundIsOn) {
playSound(winSound);
}, [winner, soundIsOn]);
Note that the the dependency array now needs to include soundIsOn. The problem arises when the sound is off, the user wins the game, and then the user turns sound on later. This effect will be called since soundIsOn changes, resulting in the win sound.
I've read through all the documentation and looked for similar questions. The only potential solution I've seen is to use a ref inside a custom hook to keep track of previous states and check which state has changed, but that seems a bit messy and unsatisfying. Is there a 'proper' way to deal with this type of thing? Ideally either by modifying the effect, or somehow working it into the click-handler.

Solution #1
Manage trigger event with other state not winner object itself.
const [winner, _setWinner] = useState(...);
const [shouldPlaySound, setShouldPlaySound] = useState(false);
// wrapper function (it is not required)
const setWinner = (winner) => {
setShouldPlaySound(true);
_setWinner(winner)
}
...
// if winner is selected
setWinner(...);
...
// Side effect
useEffect(() => {
if(shouldPlaySound && soundIsOn){
playSound(winSound);
}
setShouldPlaySound(false);
}, [shouldPlaySound, soundIsOn])
Also new setWinner wrapper function doesn't need to be defined with useCallback because it includes only setStates of useState.
But defining setWinner wrapper is not required. This is your choice for code readbility.
Solution #2
But I think if you need to play sound when winner is selected, just call playSound when winner is selected.
// when winner is selected
setWinner(winner);
if(soundIsOn){
playSound(winSound);
}

Related

Excessive number of pending callbacks

I'm trying to arrange the data gotten from firebase but after I arrange it the app becomes slow and if I click on a button it gives an error saying "Excessive number of pending callbacks".
useEffect(() => {
if (chats.length && users.length) {
const list = [];
chats.forEach((chat) => {
if (chat.members.includes(userId)) {
chat.members.forEach((y) => {
if (y !== userId) {
console.log("receiver: " + y);
users.forEach((z) => {
if (z.id === y) {
console.log(z);
list.push({
chat: chat,
acc: z,
user: user
});
}
});
console.log(list);
}
});
}
});
setUserChats(list);
}
}, [chats, users]);
users and chats are both states that I got from firebase on snapshot also in useEffect
One guess: Your dependencies don't work in the way you expect them to. chats and users aren't primitives, so depending on how they get created and passed, it's possible useEffect is run on every render, no matter whether chat and users have changed in structure or not. So this is what might happen:
useEffect() will be called on every rerender due to your invalid dependencies.
useEffect() calls setUserChats() on every turn. In theory, setUserChats() will check whether the new state actually differs, but in the same manner as useEffect it "fails" in the comparison and takes every list as a new state.
setState() will trigger a new render. Rinse, repeat with 1)
What you need to understand it that useEffect checks whether dependencies have changed (and setUserChats() does so as well to decide whether new state actually differs from the old one). This check relies on the identity/equal reference, i.e. on oldValue === newValue. For non-primitive values that means: it doesn't matter if oldValue and newValue look alike or are perfect clones even - if they don't share the same address in the memory, they are taken as non-equal.
Check out this thread or this library for solutions. In your case, a simple (but not really nice) solution would be to change your dependencies to [JSON.stringify(chats), JSON.stringify(users)], but there are more elaborate, performant and reliable solutions out there.
(Additionally, you forget to add userId to the dependencies. So something like [userId, JSON.stringify(chats), JSON.stringify(users)] might be more appropriate.)
Another thought though: I don't see why all that logic requires to be put into a useEffect() anyway. Just calculate the list in the component itself and call setUserChats(list). Or does it take too long?

How to compare oldValues and newValues on React Hooks useEffect? Multiple re-renders

The kinda the same problem as described here
How to compare oldValues and newValues on React Hooks useEffect?
But in my case, usePrevious hook does not help.
Imagine the form with several inputs, selects, and so on. You may want to look at https://app.uniswap.org/#/swap to make a similar visualization. There are several actions and data updates that will be happened on almost any change, which will lead to several re-renders, at least 4. For example.
I have 2 inputs, each represents a token. Base(first one) and Quote(second one).
This is a state for Base
const [base, setBase] = useState({
balance: undefined,
price: undefined,
value: initState?.base?.value,
token: initState?.base?.token,
tokenId: initState?.base?.tokenId,
});
and for Quote
const [quote, setQuote] = useState({
balance: undefined,
price: undefined,
value: initState?.quote?.value,
token: initState?.quote?.token,
tokenId: initState?.quote?.tokenId,
});
They gonna form a pair, like BTC/USD for example.
By changing token (instead of BTC I will choose ETH) in the select menu I will trigger several actions: fetching wallet balance, fetching price, and there are gonna be a few more rerenders with input view update and modal window close. So at least 4 of them are happening right now. I want to be able to compare base.token and basePrv with
const basePrv = usePrevious(base?.token); but on the second re-render base.token and basePrv gonna have the same token property already and it is an issue.
I also have the swap functionality between the inputs where I should change base with quote and quote with base like that
setBase(prevState => ({
...prevState,
base: quote
}));
setQuote(prevState => ({
...prevState,
quote: base
}));
In that case, there are no additional requests that should be triggered.
Right now I have useEffect with token dependency on it. But it will be fired each time when the token gonna be changed which will lead to additional asynchronous calls and 'tail' of requests if you gonna click fast. That's why I need to compare the token property that was before the change to understand should I make additional calls and requests because of the formation of new pair (BTC/USD becomes ETH/USD) or I should ignore that because it was just a "swap" (BTC/USD becomes USD/BTC) and there is no need to make additional calls and fetches. I just had to, well, swap them, not more.
So in my story, usePrevious hook will return the previous token property only once, and at the second and third time, it would be overwritten by multiple re-renders(other properties would be fetched) to the new one. So at the time when useEffect gonna be triggered, I would have no chance to compare the previous token property and the current one, because they will show the same.
I have several thoughts on how to solve it, but I am not sure is it right or wrong, because it seemed to me that the decisions look more imperative than declarative.
I can leave everything as it is (requests would be triggered always on any change no matter what it was. Was it a swap or user changed a pair). I can disable the swap button until all of the requests would be finished. It would solve the problem with requests 'tail'. But it is a kinda hotfix, that gonna be work, but I do not like it, because it would lead to additional unnecessary requests and it would be slow and bad for UX.
I can use a state to keep the previous pair on it right before the update by setBase or setQuote happens. It will allow me to use useEffect and compare previous pair to the current one to understand did the pair was changed, or just swapped and take the decision should I make fetches and calls or not.
I can get rid of useEffect with base.token and quote.token dependencies and handle everything inside of onClick handler. Because of that, the swap functionality would not trigger useEffect, and calls and fetches would be fired only if the user gonna click and choose something different. But as I said this option seemed a little bit odd to me.
I tried to use closure here, to "remember" the previous state of tokens, but it is kinda similar to use the current component state. Also, you have to initialize closure outside of the functional component body, and I do not see a possibility to transfer the init state into it that way, so the code becomes more spaghettified.
So any other ideas guys? I definitely missing something. Maybe that much of re-renders is an antipattern but I am not sure how to avoid that.
There could be multiple solutions to your problem. I would suggest to pick one which is easier to understand.
1. Modify the usePrevious hook
You can modify the usePrevious hook to survive multiple renders.
Tip: use JSON.stringify to compare if you think the value will be a complex object and might change the reference even for same real value.
function usePrevious(value) {
const prevRef = useRef();
const curRef = useRef();
if (value !== curRef.current){
// or, use
// if ( JSON.stringify(value) !== JSON.stringify(curRef.current)){
prevRef.current = curRef.current;
curRef.current = value;
}
return prevRef.current;
}
2. Sort useEffect dependency array
Since you're using tokens(strings) as dependency array of useEffect, and you don't mind their order (swap shouldn't change anything), sort the dependency array like
useEffect(
() => {
// do some effect
},
[base.token, quote.token].sort()
)
3. Store the currently fetched tokens.
While storing the API response data, also store the tokens(part of request) associated with that data. Now, you'll have 2 sets of tokens.
currently selected tokens
currently fetched tokens
You can chose to fetch only when the currently fetched tokens don't fulfil your needs. You can also extend this and store previous API request/responses and pick the result from them if possible.
Verdict
Out of all these, 3rd seems a nice & more standardised approach to me, but an overkill for your need (unless you want to cache previous results).
I would have gone with 2nd because of simplicity and minimalism. However, It still depends on what you find easier at the end.

RxJS and repeated events

I am new to RxJs in general but am investigating a bug in some React code in which, upon an unrelated action, an old event seems to be emitted and rendered to a display error. Think if you had two buttons that generated two messages somewhere on screen, and clicking one button was showing the message for the other button.
Being new to RxJs I'm not positive where the problem lays. I don't see a single ReplaySubject in the code, only Obserables, Subjects, and BehaviourSubjects. So this is either misuse of an RxJs feature or just some bad logic somewhere.
Anyway I found the code with the related Observable and I'm not quite sure what this person was trying to accomplish here. I have read up on combineLatest, map, and pipe, but this looks like pointless code to me. Could it also be somehow re-emitting old events? I don't see dynamic subscriptions anywhere, especially in this case.
Tldr I don't understand the intent of this code.
export interface IFeedback {
id: number
text: string
}
export interface IFeedbackMessages {
message: IFeedback | undefined
}
feedback$ = new BehaviorSubject<IFeedback | undefined>(undefined)
feedbackNotifs$: Observable<IFeedbackMessages> = combineLatest([
feedback$
]).pipe(
map(([feedback]) => ({
feedback
})
))
I also found this which maybe be an issue. In the React component that displays this message, am I wrong but does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
const FeedbackDisplay: React.FC () => {
const [feedbackNotifications, setFeedbackNotifications] = React.useState<IFeedbackMessages>()
React.useEffect(() =>
{
const sub = notification$.subscribe(setFeedbackNotifications)
return () => sub?.unsubscribe()
}, [notifications$])
}
Could it also be somehow re-emitting old events?
Yes, it probably is. BehaviorSubject has the unique property of immediately emitting the last value pushed to it as soon as you subscribe to it.
It's great when you want to model some persistent state value, and it's not good for events whose actual moment of occurrence is key. It sounds like the feedback messages you're working with fall into the second category, in which case Subject is probably a better choice.
does it look like each time this thing renders it subscribes and then unsubscribes to the above Subject?
Not exactly. useEffect accepts a callback, and within that callback you can optionally return a "cleanup" function. React will hang onto that function until the effect is triggered again, then it calls it to clean things up (which in this case consists of closing out the subscription) to make room for the next effect.
So in this case, the unsubscribe will only happen when the component is rendered with a new value for notifications$. Also worth pointing out that notifications$ will only change if it's either passed as a prop or created within the component function. If it's defined outside the function (imported from another file for example), you don't need to (and in fact should not) put it into useEffect's dependency array.

React useEffect dependency array: what to include

I am developing a small Tic-Tac-Toe game in react and it's working as I want it to do. Everything's fine except the es-linter and I don't want to just disable a rule as that's probably a bug. So here is one of two hooks I have, where the problem occurs:ยจ
useEffect(() => {
if (
board !== initialState.board &&
firstTurnIsPlayed() &&
players[currentPlayerIndex].bot
) {
// Select random empty tile
const emtpyTiles = getEmptyTiles(board);
const randomTileIndex = Math.floor(Math.random() * emtpyTiles.length);
const randomTile = emtpyTiles[randomTileIndex];
placeCurrentPlayerOnCoordinate(randomTile);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPlayerIndex]);
board players currentPlayerIndex are states, initialState is a constant with the initial state values, firstTurnIsPlayer is a local function only using currentPlayerIndex
So this is what I'm using right now and it's working like a charm. Whenever currentPlayerIndex changes and therefore it's a new turn of the next player, I'm checking if the new currentPlayer is a bot. If he's a bot, I'll automatically place his symbol on a random empty tile, otherwise I do nothing but waiting for the player to click on an empty tile.
That's how I want it to have, but not how the linter wants to, because I don't put all the other variables I use into the dependency array. I understand that I should do that in order to make sure my hook doesn't use wrong(not up-to-date) values of variables. On the other hand, if I add these variables to the dependencies array, the hook gets executed every time one of the vars in the array changes and that's way too often and in situations I don't want it to.
So my question right now is:
How can I satisfy my linter while also maintaining my working functionality?

React hooks: am I doing an anti pattern? updating state in function outside the component

I am beginning to use React (hooks only), and facing a strange issue. I am trying to reproduce the problem in a small test code, but can't get it to happen, except in my full blown app. This leads me to wonder if I'm doing something really wrong.
I have an array of objects, declared as a state. I map this array to display its content. Except that nothing gets displayed (the array is filled, but nothing gets displayed). Now if I declare an un-related state, make it a boolean which flips each time my array gets updated, then my array gets displayed properly. As if, in the render phase itself, React did not detect the array's changes.
A few things:
the array gets updated by a socketIO connection, I simulate it here with a timer
I update my array OUTSIDE of my component function, BUT providing the setter function to the update function
I also create part of the render fields outside my component function (this has no effect, just for readability in my full app)
I essence, this is what I am doing:
const updateArray = (setTestArray, setTestTag, addArray) => {
setTestArray(prevTestArray => {
let newTestArray = prevTestArray.map((data, index) => (data + addArray[index]))
return newTestArray
})
setTestTag(prevTag => {
return (!prevTag)
})
}
const renderArray = (currentTestArray) => {
return currentTestArray.map((data, index) => (
<div>
testArray[{index}]={data}
</div>
))
}
function TestPage(props) {
const [testArray, setTestArray] = useState([])
const [testTag, setTestTag] = useState(false)
useEffect(() => {
let samples = 3
let initArray= []
for (let i=0; i<samples;i++) initArray[i] = Math.random()
setTestArray(initArray)
// In real code: setup socket here...
setInterval(() => {
let addArray= []
for (let i=0; i<samples;i++) addArray[i] = Math.random()
updateArray(setTestArray, setTestTag, addArray)
}, 1000)
return (() => {
// In real code, disconnect socket here...
})
}, []);
return (
<Paper>
Array content:
{renderArray(testArray)}
<br/>
Tag: {(testTag)? 'true' : 'false'}
</Paper>
)
}
This works just fine. But, in my full app, if I comment out everything concerning "testTag", then my array content never displays. testArray's content is as expected, updates just fine, but placing a debugger inside the map section show that array as empty.
Thus my questions:
is my updateArray function a bad idea? From what I read, my prevTestArray input will always reflect the latest state value, and setTestArray is never supposed to change... This is the only way I see to handle the async calls my socket connection generate, without placing "testArray" in my useEffect dependencies (thus avoiding continuously connecting/disconnecting the socket?)
rendering outside the component, in renderArray, doesn't affect my tests (same result if I move the code inside my component), but is there a problem with this?
As a side note, my array's content is actually more complex is the real app (array of objects), I have tried placing this in this test code, it works just fine too...
Thank you!
Edit: Note that moving updateArray inside the useEffect seems to be the recommended pattern. I did that in my full app. The hook linter does not complain about any missing dependency, yet this still doesn't work in my full app.
But the question is still whether what I am doing here is wrong or not: I know it goes against the guidelines as it prevents the linter from doing its job, but it looks to me like this will still work, the previous state being accessible by default in the setter functions.
Edit #2: Shame on me... silly mistake in my real app code, the equivalent of updateArray had a shallow array copy at some place instead of a deep copy.
Why adding the flipping tag made it work is beyond me (knowing the data was then indeed properly displayed and updated), but getting rid of this mistake solved it all.
I will leave this question on, as the question still stand: is placing the state update, and part of the rendering outside the component a functional problem, or just something which might mater on hide dependencies (preventing the react hooks linter from doing its job) and thus simply bad practice?
The fact is that things work just fine now with both functions outside the component function, which makes sense based on what I understand from hooks at this point.

Resources