Why React Component unmounts on each useEffect dependency change?

1k Views Asked by At

I am trying to learn React by building a web application. Since I want to learn it step by step, for now I don't use Redux, I use only the React state and I have an issue.

This is my components architecture:

           App.js
             |
    _________|_________
   |                   |
Main.js              Side.js
   |                   |
Game.js              Moves.js

As you can see, I have the main file called App.js, in the left side we have the Main.js which is the central part of the application which contains Game.js where actually my game is happening. On the right side we have Side.js which is the sidebar where I want to display the moves each player does in the game. They will be displayed in Moves.js.

To be more clear think at the chess game. In the left part you actually play the game and in the right part your moves will be listed.

Now I will show you my code and explain what the problem is.

// App.js

const App = React.memo(props => {     
    let [moveList, setMovesList] = useState([]);

    return (
        <React.Fragment>  
            <div className="col-8">                                            
                <Main setMovesList={setMovesList} />                        
            </div>
            <div className="col-4">  
                <Side moveList={moveList} />
            </div>    
        </React.Fragment>
    );
});

// Main.js

const Main = React.memo(props => { 
    return (
        <React.Fragment>                
            <Game setMovesList={props.setMovesList} />
        </React.Fragment>
    );
});

// Game.js

const Game= React.memo(props => { 
    useEffect(() => {
        function executeMove(e) {
            props.setMovesList(e.target);
        }

        document.getElementById('board').addEventListener('click', executeMove, false);    

        return () => {            
            document.getElementById('board').removeEventListener('click', executeMove, false);              
        };
    }) 
    
    return (
        // render the game board
    );
});

// Side.js

const Side= React.memo(props => { 
    return (
        <React.Fragment>                
            <Moves moveList={props.moveList} />
        </React.Fragment>
    );
});

// Moves.js

const Moves= React.memo(props => { 
    let [listItems, setListItems] = useState([]);

    useEffect(() => {
        let items = [];                    

        for (let i = 0; i < props.moveList.length; i++) {
            items.push(<div key={i+1}><div>{i+1}</div><div>{props.moveList[i]}</div></div>)                                          
        }

        setListItems(items);

        return () => { 
            console.log('why this is being triggered on each move?') 
        }; 

    }, [props.moveList]);

    return (
        <React.Fragment>                
            {listItems}  
        </React.Fragment>
    );
});

As you can see on my code, I have defined the state in App.js. On the left side I pass the function which updates the state based on the moves the player does. On the right side I pass the state in order to update the view.

My problem is that on each click event inside Game.js the component Moves.js unmounts and that console.log is being triggered and I wasn't expected it to behave like that. I was expecting that it will unmount only when I change a view to another.

Any idea why this is happening ? Feel free to ask me anything if what I wrote does not make sense.

3

There are 3 best solutions below

2
On BEST ANSWER

Thanks for explaining your question so well - it was really easy to understand.

Now, the thing is, your component isn't actually unmounting. You've passed props.movesList as a dependency for the usEffect. Now the first time your useEffect is triggered, it will set up the return statement. The next time the useEffect gets triggered due to a change in props.movesList, the return statement will get executed.

If you intend to execute something on unmount of a component - shift it to another useEffect with an empty dependency array.

1
On

It seems the problem is occurred by Game element.

It triggers addEventListener on every render.

Why not use onClick event handler

/* remove this part
 useEffect(() => {
        function executeMove(e) {
            props.setMovesList(e.target);
        }

        document.getElementById('board').addEventListener('click', executeMove, false);    
    }) 

*/

const executeMove = (e) => {
     props.setMovesList(e.target);
}

return (
    <div id="board" onClick={executeMove}>
    ...
    </div>
)

If you want to use addEventListener, it should be added when the component mounted. Pass empty array([]) to useEffect as second parameter.

 useEffect(() => {
        function executeMove(e) {
            props.setMovesList(e.target);
        }

        document.getElementById('board').addEventListener('click', executeMove, false);    
    }, []) 
0
On

answering your question

The answer to your question
"why this is being triggered on each move"
would be:
"because useEffect wants to update the component with the changed state"

But I would be inclined to say: "you should not ask this question, you should not care"

understanding useEffect

You should understand useEffect as something that makes sure the state is up to date, not as a kind of lifecycle hook.

Imagine for a moment that useEffect gets called all the time, over and over again, just to make sure everything is up to date. This is not true, but this mental model might help to understand.

You don't care if and when useEffect gets called, you only care about if the state is correct.

The function returned from useEffect should clean up its own stuff (e.g. the eventlisteners), again, making sure everything is clean and up to date, but it is not a onUnmount handler.

understanding React hooks

You should get used to the idea that every functional component and every hook is called over and over again. React decides if it might not be necessary.

If you really have performance problems, you might use e.g. React.memo and useCallback, but even then, do not rely on that anything is not called anymore. React might call your function anyway, if it thinks it is necessary. Use React.memo only as kind of a hint to react to do some optimization here.

more React tips

  • work on state
  • display the state

E.g. do not create a list of <div>, as you did, instead, create a list of e.g. objects, and render that list inside the view. You might even create an own MovesView component, only displaying the list. That might be a bit too much separation in your example, but you should get used to the idea, also I assume your real component will be much bigger at the end.

Don’t be afraid to split components into smaller components.