React router 6 卸载时的副作用清理
React router 6 side-effect clean-up on unmount
迁移到 v6 后,我的 react-router 出现问题。问题是当路由器用于渲染组件时会产生副作用并且应该在卸载时进行清理。在 v6 的情况下,我没有让组件 运行 它的清理功能。我假设这是设计使然,但我想知道 how/can 我在 v6 中对 v5 逻辑建模。
我有两个最小的应用程序可以显示我的问题:
- v5: https://stackblitz.com/edit/github-xz5dvi?file=src/App.tsx ▶️ 在路径之间切换卸载之前的组件,然后渲染新组件。它看起来像这样:
- v6:https://stackblitz.com/edit/github-xz5dvi-tz4vuo?file=src/App.tsx ▶️ v6 中的相同应用程序,其中前一个组件未卸载,组件之间切换时的行为无法正常工作。它看起来像这样:
在这两个应用程序中,想法是当您导航到页面时,它会呈现一个组件,该组件修改 DOM 并删除它在卸载时添加的 DOM 元素。这在 v6 的情况下不会发生,添加的 DOM 元素保留在 DOM 中并且它们开始堆叠:
useEffect
具有依赖项应该 运行 在渲染后进行清理,所以这实际上是正确的行为。
为了运行卸载时的清理,使用useEffect
和一个空的依赖数组
React.useEffect(() => {
// Side effect where we modify dom
console.log('modifying: ', name);
const element = document.createElement('span');
element.innerText = `This text was added as a side-effect for component: ${name}`;
const container = document.getElementById(name);
container?.appendChild(element);
// Clean-up on unmount
return () => {
console.log('unmounting: ', name);
const container = document.getElementById(name);
container?.parentNode?.removeChild(container);
};
}, []);
但是如果你想实现与 react-router v5 相同的(无效的)行为,你必须强制路由器在每次其 prop 更改时不必要地重新安装相同的组件。
不推荐这样做,但你可以这样做:
const pages = [...Array(3).keys()].map((x) => (
<Route
key={`route-${x}`}
path={`component-${x}`}
element={<Component key={`component-${x}`} name={`component-${x}`} />}
/>
));
(唯一的 key
道具将强制做出反应以不重用相同的组件)
对useEffect
的解释:
useEffect
渲染后总是 运行s
- previous
useEffect
总是在 运行ning new useEffect
之前清理
所以首先是渲染,然后useEffect
有新的依赖值想要运行,所以之前的useEffect
有旧的值必须清理。
例子
interface FooProps {
id: string
}
const Foo: React.FC<FooProps> = ({ id }) => {
React.useEffect(() => {
console.log({ id })
return () => {
console.log({ id })
}
}, [id]);
return <div id={id} />
}
使用该组件时的事件顺序:
id
开始为 'a'
呈现 ID 为 'a'
的 - div
useEffect
运行s 其依赖项 id
设置为 'a'
- Foo 收到 属性
id
,值更改为 'b'
呈现 ID 为 'b'
的 - div 而不是 ID 为
'a'
的 div
useEffect
运行s 清理,其依赖项 id
设置为 'a'
useEffect
运行s 依赖性 id
设置为 'b'
因此,为了使这一点绝对清楚:
useEffect(() => {
return () => {
// This cleanup runs only on unmount
}
}, []);
useEffect(() => {
return () => {
// This cleanup runs right before this useEffect runs with updated name value
}
}, [name]);
请不要 Imperative
使用 React Declarative
结构,它有时可能会像这样咬你,(当然,命令式我的意思是在 useEffect
中添加 innerText) .
这里需要注意的几点是:
这与 react-router
你使用的 React useEffect
无关,它称你 effects
和 clean up
功能相同(如您可以通过检查 console
并看到 modifying
和 unmounting
在两个项目之间是相同的。
第二点是你的cleanup
函数,除了console.log('unmounting: ', name);
,它什么都不做,因为在这两个项目中container
都是null
.
const container = document.getElementById(name); //container is null
container?.parentNode?.removeChild(container);
这两个项目的不同之处在于,在 v6 中我们使用 Outlet
组件,并且它保留了您强制添加的 text
。
但如果您要使用任何其他需要 unsubscribe
或 cleanup
的 effect
,您可以在两个版本中完美地完成它。
总而言之,两个版本都可以正常工作,但是您添加或删除文本的方式是错误的,您可以尝试其他方式,例如根据更 Reacty 的状态显示或隐藏文本
迁移到 v6 后,我的 react-router 出现问题。问题是当路由器用于渲染组件时会产生副作用并且应该在卸载时进行清理。在 v6 的情况下,我没有让组件 运行 它的清理功能。我假设这是设计使然,但我想知道 how/can 我在 v6 中对 v5 逻辑建模。
我有两个最小的应用程序可以显示我的问题:
- v5: https://stackblitz.com/edit/github-xz5dvi?file=src/App.tsx ▶️ 在路径之间切换卸载之前的组件,然后渲染新组件。它看起来像这样:
- v6:https://stackblitz.com/edit/github-xz5dvi-tz4vuo?file=src/App.tsx ▶️ v6 中的相同应用程序,其中前一个组件未卸载,组件之间切换时的行为无法正常工作。它看起来像这样:
在这两个应用程序中,想法是当您导航到页面时,它会呈现一个组件,该组件修改 DOM 并删除它在卸载时添加的 DOM 元素。这在 v6 的情况下不会发生,添加的 DOM 元素保留在 DOM 中并且它们开始堆叠:
useEffect
具有依赖项应该 运行 在渲染后进行清理,所以这实际上是正确的行为。
为了运行卸载时的清理,使用useEffect
和一个空的依赖数组
React.useEffect(() => {
// Side effect where we modify dom
console.log('modifying: ', name);
const element = document.createElement('span');
element.innerText = `This text was added as a side-effect for component: ${name}`;
const container = document.getElementById(name);
container?.appendChild(element);
// Clean-up on unmount
return () => {
console.log('unmounting: ', name);
const container = document.getElementById(name);
container?.parentNode?.removeChild(container);
};
}, []);
但是如果你想实现与 react-router v5 相同的(无效的)行为,你必须强制路由器在每次其 prop 更改时不必要地重新安装相同的组件。
不推荐这样做,但你可以这样做:
const pages = [...Array(3).keys()].map((x) => (
<Route
key={`route-${x}`}
path={`component-${x}`}
element={<Component key={`component-${x}`} name={`component-${x}`} />}
/>
));
(唯一的 key
道具将强制做出反应以不重用相同的组件)
对useEffect
的解释:
useEffect
渲染后总是 运行s- previous
useEffect
总是在 运行ning newuseEffect
之前清理
所以首先是渲染,然后useEffect
有新的依赖值想要运行,所以之前的useEffect
有旧的值必须清理。
例子
interface FooProps {
id: string
}
const Foo: React.FC<FooProps> = ({ id }) => {
React.useEffect(() => {
console.log({ id })
return () => {
console.log({ id })
}
}, [id]);
return <div id={id} />
}
使用该组件时的事件顺序:
id
开始为'a'
呈现 ID 为 - div
useEffect
运行s 其依赖项id
设置为'a'
- Foo 收到 属性
id
,值更改为'b'
呈现 ID 为 - div 而不是 ID 为
'a'
的 div
useEffect
运行s 清理,其依赖项id
设置为'a'
useEffect
运行s 依赖性id
设置为'b'
'a'
的 'b'
的 因此,为了使这一点绝对清楚:
useEffect(() => {
return () => {
// This cleanup runs only on unmount
}
}, []);
useEffect(() => {
return () => {
// This cleanup runs right before this useEffect runs with updated name value
}
}, [name]);
请不要 Imperative
使用 React Declarative
结构,它有时可能会像这样咬你,(当然,命令式我的意思是在 useEffect
中添加 innerText) .
这里需要注意的几点是:
这与
react-router
你使用的 ReactuseEffect
无关,它称你effects
和clean up
功能相同(如您可以通过检查console
并看到modifying
和unmounting
在两个项目之间是相同的。第二点是你的
cleanup
函数,除了console.log('unmounting: ', name);
,它什么都不做,因为在这两个项目中container
都是null
.
const container = document.getElementById(name); //container is null
container?.parentNode?.removeChild(container);
这两个项目的不同之处在于,在 v6 中我们使用 Outlet
组件,并且它保留了您强制添加的 text
。
但如果您要使用任何其他需要 unsubscribe
或 cleanup
的 effect
,您可以在两个版本中完美地完成它。
总而言之,两个版本都可以正常工作,但是您添加或删除文本的方式是错误的,您可以尝试其他方式,例如根据更 Reacty 的状态显示或隐藏文本