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.
CodePudding user response:
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 setState
s 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);
}