React: Help! Multiple conflicting copies of state w/ useReducer and useEffect

Hello! I am working on the React Drum Pad project. I have been using useReducer (the React hook, not Redux) to organize/dispatch my actions, and useEffect to add an event listener to a component. Here are the relevant parts of what I have:
My reducer function:

function reducer(state, action){
switch(action.type) {

        case ACTIONS.TOGGLE_POWER:
        return {...state, isPowerOn: !state.isPowerOn, isPlaying: false }
  
        case ACTIONS.TAP_KEY:
        getDrumKitData();
        handleAudioKeyDown(action.payload);
        return {...state, isPlaying: false}

        default:
        return state;

    }

}

Power Button component:

export default function Power(){

/*I have wrapped everything in a Context containing the state/dispatch values from useReducer function*/

  const globalState = useContext(GlobalStateContext);

  const dispatch = useContext(DispatchContext);

  const state = globalState.state;


    return (

        <div id="power">

            <button className={state.isPowerOn ? "power-on" : "power-off"} id="power-button" onClick={()=> dispatch({type: ACTIONS.TOGGLE_POWER})}>

              <FontAwesomeIcon icon={faPowerOff} aria-hidden="true" />

            </button>

        </div>

        );

}

Relevant parts of Buttons Component:

export default function Buttons(){

const globalState = useContext(GlobalStateContext);
const dispatch = useContext(DispatchContext);
const state = globalState.state;

/*I have been trying to dispatch an action from here based on whether or not the power is on, but it didn't work so I have tried debugging:*/
useEffect(() => {
    window.addEventListener('keydown', ()=>console.log(state));
     
    return ()=> {window.removeEventListener('keydown', ()=>console.log(state))};
    
}, [state.isPowerOn]);

return(/*buttons component is here, but not relevant for question*/);
}

When I tap a key and the power is on, it prints a copy of the state object to the console. Fine and dandy. But when I turn the power off by clicking the power button (as dispatched in my reducer function) and then do a keydown, it prints two copies of the state to the console, one where isPowerOn is true and one where it is false. Then I turn the power on again and it prints three conflicting copies to the console-- true false true–and then four, and so forth. No wonder it isn’t working. Why is it doing that? Shouldn’t it just be creating a shallow copy of the state and updating the isPowerOn value? Do I need to use the spread operator or destructure my state object somehow? Please help!

Thank you so much.

What do the providers look like? And what is handleAudioKeyDown? If it’s triggering a side effect you’re likely to have issues, the reducer should be updating the state, nothing else. Just need slightly more context (is there any chance you can put this up on Codesandbox or similar?)

Thank you for reaching out and for taking the time to help me! It’s a big project with a lot of different files, so I can’t really put it up on codesandbox. I am going to slightly modify it for CodePen, but I haven’t done that yet. If I can’t find another way to solve this, I will definitely prioritize that and post it but first I want to see if this info would help you:
This is from my MasterContext file:

export const GlobalStateContext = React.createContext();
export const DispatchContext = React.createContext();

export default function MasterProvider( { children } ){
    
/*syntax: const [state,dispatch] = useReducer(reducerFunction, initialStateValue)*/
const [state, dispatch] = useReducer(reducer, {isPowerOn: true, currentVolume: 0.5, kitOneIsActive: true, isPlaying: false});


var drumKitData;
function getDrumKitData(...state) {
    drumKitData = { displayText: state.kitOneIsActive ? "Heater Kit" : "Smooth Piano Kit", buttonList: state.kitOneIsActive ? [{letter: "Q", keyCode: 81, url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-1.mp3"}, {letter: "W", keyCode: 87, url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-2.mp3"}, {letter: "E", keyCode: 69, url:"https://s3.amazonaws.com/freecodecamp/drums/Heater-3.mp3"}, {letter: "A", keyCode: 65, url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-4_1.mp3"}, {letter: "S", keyCode: 83, url: "https://s3.amazonaws.com/freecodecamp/drums/Heater-6.mp3"}, {letter: "D", keyCode: 68, url: "https://s3.amazonaws.com/freecodecamp/drums/Dsc_Oh.mp3"}, {letter: "Z", keyCode: 90, url: "https://s3.amazonaws.com/freecodecamp/drums/Kick_n_Hat.mp3"}, {letter: "X", keyCode: 88, url: "https://s3.amazonaws.com/freecodecamp/drums/RP4_KICK_1.mp3"}, {letter: "C", keyCode: 67, url: "https://s3.amazonaws.com/freecodecamp/drums/Cev_H2.mp3"}] : [{letter: "Q", keyCode: 81, url: "https://s3.amazonaws.com/freecodecamp/drums/Chord_1.mp3"}, {letter: "W", keyCode: 87, url: "https://s3.amazonaws.com/freecodecamp/drums/Chord_2.mp3"}, {letter: "E", keyCode: 69, url:"https://s3.amazonaws.com/freecodecamp/drums/Chord_3.mp3"}, {letter: "A", keyCode: 65, url: "https://s3.amazonaws.com/freecodecamp/drums/Give_us_a_light.mp3"}, {letter: "S", keyCode: 83, url: "https://s3.amazonaws.com/freecodecamp/drums/Dry_Ohh.mp3"}, {letter: "D", keyCode: 68, url: "https://s3.amazonaws.com/freecodecamp/drums/Bld_H1.mp3"}, {letter: "Z", keyCode: 90, url: "https://s3.amazonaws.com/freecodecamp/drums/punchy_kick_1.mp3"}, {letter: "X", keyCode: 88, url: "https://s3.amazonaws.com/freecodecamp/drums/side_stick_1.mp3"}, {letter: "C", keyCode: 67, url: "https://s3.amazonaws.com/freecodecamp/drums/Brk_Snr.mp3"}]};
    return drumKitData;
}

return(
        <GlobalStateContext.Provider value={{state, getDrumKitData}}>
            <DispatchContext.Provider value={dispatch}>
                {children}
            </DispatchContext.Provider>
        </GlobalStateContext.Provider>
);

My handleAudioKeyDown() function is in there too, but I commented out all the code within that function block to see if it’s the cause of the issues. I’m going to comment out the function name and that function within the reducer switch statement and see if that changes anything.

So you’re saying I shouldn’t call any functions within the reducer other than updating state?

Now that I looked into this, I believe I have been using impure functions throughout my project, so that’s what I’m going to work on changing right now.

It might not be that, but definitely make sure the reduce only updates state, nothing else. In this case, it may be more to do with that extra object that’s being added to the provider’s value, which depends on state. I’d strongly suggest either making that object part of the state or putting it in another provider: I can see there being issues when state updates and you need the properties for drumKitData regenerated

Bear in mind that useReducer is just like a beefed-up useState for when the state is a bit more complicated. So when you have something in the reducer that does {something that might affect the app but has nothing to do with actually updating the state}, it’ll often cause strange effects, because it’s like you have

const [example, setExample] = React.useState();

Then when you run setExample, instead of just changing the value of example, it also does something else. Which may well then cause the component to rerender, which causes it to try to keep example in sync, maybe cause the setExample to run again and so on. Or it might all work fine! Only thing is, if it doesn’t, it’s really difficult to debug. You can’t actually really do it with useState (it’s just a value and a function that only updates that value), but it’s really easy to do this by accident with useReducer. In exchange for a bit more power, it gives you more opportunities for shooting yourself in the foot.

As I say, that might not be the issue, but definitely move functions that have side effects out of the reducer: what you would do is put them in a useEffect, and have them run when some value in the state changes. The reducer should take the state and return an updated version, nothing else.

1 Like

Thank you for all the really valuable feedback. I am going to fix my reducer function and go through any other unnecessarily impure functions in my app and see if that could be causing my issues. I want to mark your answer as the solution and give you points for that but since I haven’t solved the issue, I will have to wait and come back later. But thank you so much.

Yes, definitely don’t mark as solution yet! It may well be something else, but it should hopefully be at least a little bit easier to figure out once you’ve cleaned that up :slightly_smiling_face:

1 Like

I think your problem might be this, at every rerender you add a new event listener but never remove it, because those are anonymous functions .

not sure if its true, but worth checking.

1 Like

Ooh, I will definitely check that as well and see what happens! Thank you for the feedback. So when it’s not an anonymous function, the cleanup works but if it’s anonymous it may not work?

The cleanup function can be an anonymous function, that shouldn’t matter.

Example Using Hooks

Note
We don’t have to return a named function from the effect. We called it cleanup here to clarify its purpose, but you could return an arrow function or call it something different.

1 Like

Ah ok. Thank you for the clarification.

I am not talking about the cleanup function, I meant the anonymous function passed to window.addEventListener and window.removeEventListener.

the problem is really simple you add an event to event listener but never remove it, so every time a re-render is needed and useEffect is called it adds a new anonymous handler function but doesn’t remove the previous one

a simple example, try this then change “someNamedFunc” to an anonymous function and you will see where the problem is

function App() {
  let [random, setRandom] = useState(0);
  function someNamedFunc(event) {
    console.log(`${random} key down`);
  }

  useEffect(() => {
    window.addEventListener("keydown", someNamedFunc);
    return () => {window.removeEventListener("keydown", someNamedFunc)}
  });

  return (
    <div className="App">
      <p>counter: {random}</p>
      <button onClick={() => setRandom(random + 1)}>Increment</button>
    </div>
  )
}

OK, I see what you are saying. I was just talking about the cleanup function. Sorry for any confusion.

oh, that totally makes sense. I will test this. Thank you!

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.