获取数据时,我得到:无法在已卸载的组件上执行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()
保证,所以我将始终设置状态。
答案 0 :(得分:7)
例如,您有一些组件执行一些异步操作,然后将结果写入状态并在页面上显示状态内容:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect(() => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it needs 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}
<a href="SOME_LOCAL_LINK">Go away from here!</a>
</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({});
let componentMounted = true; // (3) component is mounted
// ...
useEffect(() => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it needs some time
// When request is finished:
if (componentMounted){ // (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 = false; // (4) set it to false if we leave the page
}
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<a href="SOME_LOCAL_LINK">Go away from here!</a>
</div>
);
}
答案 1 :(得分:2)
在AbortController
个请求之间共享fetch()
是正确的方法。
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>
答案 2 :(得分:1)
对我来说,清理组件卸载状态是有帮助的。
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
答案 3 :(得分:1)
我在滚动到顶部时遇到了类似的问题,@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>
);
};
答案 4 :(得分:0)
您可以尝试将此状态设置为类似状态,并检查是否已安装组件。这样,您可以确定如果卸载了组件,则不会尝试获取某些东西。
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>
)
希望这会对您有所帮助。
答案 5 :(得分:0)
当您导航到其他组件后对当前组件执行状态更新时,会发生此错误:
例如
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 行。
现在,在将用户导航离开之前,状态正在更新。赢赢
答案 6 :(得分:0)
如果用户导航离开,或其他原因导致组件在异步调用返回并尝试对其进行 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));
}
答案 7 :(得分:0)
尝试在useEffect中添加依赖:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
答案 8 :(得分:0)
我收到了同样的警告,这个解决方案对我有用 ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
如果你有多个获取函数,那么
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
答案 9 :(得分:0)
简单的方法
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
答案 10 :(得分:0)
有很多答案,但我想我可以更简单地演示 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
答案 11 :(得分:0)
选项={{ 过滤器类型:“复选框” , 文本标签:{ 身体: { noMatch: isLoading ? : '抱歉,没有要显示的匹配数据', }, }, }}