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>⟶ </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!
}
}
获取数据时我得到:无法对未安装的组件执行 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>⟶ </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!
}
}