为什么 React Component 在每次 useEffect 依赖项更改时卸载?

Why React Component unmounts on each useEffect dependency change?

我正在尝试通过构建 Web 应用程序来学习 React。因为想一步一步学,所以暂时不用Redux,只用React状态,遇到问题

这是我的组件架构:

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

如您所见,我有一个名为 App.js 的主文件,在左侧我们有 Main.js,它是包含 Game.js 的应用程序的中心部分,其中实际上我的游戏正在发生。在右侧,我们有 Side.js 这是侧边栏,我想在其中显示每个玩家在游戏中所做的动作。它们将显示在 Moves.js.

下棋想得更清楚。左边是你实际玩的游戏,右边是你的动作。

现在我将向您展示我的代码并解释问题所在。

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

正如您在我的代码中看到的,我已经在 App.js 中定义了状态。在左侧,我传递了根据玩家的动作更新状态的函数。在右侧,我传递状态以更新视图。

我的问题是,在 Game.js 中的每个点击事件中,组件 Moves.js 卸载并且 console.log 被触发,我没想到它会这样。我原以为只有当我将视图更改为另一个视图时它才会卸载。

知道为什么会这样吗?如果我写的有什么不明白的,请随时问我。

看来问题出在游戏元素上。

它在每次渲染时触发 addEventListener。

为什么不使用 onClick 事件处理程序

/* 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>
)

如果要使用addEventListener,需要在组件挂载时添加。将空数组 ([]) 作为第二个参数传递给 useEffect

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

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

感谢您如此详细地解释您的问题 - 它真的很容易理解。

现在,问题是,您的组件实际上并没有卸载。您已将 props.movesList 作为 usEffect 的依赖项传递。现在第一次触发 useEffect 时,它将设置 return 语句。下一次 useEffect 因 props.movesList 的变化而被触发时,return 语句将被执行。

如果您打算在卸载组件时执行某些操作 - 将其转移到另一个具有空依赖项数组的 useEffect。

回答您的问题

你问题的答案
“为什么每次移动都会触发”
将是:
"因为 useEffect 想要用更改后的状态更新组件"

但我倾向于说: “你不应该问这个问题,你不应该关心”

理解useEffect

您应该将useEffect理解为确保状态是最新的东西,而不是作为一种生命周期钩子.

想象一下,useEffect 一直被调用 ,一遍又一遍,只是为了确保一切都是最新的。这不是真的,但这种心智模型可能有助于理解。

你不关心 useEffect 是否以及何时被调用,你只关心状态是否正确。

useEffect 返回的函数应该清理它自己的东西(例如事件监听器),再次确保所有东西都是干净的和最新的,但它 不是 一个 onUnmount 处理程序。

了解 React 钩子

您应该习惯于一遍又一遍地调用每个功能组件和每个钩子的想法。 React 决定是否有必要。

如果你真的有性能问题,你可以使用例如React.memouseCallback,但即便如此,也不要依赖于不再调用任何东西。 React 可能会调用你的函数,如果它认为有必要的话。使用 React.memo 只是作为一种 反应的提示 在这里做一些优化。

更多 React 技巧

  • 状态工作
  • 显示状态

例如不要像您那样创建 <div> 的列表,而是创建一个列表,例如对象,并在视图中呈现该列表。您甚至可以创建自己的 MovesView 组件,只显示列表。在您的示例中,这可能有点分离,但您应该习惯这个想法,而且我假设您的实际组件最终会更大。

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