【问题标题】:React useEffect causing: Can't perform a React state update on an unmounted componentReact useEffect 导致:无法对未安装的组件执行 React 状态更新
【发布时间】:2019-07-24 01:29:31
【问题描述】:

获取数据时,我得到:无法对未安装的组件执行 React 状态更新。该应用程序仍然可以运行,但 react 表明我可能会导致内存泄漏。

这是一个空操作,但它表明您的应用程序中存在内存泄漏。要解决此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。”

为什么我不断收到此警告?

我尝试研究这些解决方案:

https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal

https://developer.mozilla.org/en-US/docs/Web/API/AbortController

但这仍然给了我警告。

const  ArtistProfile = props => {
  const [artistData, setArtistData] = useState(null)
  const token = props.spotifyAPI.user_token

  const fetchData = () => {
    const id = window.location.pathname.split("/").pop()
    console.log(id)
    props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
    .then(data => {setArtistData(data)})
  }
  useEffect(() => {
    fetchData()
    return () => { props.spotifyAPI.cancelRequest() }
  }, [])
  
  return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )
}

编辑:

在我的 api 文件中,我添加了 AbortController() 并使用了 signal,因此我可以取消请求。

export function spotifyAPI() {
  const controller = new AbortController()
  const signal = controller.signal

// code ...

  this.getArtist = (id) => {
    return (
      fetch(
        `https://api.spotify.com/v1/artists/${id}`, {
        headers: {"Authorization": "Bearer " + this.user_token}
      }, {signal})
      .then(response => {
        return checkServerStat(response.status, response.json())
      })
    )
  }

  // code ...

  // this is my cancel method
  this.cancelRequest = () => controller.abort()
}

我的spotify.getArtistProfile() 看起来像这样

this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
  return Promise.all([
    this.getArtist(id),
    this.getArtistAlbums(id,includeGroups,market,limit,offset),
    this.getArtistTopTracks(id,market)
  ])
  .then(response => {
    return ({
      artist: response[0],
      artistAlbums: response[1],
      artistTopTracks: response[2]
    })
  })
}

但是因为我的信号用于在 Promise.all 中解决的单个 api 调用,所以我不能 abort() 承诺所以我将始终设置状态。

【问题讨论】:

  • 警告是因为 Promise getArtistProfile() 返回在组件卸载后解析。取消该请求,或者如果不可能,则在 .then() 处理程序中添加一个检查,以便在组件已卸载时不会调用 setArtistData()
  • 如果不了解此组件之外的应用程序的更多信息,就无法解释为什么会发生这种情况。我们需要知道是什么导致这个组件挂载/卸载。当您收到错误时,应用程序中发生了什么?
  • @ııı 如何检查组件是否已卸载?
  • 这不是真正的内存泄漏,但很可能是一个错误的警告——这就是 React 团队将在下一个版本中删除警告的原因。见PR

标签: javascript reactjs fetch react-hooks


【解决方案1】:

您可以尝试设置这样的状态并检查您的组件是否已安装。这样您就可以确定,如果您的组件已卸载,您不会尝试获取任何东西。

const [didMount, setDidMount] = useState(false); 

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

if(!didMount) {
  return null;
}

return (
    <ArtistProfileContainer>
      <AlbumContainer>
        {artistData ? artistData.artistAlbums.items.map(album => {
          return (
            <AlbumTag
              image={album.images[0].url}
              name={album.name}
              artists={album.artists}
              key={album.id}
            />
          )
        })
        : null}
      </AlbumContainer>
    </ArtistProfileContainer>
  )

希望这会对你有所帮助。

【讨论】:

  • didMount 在卸载状态下将是true
  • 你能解释一下为什么吗?
  • 组件挂载,然后效果运行并将didMount设置为true,然后组件卸载但didMount永远不会重置
  • 这是我在我的应用程序中解决 SSR 问题的一种方法,我认为这种方法也适用于这种情况。如果不是,我猜应该取消承诺。
  • 错误:Rendered more hooks than during the previous render.
【解决方案2】:

fetch() 请求之间共享AbortController 是正确的方法。
任何Promises 被中止时,Promise.all() 将用 AbortError 拒绝:

function Component(props) {
  const [fetched, setFetched] = React.useState(false);
  React.useEffect(() => {
    const ac = new AbortController();
    Promise.all([
      fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
      fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
    ]).then(() => setFetched(true))
      .catch(ex => console.error(ex));
    return () => ac.abort(); // Abort both fetches on unmount
  }, []);
  return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>

【讨论】:

    【解决方案3】:

    对我来说,清除组件卸载中的状态有帮助。

     const [state, setState] = useState({});
    
    useEffect(() => {
        myFunction();
        return () => {
          setState({}); // This worked for me
        };
    }, []);
    
    const myFunction = () => {
        setState({
            name: 'Jhon',
            surname: 'Doe',
        })
    }
    
    

    【讨论】:

    • 我不明白背后的逻辑,但它有效。
    • 请解释一下。
    • 哦,我想我明白了。 useEffect 中的回调函数只会在组件被卸载时执行。这就是为什么我们可以在组件被卸载之前访问 namesurname 的状态属性。
    • 当您从 useEffect 返回一个函数时,该函数将在组件卸载时执行。因此,利用这一点,您将状态设置为空。这样做,每当您离开该屏幕或组件卸载时,状态将为空,因此您的屏幕组件不会再次尝试重新渲染。我希望这会有所帮助
    • 即使你从 useEffect 返回一个空函数,这也会起作用。 React 只是确保你从 useEffect 返回一个函数来执行清理。它不在乎你执行什么清理
    【解决方案4】:

    我在滚动到顶部时遇到了类似的问题,@CalosVallejo 的回答解决了它:) 非常感谢!!

    const ScrollToTop = () => { 
    
      const [showScroll, setShowScroll] = useState();
    
    //------------------ solution
      useEffect(() => {
        checkScrollTop();
        return () => {
          setShowScroll({}); // This worked for me
        };
      }, []);
    //-----------------  solution
    
      const checkScrollTop = () => {
        setShowScroll(true);
     
      };
    
      const scrollTop = () => {
        window.scrollTo({ top: 0, behavior: "smooth" });
     
      };
    
      window.addEventListener("scroll", checkScrollTop);
    
      return (
        <React.Fragment>
          <div className="back-to-top">
            <h1
              className="scrollTop"
              onClick={scrollTop}
              style={{ display: showScroll }}
            >
              {" "}
              Back to top <span>&#10230; </span>
            </h1>
          </div>
        </React.Fragment>
      );
    };

    【讨论】:

    • 你有 window.addEventListener("scroll", checkScrollTop);正在渲染
    【解决方案5】:

    当您在导航到其他组件后对当前组件执行状态更新时会发生此错误:

    例如

      axios
          .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
          .then((res) => {
            if (res.status === 200) {
              dispatch(login(res.data.data)); // line#5 logging user in
              setSigningIn(false); // line#6 updating some state
            } else {
              setSigningIn(false);
              ToastAndroid.show(
                "Email or Password is not correct!",
                ToastAndroid.LONG
              );
            }
          })
    

    在第 5 行的上述情况下,我正在调度 login 操作,该操作作为回报将用户导航到仪表板,因此登录屏幕现在被卸载。
    现在,当 React Native 到达第 6 行并看到状态正在更新时,它会大声喊我该怎么做,login component 不存在了。

    解决方案:

      axios
          .post(API.BASE_URI + API.LOGIN, { email: username, password: password })
          .then((res) => {
            if (res.status === 200) {
              setSigningIn(false); // line#6 updating some state -- moved this line up
              dispatch(login(res.data.data)); // line#5 logging user in
            } else {
              setSigningIn(false);
              ToastAndroid.show(
                "Email or Password is not correct!",
                ToastAndroid.LONG
              );
            }
          })
    

    只需将反应状态更新移到上面,将第 6 行移到第 5 行。
    现在在导航用户离开之前状态正在更新。赢赢

    【讨论】:

      【解决方案6】:

      例如,您有一些组件执行一些异步操作,然后将结果写入状态并在页面上显示状态内容:

      export default function MyComponent() {
          const [loading, setLoading] = useState(false);
          const [someData, setSomeData] = useState({});
          // ...
          useEffect(() => {
              setLoading(true);
              someResponse = await doVeryLongRequest(); // it takes some time
              // When request is finished:
              setSomeData(someResponse.data); // (1) write data to state
              setLoading(false); // (2) write some value to state
          }, []);
      
          return (
              <div className={loading ? "loading" : ""}>
                  {someData}
                  <a href="SOME_LOCAL_LINK">Go away from here!</a>
              </div>
          );
      }
      

      假设用户在doVeryLongRequest() 仍然执行时点击了某个链接。 MyComponent 已卸载,但请求仍然存在,当它得到响应时,它会尝试在 (1)(2) 行中设置状态并尝试更改HTML 中的适当节点。我们会收到来自主题的错误。

      我们可以通过检查组件是否仍然挂载来修复它。让我们创建一个componentMounted ref(下面的(3) 行)并将其设置为true。卸载组件后,我们将其设置为 false(下面的 (4) 行)。让我们在每次尝试设置状态时检查componentMounted 变量(下面的(5) 行)。

      修复代码:

      export default function MyComponent() {
          const [loading, setLoading] = useState(false);
          const [someData, setSomeData] = useState({});
          const componentMounted = useRef(true); // (3) component is mounted
          // ...
          useEffect(() => {
              setLoading(true);
              someResponse = await doVeryLongRequest(); // it takes some time
              // When request is finished:
              if (componentMounted.current){ // (5) is component still mounted?
                  setSomeData(someResponse.data); // (1) write data to state
                  setLoading(false); // (2) write some value to state
              }
              return () => { // This code runs when component is unmounted
                  componentMounted.current = false; // (4) set it to false when we leave the page
              }
          }, []);
      
          return (
              <div className={loading ? "loading" : ""}>
                  {someData}
                  <a href="SOME_LOCAL_LINK">Go away from here!</a>
              </div>
          );
      }
      

      【讨论】:

      • 我对这些信息没有信心,但是以这种方式设置 componentMounted 变量可能会触发以下警告:“每次渲染后,从 React Hook useEffect 内部对 'componentMounted' 变量的分配将丢失。要随着时间的推移保留该值,请将其存储在 useRef Hook 中并将可变值保留在 '.current' 属性中。..." 在这种情况下,可能需要按照此处的建议将其设置为状态:stackoverflow.com/questions/56155959/…
      • 它是有效的,但是你应该使用 useRef 挂钩来存储 componentMounted 的值(可变值)或将 componentMounted 变量的声明移动到 useEffect
      • 同意,伙计们。固定
      【解决方案7】:

      如果用户离开,或者其他原因导致组件在异步调用返回并尝试对其进行 setState 之前被破坏,则会导致错误。如果它确实是一个后期完成的异步调用,它通常是无害的。有几种方法可以消除错误。

      如果你正在实现像useAsync 这样的钩子,你可以用let 而不是const 来声明你的useStates,并且在useEffect 返回的析构函数中,将setState 函数设置为无操作功能。

      
      export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
        let [parameters, setParameters] = useState(rest);
        if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
          setParameters(rest);
      
        const refresh: () => void = useCallback(() => {
          const promise: Promise<T | void> = gettor
            .apply(null, parameters)
            .then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
            .catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
          setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
          return promise;
        }, [gettor, parameters]);
      
        useEffect(() => {
          refresh();
          // and for when async finishes after user navs away //////////
          return () => { setTuple = setParameters = (() => undefined) } 
        }, [refresh]);
      
        let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
        return tuple;
      }
      

      不过,这在组件中效果不佳。在那里,您可以将 useState 封装在一个跟踪挂载/卸载的函数中,并使用 if-check 封装返回的 setState 函数。

      export const MyComponent = () => {
        const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
        // ..etc.
      
      // imported from elsewhere ////
      
      export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
        const [val, setVal] = useStateTuple;
        const [isMounted, setIsMounted] = useState(true);
        useEffect(() => () => setIsMounted(false), []);
        return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
      }
      

      然后您可以创建一个useStateAsync 挂钩来简化一点。

      export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
        return useUnlessUnmounted(useState(initialState));
      }
      

      【讨论】:

        【解决方案8】:

        尝试在useEffect中添加依赖:

          useEffect(() => {
            fetchData()
            return () => { props.spotifyAPI.cancelRequest() }
          }, [fetchData, props.spotifyAPI])
        

        【讨论】:

          【解决方案9】:

          我收到了同样的警告,这个解决方案对我有用 ->

          useEffect(() => {
              const unsubscribe = fetchData(); //subscribe
              return unsubscribe; //unsubscribe
          }, []);
          

          如果你有多个 fetch 函数,那么

          const getData = () => {
              fetch1();
              fetch2();
              fetch3();
          }
          
          useEffect(() => {
              const unsubscribe = getData(); //subscribe
              return unsubscribe; //unsubscribe
          }, []);
          

          【讨论】:

            【解决方案10】:

            简单的方法

                let fetchingFunction= async()=>{
                  // fetching
                }
            
            React.useEffect(() => {
                fetchingFunction();
                return () => {
                    fetchingFunction= null
                }
            }, [])
            

            【讨论】:

              【解决方案11】:

              有很多答案,但我想我可以更简单地演示 abort 的工作原理(至少它如何为我解决了这个问题):

              useEffect(() => {
                // get abortion variables
                let abortController = new AbortController();
                let aborted = abortController.signal.aborted; // true || false
                async function fetchResults() {
                  let response = await fetch(`[WEBSITE LINK]`);
                  let data = await response.json();
                  aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
                  if (aborted === false) {
                    // All your 'set states' inside this kind of 'if' statement
                    setState(data);
                  }
                }
                fetchResults();
                return () => {
                  abortController.abort();
                };
              }, [])
              

              其他方法: https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8

              【讨论】:

              • 这是一个正确的验证中止信号
              【解决方案12】:

              选项={{ 过滤器类型:“复选框” , 文本标签:{ 身体: { noMatch: isLoading ? : '对不起,没有匹配的数据显示', }, }, }}

              【讨论】:

                【解决方案13】:

                通常在有条件地显示组件时会出现此问题,例如:

                showModal && <Modal onClose={toggleModal}/> 
                

                你可以尝试在Modal onClose 函数中做一些小技巧,比如

                setTimeout(onClose, 0)
                

                【讨论】:

                  【解决方案14】:

                  这对我有用:')

                     const [state, setState] = useState({});
                      useEffect( async ()=>{
                            let data= await props.data; // data from API too
                            setState(users);
                          },[props.data]);
                  

                  【讨论】:

                    【解决方案15】:

                    为什么我不断收到此警告?

                    此警告的目的是帮助您防止应用程序中的内存泄漏。如果组件在从 DOM 中卸载后更新了它的状态,这表明可能存在内存泄漏,但这表明有很多误报。

                    我如何知道我是否有内存泄漏?

                    如果一个对象的寿命比你的组件长,直接或间接地持有对它的引用,你就会发生内存泄漏。这通常发生在您订阅事件或某种类型的更改,而当您的组件从 DOM 卸载时没有取消订阅。

                    它通常看起来像这样:

                    useEffect(() => {
                      function handleChange() {
                         setState(store.getState())
                      }
                      // "store" lives longer than the component, 
                      // and will hold a reference to the handleChange function.
                      // Preventing the component to be garbage collected after 
                      // unmount.
                      store.subscribe(handleChange)
                    
                      // Uncomment the line below to avoid memory leak in your component
                      // return () => store.unsubscribe(handleChange)
                    }, [])
                    

                    store 是一个在 React 树(可能在上下文提供程序中)或在全局/模块范围内的对象。另一个例子是订阅事件:

                    useEffect(() => {
                      function handleScroll() {
                         setState(window.scrollY)
                      }
                      // document is an object in global scope, and will hold a reference
                      // to the handleScroll function, preventing garbage collection
                      document.addEventListener('scroll', handleScroll)
                      // Uncomment the line below to avoid memory leak in your component
                      // return () => document.removeEventListener(handleChange)
                    }, [])
                    

                    另一个值得记住的例子是web API setInterval,如果您在卸载时忘记调用clearInterval,也会导致内存泄漏。

                    但这不是我要做的,我为什么要关心这个警告?

                    React 的策略会在组件卸载后发生状态更新时发出警告,这会产生很多误报。我见过的最常见的是在异步网络请求之后设置状态:

                    async function handleSubmit() {
                      setPending(true)
                      await post('/someapi') // component might unmount while we're waiting
                      setPending(false)
                    }
                    

                    从技术上讲,这也是内存泄漏,因为组件在不再需要后不会立即释放。如果您的“帖子”需要很长时间才能完成,那么释放内存也需要很长时间。但是,这不是您应该担心的事情,因为它最终会被垃圾收集。 在这些情况下,您可以直接忽略警告

                    但是看到这个警告实在是太烦了,怎么去掉呢?

                    stackoverflow 上有很多博客和答案建议跟踪组件的挂载状态并将状态更新包装在 if 语句中:

                    let isMountedRef = useRef(false)
                    useEffect(() => {
                      isMountedRef.current = true
                      return () => {
                        isMountedRef.current = false
                      }
                    }, [])
                    
                    async function handleSubmit() {
                      setPending(true)
                      await post('/someapi')
                      if (!isMountedRef.current) {
                        setPending(false)
                      }
                    }
                    

                    这不是推荐的方法!它不仅降低了代码的可读性并增加了运行时开销,but it might also might not work well with future features of React它对“内存泄漏”也没有任何作用,只要没有额外的代码,组件仍然会存在。

                    推荐的处理方法是取消异步函数(例如AbortController API),或者忽略它。

                    事实上,React 开发团队认识到避免误报太困难的事实,并且has removed the warning for the next release of React。我测试了 React 18 的 beta 版本,但它不再存在。

                    【讨论】:

                      猜你喜欢
                      • 2022-01-18
                      • 1970-01-01
                      • 2021-12-03
                      • 2021-09-28
                      • 2021-07-01
                      • 2020-11-21
                      • 1970-01-01
                      • 2020-03-20
                      • 2021-11-30
                      相关资源
                      最近更新 更多