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

390706 次浏览

您可以尝试这样设置一个状态,并检查您的组件是否安装。通过这种方式,您可以确保如果您的组件被卸载,您不会尝试获取某些内容。

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是正确的方法。
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操作,这个操作会将用户导航到仪表板,因此登录屏幕现在会被卸载。
现在,当反应本机到达第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行。
现在正在更新状态,然后将用户导航离开

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

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

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

如果您正在实现像 useAsync这样的钩子,那么您可以使用 let而不是 const来声明 useStates,并且在 useEffect 返回的析构函数中,将 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 包装在一个跟踪已安装/未安装的函数中,并使用 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

选项 = \{\{ FilterType: “ checkbox” , TextLabels: { 正文: { 正在加载? : “对不起,没有匹配的数据显示” }, }, }}

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

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

为什么我总是收到这样的警告?

此警告的目的是帮助您防止应用程序中的内存泄漏。如果组件在从 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 的未来特性。如果没有额外的代码,组件仍然会存在。

处理这个问题的推荐方法是取消异步函数(例如 中止控制器 API) ,或者忽略它。

事实上,React dev 团队认识到避免误报太困难了,而且 已经删除了反应18版的警告

与我的应用程序类似的问题是,我使用 useEffect来获取一些数据,然后用这些数据更新一个状态:

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


setBlogUser(queryUser);
};


fetchUser();


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

这比卡洛斯 · 瓦列霍的回答更好。

我在 React NativeiOS 中遇到了这个问题,并通过将 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!
}
}
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);

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

对我有用。