ChatGPT解决这个技术问题 Extra ChatGPT

React useEffect 导致:无法对未安装的组件执行 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

C
Carlos Vallejo

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

 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 返回一个函数来执行清理。它不在乎你执行什么清理
@Pankaj 对我来说,情况并非如此,只有“取消定义”状态才能消除问题
ⵗⵆⵗ

fetch() 请求之间共享 AbortController 是正确的方法。
任何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)); 返回() => ac.abort(); // 在卸载时中止两次获取 }, []);取回; } const main = document.querySelector('main'); ReactDOM.render(React.createElement(Component), main); setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // 1ms 后卸载


D
Dzmitry Kulahin

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

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() 仍在执行时点击了某个链接。 MyComponent 已卸载,但请求仍然存在,当它收到响应时,它会尝试在 (1)(2) 行中设置状态并尝试更改HTML 中的适当节点。我们会从主题中得到一个错误。

我们可以通过检查组件是否仍然安装来修复它。让我们创建一个 componentMounted 引用(下面的 (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>
    );
}

我对此信息没有信心,但以这种方式设置 componentMounted 变量可能会触发以下警告:“每次渲染后,React Hook useEffect 内部对 'componentMounted' 变量的分配将丢失。随着时间的推移保留该值,将其存储在 useRef Hook 中并将可变值保留在 '.current' 属性中。...”在这种情况下,可能需要按照此处的建议将其设置为状态:stackoverflow.com/questions/56155959/…
它是有效的,但您应该使用 useRef 挂钩来存储 componentMounted 的值(可变值)或将 componentMounted 变量的声明移动到 useEffect
同意,伙计们。固定的
useEffect 不需要异步回调来使用 someResponse 处的等待吗? useEffect(async () => {...},[])
谢谢,路易吉,你是对的。固定的
M
Mertcan Diken

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

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.
A
Abdulhakim Zeinu

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

const ScrollToTop = () => { const [showScroll, setShowScroll] = useState(); //----------------- 解决方案 useEffect(() => { checkScrollTop(); return () => { setShowScroll({}); // 这对我有用}; }, []); //----------------- 解决方案 const checkScrollTop = () => { setShowScroll(true); }; const scrollTop = () => { window.scrollTo({ top: 0, behavior: "smooth" }); }; window.addEventListener("滚动", checkScrollTop); return (

{" "} 返回顶部

); };


你有 window.addEventListener("scroll", checkScrollTop);是渲染
m
micnil

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

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

另一个值得记住的例子是 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 in v18 of React


s
skmak

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

例如

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

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


X
Xelphin

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


这是验证中止信号的正确方法
D
Dharman

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

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

如果你有不止一个 fetch 函数,那么

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

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

R
Ron Newcomb

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

如果您要实现像 useAsync 这样的钩子,您可以使用 let 而不是 const 声明您的 useState,并且在 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));
}

M
Marcos Santos Dev

尝试在useEffect中添加依赖:

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

T
Thomas

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

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

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

setTimeout(onClose, 0)

K
Kai - Kazuya Ito

这对我有用:')

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

G
Green

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

错误代码(导致错误):

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

Z
Zach Jensz

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

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

    setBlogUser(queryUser);
  };

  fetchUser();

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

这改进了 Carlos Vallejo 的回答。


K
Kaleem Ullah Khan
useEffect(() => {  
let abortController = new AbortController();  
// your async action is here  
return () => {  
abortController.abort();  
}  
}, []);

在上面的代码中,我使用了 AbortController 来取消订阅效果。当同步操作完成时,我会中止控制器并取消订阅效果。

它对我有用....


E
Emin Adiloğlu

简单的方法

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

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

h
hamza rebai

options={{ filterType: "checkbox" , textLabels: { body: { noMatch: isLoading ? : '对不起,没有匹配的数据显示', }, }, }}


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

如果没有,您应该更多地研究 AbortControllers 是如何工作的。此代码甚至不使用中止控制器,因此在您的示例中不需要它。
为了将来参考,最好尝试add a description to your code,因为解释它的作用将有助于 OP 和其他人学习。