簡體   English   中英

React useEffect 導致:無法對未安裝的組件執行 React 狀態更新

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

獲取數據時,我得到:無法對未安裝的組件執行 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()該承諾,所以我將始終設置狀態。

對我來說,清理組件卸載時的狀態很有幫助。

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

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

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

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

例如,您有一些組件執行一些異步操作,然后將結果寫入狀態並在頁面上顯示狀態內容:

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

您可以嘗試設置這樣的狀態並檢查您的組件是否已安裝。 這樣你就可以確定如果你的組件被卸載了,你就不會試圖獲取一些東西。

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

希望這會幫助你。

我在滾動到頂部時遇到了類似的問題,@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> ); };

為什么我不斷收到此警告?

此警告的目的是幫助您防止應用程序中的內存泄漏。 如果組件在從 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)
  }
}

這不是推薦的方法! 它不僅降低了代碼的可讀性並增加了運行時開銷,而且還可能無法很好地與 React 的未來功能配合使用 它對“內存泄漏”也沒有任何作用,只要沒有額外的代碼,組件仍然會存在。

處理此問題的推薦方法是取消異步函數(例如使用AbortController API ),或者忽略它。

事實上,React 開發團隊認識到避免誤報太難的事實,並且已經移除了 React v18 中的警告

當您在導航到其他組件后對當前組件執行狀態更新時會發生此錯誤:

例如

  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 行。
現在狀態在導航用戶離開之前被更新。 贏贏

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

我得到了同樣的警告,這個解決方案對我有用->

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

如果你有不止一個 fetch 函數,那么

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

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

如果用戶導航離開,或者其他原因導致組件在異步調用返回並嘗試對其進行 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));
}

嘗試在useEffect中添加依賴:

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

當您有條件地顯示組件時,通常會出現此問題,例如:

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

我的應用程序存在類似問題,我使用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 調用移動到一個捕獲中來修復它。 見下文:

錯誤代碼(導致錯誤):

  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!
    }
  }
useEffect(() => {  
let abortController = new AbortController();  
// your async action is here  
return () => {  
abortController.abort();  
}  
}, []);

在上面的代碼中,我使用了 AbortController 來取消訂閱效果。 當同步操作完成時,我會中止控制器並取消訂閱效果。

它對我有用....

簡單的方法

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

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

options={{ filterType: "checkbox" , textLabels: { body: { noMatch: isLoading ? : '對不起,沒有匹配的數據顯示', }, }, }}

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM