我们如何使用 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>;
}

DEMO

然而,更好的方法是将第二个参数传递给 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;
        }
      }
    });
  }
}