【问题标题】:How can we implement componentWillUnmount using react hooks?我们如何使用 react hooks 实现 componentWillUnmount?
【发布时间】:2019-09-11 03:41:59
【问题描述】:

在卸载和销毁组件之前立即调用方法componentWillUnmount()。如果我们使用useEffect 和一个空数组([])作为第二个参数,并将我们的函数放在return 语句中,它将在组件卸载后执行,甚至在另一个组件安装后执行。据我了解,这样做是出于性能原因。为了不延迟渲染。

所以问题是 - 我们如何在组件被卸载之前使用钩子调用某些函数?

我正在尝试做的是一个应用程序,它可以在用户键入时保存用户的输入(不提交表单)。我使用 setInterval 每 N 秒保存一次更新的文本。而且我需要在组件卸载之前强制保存更新。我不想在导航之前通过反应路由器使用提示。这是一个电子应用程序。感谢您对如何实现此类功能的任何想法或建议。

更新

不幸的是,Effects with Cleanup 在让浏览器绘制后运行。更多细节可以在这里找到:So What About Cleanup?。它基本上意味着在卸载组件后运行清理,它与在componentWillUnmount() 中执行代码不同。如果我将 console.log 语句放在清理代码和另一个组件中,我可以清楚地看到调用顺序。问题是我们是否可以使用钩子卸载组件之前执行一些代码。

更新2

正如我所见,我应该更好地描述我的用例。让我们想象一个理论应用程序,它将数据保存在 Redux 存储中。我们有两个具有某些形式的组件。为简单起见,我们没有任何后端或任何异步逻辑。我们只使用 Redux 存储作为数据存储。

我们不想在每次击键时更新 Redux 存储。因此,我们将实际值保存在本地组件的状态中,当组件挂载时,我们使用存储中的值进行初始化。我们还创建了一个为 1s 设置 setInterval 的效果。

我们有以下流程。一个用户输入一些东西。更新存储在本地组件状态中,直到调用我们的 setInterval 回调。回调只是将数据放入存储中(调度操作)。我们将回调放在 useEffect return 语句中,以在组件卸载时强制保存存储,因为我们希望在这种情况下尽快保存数据以存储。

当用户在第一个组件中键入内容并立即转到第二个组件(比 1 秒快)时,就会出现问题。由于我们的第一个组件中的清理将在重新渲染后调用,因此我们的存储在第二个组件安装之前不会更新。正因为如此,第二个组件将获得其本地状态的过时值。

如果我们将回调放在componentWillUnmount() 中,它将在卸载之前调用,并且存储将在下一个组件安装之前更新。那么我们可以使用钩子来实现吗?

【问题讨论】:

  • 请阅读Effects with Cleanup。这就是答案。
  • @ArupRakshit,我只是重读了一遍,它仍然没有回答这个问题。钩子上的返回函数在卸载之后运行,如何在卸载之前使用钩子运行代码?

标签: javascript reactjs react-hooks


【解决方案1】:

componentWillUnmount 可以通过在useEffect 钩子中返回一个函数来模拟。返回的函数将在每次重新渲染组件之前调用。严格来说,这是一回事,但您应该能够使用它来模拟您想要的任何行为。

useEffect(() => {
  const unsubscribe = api.createSubscription()
  return () => unsubscribe()
})

更新

每次重新渲染时都会运行上述内容。但是,仅在安装和卸载时模拟行为(即 componentDidMount 和 componentWillUnmount)。 useEffect 接受一个second argument,它必须是一个空数组。

useEffect(() => {
  const unsubscribe = api.createSubscription()
  return () => unsubscribe()
}, [])

查看同一问题的更详细解释here

【讨论】:

  • 这似乎和大多数人的想法一样。但实际上,在重新渲染之前不会调用返回的函数。重新渲染后调用。
  • 对我来说,它会在每个组件渲染时触发(而不是仅在卸载时)。我使用 React 16.12.0。
  • useEffect 在这种情况下采用第二个参数,以模拟空数组中的卸载传递,例如useEffect(() => {...}, [ ])
  • 如果我使用空数组,如果我想读取一个状态但它一直抱怨我应该将它添加到依赖数组中,我该怎么办?
  • 虽然这清楚地解决了一些人的问题,但它并没有回答问题。 hook 上的 return 函数在 unmount 之后运行,如何在 unmount 之前使用 hooks 运行代码?
【解决方案2】:

useEffect 返回的函数在组件卸载之前或之后被调用都没有关系:您仍然可以通过闭包访问有价值的状态:

  const [input, setInput] = useState(() => Store.retrieveInput());

  useEffect(() => {
    return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
  }, []);

如果您不管理组件状态下的输入,那么您的整个结构就会被破坏,应该将其更改为在正确的位置管理状态。在您的情况下,您应该将组件的共享输入状态提升到父级。

【讨论】:

  • 谢谢,但问题不在于如何访问值。我用更详细的场景更新了我的问题,以使其更清晰。我倾向于认为钩子不能做到这一点,在这种情况下,我们仍然需要类组件。
  • @georgy 你不能将状态提升到父级吗?
  • 是的,这是一个选项。但我更喜欢使用带有componentWillUnmount() 的类组件。在我看来,提升状态会使代码更难维护。
【解决方案3】:

钩子上的 ReactJS 文档指定了这一点:

Effects 还可以选择指定如何在它们之后“清理”它们 返回一个函数。

因此,您在 useEffect 钩子中返回的任何函数都将在组件卸载时执行,以及由于后续渲染而重新运行效果之前。

【讨论】:

  • 是的,但问题是在组件被卸载之前调用代码,而不是之后
【解决方案4】:

这里的问题是如何在卸载之前使用钩子运行代码?带有钩子的返回函数在卸载后运行,虽然这对大多数用例没有影响,但它们是一些关键的区别。

在对此进行了一些调查后,我得出的结论是,目前的钩子根本无法提供componentWillUnmount 的直接替代方案。所以如果你有一个需要它的用例,至少对我来说主要是非 React 库的集成,你只需要用旧的方式来做,并使用一个组件。

更新:请参阅下面关于UseLayoutEffect() 的答案,看起来它可以解决这个问题。

【讨论】:

  • 不,useLayoutEffect 在这里并没有真正改变任何东西,因为在安装新组件后清理仍然会运行。所以这个新组件的本地状态仍然会被存储中的错误值初始化。我认为您的原始答案仍然正确。
【解决方案5】:

经过一番研究,发现 - 你仍然可以做到这一点。有点棘手,但应该可以。

你可以使用 useRef 并将要使用的 props 存储在一个闭包中,例如 render useEffect 返回回调方法

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]);

【讨论】:

    【解决方案6】:

    自从引入了useLayoutEffect钩子,你现在可以做

    useLayoutEffect(() => () => {
      // Your code here.
    }, [])
    

    模拟componentWillUnmount。这在卸载期间运行,但在元素实际离开页面之前。

    【讨论】:

      【解决方案7】:

      我同意 Frank 的观点,但代码需要看起来像这样,否则它只会在第一次渲染时运行:

      useLayoutEffect(() => {
          return () => {
              // Your code here.
          }
      }, [])
      

      这相当于 ComponentWillUnmount

      【讨论】:

        【解决方案8】:

        类似于@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();
                };
            }, []);
        };
        

        【讨论】:

          【解决方案9】:

          我遇到了一个独特的情况,useEffect(() =&gt; () =&gt; { ... }, []); 的答案对我不起作用。这是因为我的组件从未被渲染——我在注册useEffect钩子之前抛出了一个异常。

          function Component() {
            useEffect(() => () => { console.log("Cleanup!"); }, []);
          
            if (promise) throw promise;
            if (error) throw error;
          
            return <h1>Got value: {value}</h1>;
          }
          

          在上面的例子中,通过抛出一个 Promise&lt;T&gt; 告诉 react 暂停直到 promise 被解决。然而,一旦 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;
                  }
                }
              });
            }
          }
          

          【讨论】:

            猜你喜欢
            • 2020-06-25
            • 1970-01-01
            • 2019-06-30
            • 2019-07-07
            • 1970-01-01
            • 2019-07-28
            • 2020-04-28
            • 2021-03-24
            • 2019-06-14
            相关资源
            最近更新 更多