为什么 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.memo
和 useCallback
,但即便如此,也不要依赖于不再调用任何东西。
React 可能会调用你的函数,如果它认为有必要的话。使用 React.memo
只是作为一种 反应的提示 在这里做一些优化。
更多 React 技巧
- 状态工作
- 显示状态
例如不要像您那样创建 <div>
的列表,而是创建一个列表,例如对象,并在视图中呈现该列表。您甚至可以创建自己的 MovesView
组件,只显示列表。在您的示例中,这可能有点分离,但您应该习惯这个想法,而且我假设您的实际组件最终会更大。
Don’t be afraid to split components into smaller components.
我正在尝试通过构建 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.memo
和 useCallback
,但即便如此,也不要依赖于不再调用任何东西。
React 可能会调用你的函数,如果它认为有必要的话。使用 React.memo
只是作为一种 反应的提示 在这里做一些优化。
更多 React 技巧
- 状态工作
- 显示状态
例如不要像您那样创建 <div>
的列表,而是创建一个列表,例如对象,并在视图中呈现该列表。您甚至可以创建自己的 MovesView
组件,只显示列表。在您的示例中,这可能有点分离,但您应该习惯这个想法,而且我假设您的实际组件最终会更大。
Don’t be afraid to split components into smaller components.