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

IT技术 javascript reactjs fetch react-hooks
2021-04-02 11:38:24

获取数据时,我得到:无法在未安装的组件上执行 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]
    })
  })
}

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

6个回答

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

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

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

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

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

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
<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>

例如,您有一些组件执行一些异步操作,然后将结果写入 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>
    );
}

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

我们可以通过检查组件是否仍然安装来修复它。让我们创建一个componentMountedref(下面的(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 needs 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/...
2021-05-22 11:38:24
同意,伙计们。固定的
2021-06-09 11:38:24
它是有效的,但您应该使用 useRef 钩子来存储componentMounted(可变值)的值或将componentMounted变量的声明移动useEffect
2021-06-10 11:38:24

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

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

希望这会帮助你。

你能进一步解释一下为什么吗?
2021-05-24 11:38:24
错误: Rendered more hooks than during the previous render.
2021-05-31 11:38:24
这是我在我的应用程序中解决 SSR 问题的一种方法,我认为也适用于这种情况。如果没有Promise应该取消我猜。
2021-06-04 11:38:24
组件安装,然后效果运行并设置didMounttrue,然后组件卸载但didMount永远不会重置
2021-06-10 11:38:24
didMounttrue处于未安装状态。
2021-06-13 11:38:24

我在滚动到顶部时遇到了类似的问题,@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); 正在渲染
2021-06-06 11:38:24