【问题标题】:Checking if a component is unmounted using react hooks?检查是否使用反应钩子卸载了组件?
【发布时间】:2019-11-21 16:17:45
【问题描述】:

我正在检查组件是否已卸载,以避免调用状态更新函数。

  1. 这是第一个选项,它有效
const ref = useRef(false)
  useEffect(() => {
    ref.current = true
    return () => {
      ref.current = false
    }
  }, [])

....
if (ref.current) {
  setAnswers(answers)
  setIsLoading(false)
}
....
  1. 第二个选项是使用 useState,isMounted 总是 false,虽然我在组件确实挂载时将其更改为 true
const [isMounted, setIsMounted] = useState(false)

useEffect(() => {
  setIsMounted(true)
  return () => {
    setIsMounted(false)
  }
}, [])

....
if (isMounted) {
  setAnswers(answers)
  setIsLoading(false)
}
....

为什么与第一个选项相比,第二个选项不起作用?

【问题讨论】:

  • This 是这样做的方法,here 是它的代码。这与您的第一个示例相同。
  • 第二个不起作用,因为当您在组件卸载时设置状态但设置状态只会在下一次渲染时显示更改的值,并且由于您卸载了组件,因此不会重新渲染。跨度>

标签: reactjs react-hooks


【解决方案1】:

我编写了这个自定义钩子,它可以检查组件是否在当前时间被挂载,如果你有一个长时间运行的操作并且组件可能在它完成并更新 UI 状态之前被卸载,这很有用。

import { useCallback, useEffect, useRef } from "react";

export function useIsMounted() {
  const isMountedRef = useRef(true);
  const isMounted = useCallback(() => isMountedRef.current, []);

  useEffect(() => {
    return () => void (isMountedRef.current = false);
  }, []);

  return isMounted;
}

用法

function MyComponent() {
  const [data, setData] = React.useState()
  const isMounted = useIsMounted()

  React.useEffect(() => {
    fetch().then((data) => {
      // at this point the component may already have been removed from the tree
      // so we need to check first before updating the component state
      if (isMounted()) {
        setData(data)
      }
    })
  }, [...])

  return (...)
}

现场演示

【讨论】:

  • 很棒的解决方案。我是唯一一个认为这种事情应该在 React 中更好地处理或在库中可用的人吗?
【解决方案2】:

请仔细阅读此答案,直到最后。

您的组件似乎不止一次渲染,因此isMounted 状态将始终变为 false,因为它不会在每次更新时运行。它只运行一次并卸载。因此,您将在第二个选项数组中传递状态:

}, [isMounted])

现在,它会监视状态并在每次更新时运行效果。但为什么第一个选项有效?

这是因为您使用的是useRef,它是同步的,不同于异步的useState。如果您不清楚,请再次阅读有关 useRef 的文档:

这是可行的,因为 useRef() 创建了一个普通的 JavaScript 对象。 useRef() 和自己创建 {current: ...} 对象之间的唯一区别是 useRef 会在每次渲染时为您提供相同的 ref 对象。


顺便说一句,你不需要清理任何东西。 DOM 变化、第三方 api 反射等都需要清理进程,但不需要习惯清理状态。所以,你可以使用:

useEffect(() => {
    setIsMounted(true)
}, []) // you may watch isMounted state
     // if you're changing it's value from somewhere else

当您使用 useRef 挂钩时,您最好使用清理过程,因为它与 dom 更改有关。

【讨论】:

    【解决方案3】:

    这清除了我的错误消息,在我的 useEffect 中设置返回会取消订阅和异步任务。

      import React from 'react'
    
      const MyComponent = () => {
        
       const [fooState, setFooState] = React.useState(null)
    
       React.useEffect(()=> {
    
        //Mounted
         getFetch()
            
    
        // Unmounted
         return () => {
            setFooState(false)
         }
       })
    
          return (
            <div>Stuff</div>
          )
        }
    
    export {MyComponent as default}
    

    【讨论】:

      【解决方案4】:

      这是@Nearhuscarl's answer 的打字稿版本。

      import { useCallback, useEffect, useRef } from "react";
      /**
       * This hook provides a function that returns whether the component is still mounted.
       * This is useful as a check before calling set state operations which will generates
       * a warning when it is called when the component is unmounted.
       * @returns a function
       */
      export function useMounted(): () => boolean {
        const mountedRef = useRef(false);
        useEffect(function useMountedEffect() {
            mountedRef.current = true;
            return function useMountedEffectCleanup() {
              mountedRef.current = false;
            };
          }, []);
        return useCallback(function isMounted() {
            return mountedRef.current;
          }, [mountedRef]);
      }
      

      这是开玩笑的测试

      import { render, waitFor } from '@testing-library/react';
      import React, { useEffect } from 'react';
      import { delay } from '../delay';
      import { useMounted } from "./useMounted";
      
      describe("useMounted", () => {
      
        it("should work and not rerender", async () => {
          const callback = jest.fn();
      
          function MyComponent() {
            const isMounted = useMounted();
      
            useEffect(() => {
              callback(isMounted())
            }, [])
      
            return (<div data-testid="test">Hello world</div>);
          }
      
          const { unmount } = render(<MyComponent />)
          expect(callback.mock.calls).toEqual([[true]])
          unmount();
          expect(callback.mock.calls).toEqual([[true]])
      
        })
      
        it("should work and not rerender and unmount later", async () => {
          jest.useFakeTimers('modern');
          const callback = jest.fn();
      
          function MyComponent() {
            const isMounted = useMounted();
      
            useEffect(() => {
              (async () => {
                await delay(10000);
                callback(isMounted());
              })();
            }, [])
      
            return (<div data-testid="test">Hello world</div>);
          }
      
          const { unmount } = render(<MyComponent />)
          await waitFor(() => expect(callback).toBeCalledTimes(0));
          jest.advanceTimersByTime(5000);
          unmount();
          jest.advanceTimersByTime(5000);
          await waitFor(() => expect(callback).toBeCalledTimes(1));
          expect(callback.mock.calls).toEqual([[false]])
        })
      
      })
      

      https://github.com/trajano/react-hooks-tests/tree/master/src/useMounted中提供的资源

      【讨论】:

        【解决方案5】:

        如果你想为此使用一个小型库,那么 react-tidy 有一个自定义钩子专门用于执行此操作,称为 useIsMounted

        import React from 'react'
        import {useIsMounted} from 'react-tidy'
        
        function MyComponent() {
          const [data, setData] = React.useState(null)
          const isMounted = useIsMounted()
          React.useEffect(() => {
            fetchData().then((result) => {
              if (isMounted) {
                setData(result)
              }
            })
          }, [])
          // ...
        }
        

        Learn more about this hook

        免责声明我是这个库的作者。

        【讨论】:

          【解决方案6】:

          没有更大的上下文很难知道,但我认为您甚至不需要知道是否已安装某些东西。 useEffect(() =&gt; {...}, []) 在挂载时自动执行,您可以将需要等待挂载的任何内容放入该效果中。

          【讨论】:

          • OP 在....... 中缺少代码,但由于 if 语句中有 setSometing,我认为这是为了避免在可能未安装的组件上设置状态。示例here
          猜你喜欢
          • 2021-01-22
          • 2017-02-07
          • 1970-01-01
          • 2021-03-24
          • 1970-01-01
          • 2018-07-26
          • 2019-01-20
          • 1970-01-01
          相关资源
          最近更新 更多