React useEffect dependency array: what to include - reactjs

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?

Related

React useEffect runs too often

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);
}

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.

Create and Read State for thousands of items using Recoil

I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.
My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.
I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...
In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.
Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.
To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.
Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?
Question 1
Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world πŸ˜‰
About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following
const SaveData = () => {
const saveData = useRecoilCallback(({ snapshot }) => async () => {
const ids = await snapshot.getPromise(carIds);
for (const carId of ids) {
const car = await snapshot.getPromise(cars(carId));
const carIndex = db.findIndex(({ id }) => id === carId);
db[carIndex] = car;
}
console.log("Data saved, new `db` is");
console.log(JSON.stringify(db, null, 2));
});
return <button onClick={saveData}>Save data</button>;
};
as you can see:
it retrieves all the ids through const ids = await snapshot.getPromise(carIds);
it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));
All of that in a central point, without hooks and without subscribing the component to atoms updates.
Question 2
There are a few approaches for your use case:
creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does
doing the same but initializing the atoms through RecoilRoot' initialState prop
being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution
The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update πŸ˜‰
I hope it helps, let me know if I missed something 😊

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.

Correct/Incorrect to ignore some React useEffect dependency warnings?

Here's some sample code I've written that works fine:
useEffect(() => {
if (!rolesIsLoading && rolesStatus === 200) {
customer.roles = rolesData.roles;
}
}, [rolesIsLoading, rolesStatus]);
I'm getting this warning in the console:
React Hook useEffect has missing dependencies: 'customer.roles' and 'rolesData.roles'. Either include them or remove the dependency array react-hooks/exhaustive-deps
The thing is, the code works fine now and, in some cases, when I add such dependencies, as instructed, I end up with an endless loop or some other problem.
I'd appreciate any advice you might have as to how you resolved this when you've encountered a similar situation.
From React DOCS:
Conditionally firing an effect
The default behavior for effects is to fire the effect after every completed render. That way an effect is always recreated if one of its dependencies changes.
However, this may be overkill in some cases, like the subscription example from the previous section. We don’t need to create a new subscription on every update, only if the source props has changed.
To implement this, pass a second argument to useEffect that is the array of values that the effect depends on. Our updated example now looks like this:
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
};
},
[props.source],
);
So, from the exercpt above we see that the dependencies array serves the purpose to conditionally fire an effect.
That warning you're getting is because you're using some external (from the effect's perspective) variables in your effect function that you're not mentioning in the dependency array.
React is worried that you might be getting new values for those variables in future renders, and since your effect uses them, React "default intent" is to re-run that effect with the new variables values. So your effect would always be up to date.
So you need to think if you want to re-run your effect if/when those variables change in the future.
Basic recommendations:
Add them to the dependency array
If they never change, there will be no difference
If they change, add some conditionals to your effect to decide whether you should do something based on the new variables values or not
If they're changing only in reference, but not in value (like functions, arrays, objects, etc), you can use the useCallback or useRef hook to preserve the "reference" for those variables from the first render and not get new references on every render.

Resources