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



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

这是一个无操作,但表示您的应用程序中的内存泄漏。要修复,请在使用效率清理功能中取消所有订阅和异步任务。

为什么我要继续警告?

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

https://developer.mozilla.org/en-us/docs/web/api/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 S的任何中止时,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中。我们将从主题中获得错误。

我们可以通过检查Compponen是否仍在安装中进行修复。让我们创建一个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>
    );
}

为什么我要继续警告?

此警告的目的是帮助您防止应用程序中的内存泄漏。如果该组件在从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是一个对象,它在反应树(可能在上下文提供商中)或全局/模块范围内延伸。另一个示例是订阅事件:

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

这不是推荐的方法!不仅可以使代码降低可读取并添加运行时开销,而且可能与React的未来功能可能无法很好地工作。它也对"内存泄漏" 完全无能为力,该组件仍然可以生存,只要没有额外的代码。

建议处理此问题的推荐方法是取消异步函数(例如,使用apcontroller api),或忽略它。

实际上,React Dev团队认识到避免误报太难的事实,并且已删除了React的V18警告。

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

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

希望这对您有帮助。

我在滚动到top to top to top and @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>
  );
};

我有相同的警告,此解决方案对我有用 -

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

如果您有更多,则一个提取功能,则

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

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

例如

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

只需移动上面的反应状态更新,将第6行移动到第5行。
现在,在将用户导航之前正在更新状态。Win win

有很多答案,但我认为我可以更简单地证明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-js-using-sus-using-hook-a5ecbf9becf8

如果用户逐渐消失,或者其他内容会导致组件在异步调用返回并尝试在其上进行设置之前被销毁,则会导致错误。如果确实是一个晚期异步呼叫,那通常是无害的。

有几种沉默的方法。

如果您正在实现useAsync这样的挂钩,则可以用let而不是const声明您的USESESTATE,并且在使用useffect返回的破坏者中,将SetState函数设置为NO-OP函数。

> > >

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包裹在跟踪已安装/未填充的函数中,并将返回的SetState函数与If-Check一起包装。

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

尝试在useffect中添加依赖项:

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

我在React Antive 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获取一些数据,然后以此更新状态:

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

这改善了Carlos Vallejo的答案。

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

在上面的代码中,我使用AportController取消订阅效果。当A同步操作完成后,我中止控制器并取消订阅效果。

它对我有用....

简单的方式

    let fetchingFunction= async()=>{
      // fetching
    }
React.useEffect(() => {
    fetchingFunction();
    return () => {
        fetchingFunction= null
    }
}, [])
options={{
              filterType: "checkbox"
              ,
              textLabels: {
                body: {
                    noMatch:  isLoading ?
                    <CircularProgress />:
                        'Sorry, there is no matching data to display',
                },
            },
            }}
const onOk = useCallback(
    (moduleId?: number) => {
      /*
      Warning: Can't perform a React state update on an unmounted component.
      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.
      at AddModal;
      */
      setTimeout(() => {
        setShowAddModal(false); 
      }, 10);
      getModuleList();
      const msg = moduleId;
      message.success(msg);
    },
    [getModuleList],
  );

相关内容

  • 没有找到相关文章

最新更新