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

React useEffect causing: Can't perform a React state update on an unmounted component

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

This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."

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

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

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() 承诺,所以我将始终设置状态。

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

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

希望对您有所帮助。

fetch() 个请求之间共享 AbortController 是正确的方法。
any 中的 Promise 被中止时,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>

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

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

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

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

我在滚动到顶部时遇到了类似的问题,@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>
  );
};

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

例如

  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 操作,该操作在 return 中将用户导航到仪表板,因此登录屏幕现在被卸载。
现在,当 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
          );
        }
      })

只需将 React 状态更新移至上方,将第 6 行移至第 5 行。
现在在将用户导航离开之前正在更新状态。赢了赢了

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

export default function MyComponent() {
    const [loading, setLoading] = useState(false);
    const [someData, setSomeData] = useState({});
    // ...
    useEffect( async () => {
        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}
            <Link to="SOME_LOCAL_LINK">Go away from here!</Link>
        </div>
    );
}

假设用户在 doVeryLongRequest() 仍然执行时单击了一些 link。 MyComponent 已卸载,但请求仍然存在,当它收到响应时,它会尝试在 (1)(2)[= 行中设置状态31=] 并尝试更改 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( async () => {
        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}
            <Link to="SOME_LOCAL_LINK">Go away from here!</Link>
        </div>
    );
}

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

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


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 包装在一个跟踪 mounted/unmounted 的函数中,并使用 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));
}

尝试在useEffect中添加依赖:

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

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

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

如果你有不止一个获取函数那么

const getData = () => {
    fetch1();
    fetch2();
    fetch3();
}

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

简单的方法

    let fetchingFunction= async()=>{
      // fetching
    }

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

有很多答案,但我想我可以更简单地演示 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

选项={{ 过滤器类型:“复选框” , 文本标签:{ body:{ noMatch:正在加载? : 'Sorry, there is no matching data to display', }, }, }}

这个问题通常发生在你有条件地显示组件时,例如:

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

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

setTimeout(onClose, 0)

这对我有用:')

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

Why do I keep getting this warning?

此警告的目的是帮助您防止应用程序中的内存泄漏。如果组件在从 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 树更上层的对象(可能在上下文提供者中),或者在 global/module 范围内。另一个例子是订阅事件:

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

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

但是看到这个警告好烦,怎么去掉?

Whosebug 上有很多博客和答案建议跟踪组件的安装状态并将状态更新包装在 if-statement:

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 版本,但它不再存在。

useEffect(() => {
    const abortController = new AbortController();
MyFunction()
    return () => {
      abortController.abort();
    };
  }, []);

我的应用程序存在类似问题,我使用 useEffect 获取一些数据,然后用它更新状态:

useEffect(() => {
  const fetchUser = async() => {
    const {
      data: {
        queryUser
      },
    } = await authFetch.get(`/auth/getUser?userId=${createdBy}`);

    setBlogUser(queryUser);
  };

  fetchUser();

  return () => {
    setBlogUser(null);
  };
}, [_id]);

这改进了 Carlos Vallejo 的回答。

我在 React Native 中遇到了这个问题 iOS 并通过将我的 setState 调用移到一个 catch 中来修复它。见下文:

错误代码(导致错误):

  const signupHandler = async (email, password) => {
    setLoading(true)
    try {
      const token = await createUser(email, password)
      authContext.authenticate(token) 
    } catch (error) {
      Alert.alert('Error', 'Could not create user.')
    }
    setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
  }

好的代码(没有错误):

  const signupHandler = async (email, password) => {
    setLoading(true)
    try {
      const token = await createUser(email, password)
      authContext.authenticate(token) 
    } catch (error) {
      Alert.alert('Error', 'Could not create user.')
      setLoading(false) // moving this line INTO the catch call resolved the error!
    }
  }