React useEffect 导致:无法对未安装的组件执行 React 状态更新

Rya*_*Sam 67 javascript fetch reactjs react-hooks

获取数据时,我得到:无法对未安装的组件执行 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>
  )
}
Run Code Online (Sandbox Code Playgroud)

编辑:

在我的 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()
}
Run Code Online (Sandbox Code Playgroud)

我的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]
    })
  })
}
Run Code Online (Sandbox Code Playgroud)

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

小智 37

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

 const [state, setState] = useState({});

useEffect(() => {
    myFunction();
    return () => {
      setState({}); // This worked for me
    };
}, []);

const myFunction = () => {
    setState({
        name: 'Jhon',
        surname: 'Doe',
    })
}

Run Code Online (Sandbox Code Playgroud)

  • 当您从 useEffect 返回一个函数时,该函数将在组件卸载时执行。因此,利用这一点,您将状态设置为空。这样做,每当您离开该屏幕或卸载组件时,状态将为空,因此屏幕的组件不会再次尝试重新渲染。我希望这有帮助 (17认同)
  • 我不明白背后的逻辑,但它有效。 (10认同)
  • 哦,我想我明白了。useEffect 中的回调函数只有在组件被卸载时才会执行。这就是为什么我们可以在组件卸载之前访问状态的“name”和“surname”属性。 (7认同)
  • 在许多情况下,通过在 useEffect() 中返回一个空函数,你会让 React 认为你正在清理任何副作用,它将修复卸载组件错误。 (2认同)

ᆼᆺᆼ*_*ᆼᆺᆼ 30

AbortControllerfetch()请求之间共享是正确的方法。
所有的的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
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)


mic*_*nil 14

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

此警告的目的是帮助您防止应用程序中的内存泄漏。如果组件在从 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)
}, [])
Run Code Online (Sandbox Code Playgroud)

一个对象位于storeReact 树的更上层(可能在上下文提供者中),或者在全局/模块范围中。另一个例子是订阅事件:

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(handleScroll)
}, [])
Run Code Online (Sandbox Code Playgroud)

另一个值得记住的例子是Web APIsetInterval,如果在卸载时忘记调用,也可能导致内存泄漏clearInterval

但这不是我正在做的事情,我为什么要关心这个警告呢?

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

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

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

但是看到这个警告实在是太烦人了,如何去掉呢?

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

这不是推荐的方法!它不仅会降低代码的可读性并增加运行时开销,而且可能无法与 React 的未来功能很好地配合它对“内存泄漏”也没有任何作用,组件仍然会像没有额外代码一样长时间存在。

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

事实上,React 开发团队认识到避免误报太困难,并在 React v18 中删除了警告


Dmi*_*hin 10

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

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    // ...
    useEffect(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs 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>
    );
}
Run Code Online (Sandbox Code Playgroud)

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

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

修复代码:

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    let componentMounted = true; // (3) component is mounted
    // ...
    useEffect(() => {
        setLoading(true);
        someResponse = await doVeryLongRequest(); // it needs some time
        // When request is finished:
        if (componentMounted){ // (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 = false; // (4) set it to false if we leave the page
        }
    }, []);

    return (
        <div className={loading ? "loading" : ""}>
            {someData}
            <a href="SOME_LOCAL_LINK">Go away from here!</a>
        </div>
    );
}
Run Code Online (Sandbox Code Playgroud)

  • 我对这些信息没有信心,但是以这种方式设置 componentMounted 变量可能会触发以下警告:“每次渲染后,React Hook useEffect 中对 'componentMounted' 变量的赋值将会丢失。为了随着时间的推移保留该值,将其存储在 useRef Hook 中,并将可变值保留在“.current”属性中。...”在这种情况下,可能需要按照此处的建议将其设置为状态:/sf/ask/3930917161/ /直接在这个错误中的useeffect内移动这个变量意味着什么 (2认同)
  • 它是有效的,但您应该使用 useRef 钩子来存储 `componentMounted` 的值(可变值)或将 `componentMounted` 变量的声明移动到 `useEffect` 中 (2认同)

Mer*_*ken 7

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

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>
  )
Run Code Online (Sandbox Code Playgroud)

希望这会帮助你。

  • 错误:“渲染的钩子比上次渲染期间更多。” (2认同)

小智 5

我遇到了类似的问题,滚动到顶部,@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>
  );
};
Run Code Online (Sandbox Code Playgroud)