我们如何使用 React Hooks 实现 componentWillUnmount?
How can we implement componentWillUnmount using react hooks?
componentWillUnmount()
方法 在 卸载和销毁组件之前立即调用。如果我们使用 useEffect
和一个空数组 ([]) 作为第二个参数,并将我们的函数放在 return 语句中,它将在卸载组件后执行,甚至在安装另一个组件后执行。据我了解,这样做是出于性能原因。为了不耽误渲染。
所以问题是 - 我们如何在卸载组件之前使用挂钩调用某些函数?
我想做的是一个应用程序,它可以在用户键入时保存他的输入(无需提交表单)。我使用 setInterval 每 N 秒保存一次更新的文本。我需要在卸载组件之前强制保存更新。我不想在导航之前通过反应路由器使用提示。这是一个电子申请。我很感激任何有关如何实现此类功能的想法或建议。
更新
不幸的是,Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?。它基本上意味着清理是 运行 在卸载组件之后,它与在 componentWillUnmount()
中执行代码不同。如果我在清理代码和另一个组件中放置 console.log 语句,我可以清楚地看到调用顺序。问题是我们是否可以在使用挂钩卸载组件之前执行一些代码。
更新2
据我所知,我应该更好地描述我的用例。让我们想象一个理论上的应用程序,它将其数据保存在 Redux 存储中。我们有两个具有某些形式的组件。为简单起见,我们没有任何后端或任何异步逻辑。我们只使用 Redux 存储作为数据存储。
我们不想在每次击键时更新 Redux 存储。因此,我们将实际值保存在本地组件的状态中,当组件安装时,我们使用商店中的值对其进行初始化。我们还创建了一个将 setInterval 设置为 1s 的效果。
我们有以下流程。用户键入内容。更新存储在本地组件状态中,直到我们的 setInterval 回调被调用。回调只是将数据放入商店(调度操作)。我们将回调放在 useEffect return 语句中以在组件卸载时强制保存到存储,因为我们希望在这种情况下尽快保存数据以进行存储。
当用户在第一个组件中键入内容并立即转到第二个组件(快于 1 秒)时,问题就来了。由于我们的第一个组件中的清理将在重新渲染后调用,因此在安装第二个组件之前我们的商店不会更新。因此,第二个组件将获得过时的值到其本地状态。
如果我们将回调放在 componentWillUnmount()
中,它会在卸载之前调用,并且存储将在下一个组件安装之前更新。那么我们可以使用钩子来实现吗?
componentWillUnmount
可以通过在 useEffect
钩子中返回一个函数来模拟。返回的函数将在每次重新渲染组件之前调用。严格来说,这是同一件事,但您应该能够使用它来模拟您想要的任何行为。
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
更新
以上将 运行 每次重新渲染。但是,仅模拟安装和卸载时的行为(即 componentDidMount 和 componentWillUnmount)。 useEffect 需要一个 second argument ,它需要是一个空数组。
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
查看同一问题的更详细解释 。
从 useEffect
返回的函数在组件卸载之前或之后被调用并不重要:您仍然可以通过闭包访问状态 valuey:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
如果您不管理组件状态中的输入,您的整个结构就会被破坏,应该更改为在正确的位置管理状态。在您的情况下,您应该将组件的共享输入状态提升到父级。
关于挂钩的 ReactJS 文档指定了这一点:
Effects may also optionally specify how to “clean up” after them by
returning a function.
因此,您 return 在 useEffect
挂钩中的任何功能都将在组件卸载时执行,以及在重新 运行 由于后续渲染而产生的效果之前执行。
这里的问题是如何在卸载之前运行 使用钩子编写代码? return 函数带有挂钩 运行s AFTER unmount,虽然这对大多数用例没有影响,但在某些情况下是关键差异。
对此做了一些调查,我得出的结论是,目前的 hooks 根本没有提供 componentWillUnmount
的直接替代方案。所以如果你有一个需要它的用例,至少对我来说主要是非 React 库的集成,你只需要用旧的方法来做,使用一个组件。
更新: 请参阅下面关于 UseLayoutEffect()
的答案,看起来可以解决此问题。
经过一些研究,发现 - 您仍然可以完成此操作。有点棘手,但应该可以。
您可以使用 useRef 并将要使用的道具存储在闭包中,例如 render useEffect return 回调方法
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
然而,更好的方法是将第二个参数传递给 useEffect
,以便在所需道具发生任何更改时进行清理和初始化
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
自从引入 useLayoutEffect
钩子后,您现在可以做
useLayoutEffect(() => () => {
// Your code here.
}, [])
模拟componentWillUnmount
。这在卸载期间运行,但在元素实际离开页面之前。
我同意 Frank 的观点,但代码需要看起来像这样,否则它将 运行 仅在第一次渲染时出现:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
这相当于 ComponentWillUnmount
类似于@pritam 的回答,但有一个抽象的代码示例。 useRef 的整个想法是让你跟踪回调的变化,而不是在执行时有一个陈旧的关闭。因此,底部的 useEffect 可以有一个空的依赖数组,以确保它只在组件卸载时运行。 See the code demo.
可重复使用的挂钩:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
我遇到了一个独特的情况,useEffect(() => () => { ... }, []);
答案对我不起作用。这是因为我的组件 从未被渲染过 — 我在注册 useEffect
钩子之前抛出了一个异常。
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
在上面的例子中,通过抛出一个 Promise<T>
告诉反应暂停,直到承诺被解决。然而,一旦 promise 被解决,就会抛出一个错误。由于组件永远不会被渲染并直接进入 ErrorBoundary,因此 useEffect()
挂钩永远不会被注册!
如果您的情况与我相似,这段小代码可能会有所帮助:
为了解决这个问题,我将 ErrorBoundary
代码修改为 运行 恢复后的拆解列表
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
然后,我创建了一个 useTeardown
挂钩,它将添加需要 运行 的拆解,或者尽可能使用 useEffect
。如果你有错误边界嵌套,你很可能需要修改它,但对于我的简单用例,它工作得很好。
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}
componentWillUnmount()
方法 在 卸载和销毁组件之前立即调用。如果我们使用 useEffect
和一个空数组 ([]) 作为第二个参数,并将我们的函数放在 return 语句中,它将在卸载组件后执行,甚至在安装另一个组件后执行。据我了解,这样做是出于性能原因。为了不耽误渲染。
所以问题是 - 我们如何在卸载组件之前使用挂钩调用某些函数?
我想做的是一个应用程序,它可以在用户键入时保存他的输入(无需提交表单)。我使用 setInterval 每 N 秒保存一次更新的文本。我需要在卸载组件之前强制保存更新。我不想在导航之前通过反应路由器使用提示。这是一个电子申请。我很感激任何有关如何实现此类功能的想法或建议。
更新
不幸的是,Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?。它基本上意味着清理是 运行 在卸载组件之后,它与在 componentWillUnmount()
中执行代码不同。如果我在清理代码和另一个组件中放置 console.log 语句,我可以清楚地看到调用顺序。问题是我们是否可以在使用挂钩卸载组件之前执行一些代码。
更新2
据我所知,我应该更好地描述我的用例。让我们想象一个理论上的应用程序,它将其数据保存在 Redux 存储中。我们有两个具有某些形式的组件。为简单起见,我们没有任何后端或任何异步逻辑。我们只使用 Redux 存储作为数据存储。
我们不想在每次击键时更新 Redux 存储。因此,我们将实际值保存在本地组件的状态中,当组件安装时,我们使用商店中的值对其进行初始化。我们还创建了一个将 setInterval 设置为 1s 的效果。
我们有以下流程。用户键入内容。更新存储在本地组件状态中,直到我们的 setInterval 回调被调用。回调只是将数据放入商店(调度操作)。我们将回调放在 useEffect return 语句中以在组件卸载时强制保存到存储,因为我们希望在这种情况下尽快保存数据以进行存储。
当用户在第一个组件中键入内容并立即转到第二个组件(快于 1 秒)时,问题就来了。由于我们的第一个组件中的清理将在重新渲染后调用,因此在安装第二个组件之前我们的商店不会更新。因此,第二个组件将获得过时的值到其本地状态。
如果我们将回调放在 componentWillUnmount()
中,它会在卸载之前调用,并且存储将在下一个组件安装之前更新。那么我们可以使用钩子来实现吗?
componentWillUnmount
可以通过在 useEffect
钩子中返回一个函数来模拟。返回的函数将在每次重新渲染组件之前调用。严格来说,这是同一件事,但您应该能够使用它来模拟您想要的任何行为。
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
更新
以上将 运行 每次重新渲染。但是,仅模拟安装和卸载时的行为(即 componentDidMount 和 componentWillUnmount)。 useEffect 需要一个 second argument ,它需要是一个空数组。
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
查看同一问题的更详细解释
从 useEffect
返回的函数在组件卸载之前或之后被调用并不重要:您仍然可以通过闭包访问状态 valuey:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
如果您不管理组件状态中的输入,您的整个结构就会被破坏,应该更改为在正确的位置管理状态。在您的情况下,您应该将组件的共享输入状态提升到父级。
关于挂钩的 ReactJS 文档指定了这一点:
Effects may also optionally specify how to “clean up” after them by returning a function.
因此,您 return 在 useEffect
挂钩中的任何功能都将在组件卸载时执行,以及在重新 运行 由于后续渲染而产生的效果之前执行。
这里的问题是如何在卸载之前运行 使用钩子编写代码? return 函数带有挂钩 运行s AFTER unmount,虽然这对大多数用例没有影响,但在某些情况下是关键差异。
对此做了一些调查,我得出的结论是,目前的 hooks 根本没有提供 componentWillUnmount
的直接替代方案。所以如果你有一个需要它的用例,至少对我来说主要是非 React 库的集成,你只需要用旧的方法来做,使用一个组件。
更新: 请参阅下面关于 UseLayoutEffect()
的答案,看起来可以解决此问题。
经过一些研究,发现 - 您仍然可以完成此操作。有点棘手,但应该可以。
您可以使用 useRef 并将要使用的道具存储在闭包中,例如 render useEffect return 回调方法
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
然而,更好的方法是将第二个参数传递给 useEffect
,以便在所需道具发生任何更改时进行清理和初始化
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
自从引入 useLayoutEffect
钩子后,您现在可以做
useLayoutEffect(() => () => {
// Your code here.
}, [])
模拟componentWillUnmount
。这在卸载期间运行,但在元素实际离开页面之前。
我同意 Frank 的观点,但代码需要看起来像这样,否则它将 运行 仅在第一次渲染时出现:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
这相当于 ComponentWillUnmount
类似于@pritam 的回答,但有一个抽象的代码示例。 useRef 的整个想法是让你跟踪回调的变化,而不是在执行时有一个陈旧的关闭。因此,底部的 useEffect 可以有一个空的依赖数组,以确保它只在组件卸载时运行。 See the code demo.
可重复使用的挂钩:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
我遇到了一个独特的情况,useEffect(() => () => { ... }, []);
答案对我不起作用。这是因为我的组件 从未被渲染过 — 我在注册 useEffect
钩子之前抛出了一个异常。
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
在上面的例子中,通过抛出一个 Promise<T>
告诉反应暂停,直到承诺被解决。然而,一旦 promise 被解决,就会抛出一个错误。由于组件永远不会被渲染并直接进入 ErrorBoundary,因此 useEffect()
挂钩永远不会被注册!
如果您的情况与我相似,这段小代码可能会有所帮助:
为了解决这个问题,我将 ErrorBoundary
代码修改为 运行 恢复后的拆解列表
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
然后,我创建了一个 useTeardown
挂钩,它将添加需要 运行 的拆解,或者尽可能使用 useEffect
。如果你有错误边界嵌套,你很可能需要修改它,但对于我的简单用例,它工作得很好。
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}