获取数据时,我得到:无法对未安装的组件执行 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()
承诺,所以我将始终设置状态。
getArtistProfile()
返回在组件卸载后解析。取消该请求,或者如果不可能,则在 .then()
处理程序中添加一个检查,以便在组件已卸载时不会调用 setArtistData()
对我来说,清理组件卸载时的状态很有帮助。
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)); 返回() => ac.abort(); // 在卸载时中止两次获取 }, []);取回; } const main = document.querySelector('main'); ReactDOM.render(React.createElement(Component), main); setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // 1ms 后卸载
例如,您有一些组件执行一些异步操作,然后将结果写入状态并在页面上显示状态内容:
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
引用(下面的 (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>
);
}
componentMounted
的值(可变值)或将 componentMounted
变量的声明移动到 useEffect
someResponse
处的等待吗? useEffect(async () => {...},[])
您可以尝试设置这样的状态并检查您的组件是否已安装。这样你就可以确定如果你的组件被卸载了,你就不会试图获取一些东西。
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>
)
希望这会帮助你。
didMount
将是 true
处于卸载状态。
didMount
设置为 true
,然后组件卸载但 didMount
永远不会重置
Rendered more hooks than during the previous render.
我在滚动到顶部时遇到了类似的问题,@CalosVallejo 的回答解决了它:) 非常感谢!!
const ScrollToTop = () => { const [showScroll, setShowScroll] = useState(); //----------------- 解决方案 useEffect(() => { checkScrollTop(); return () => { setShowScroll({}); // 这对我有用}; }, []); //----------------- 解决方案 const checkScrollTop = () => { setShowScroll(true); }; const scrollTop = () => { window.scrollTo({ top: 0, behavior: "smooth" }); }; window.addEventListener("滚动", checkScrollTop); return ( {" "} 返回顶部 ⟶
为什么我不断收到此警告?
此警告的目的是帮助您防止应用程序中的内存泄漏。如果组件在从 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)
}
}
这不是推荐的方法!它不仅会降低代码的可读性并增加运行时开销,but it might also might not work well with future features of React。 它对“内存泄漏”也没有任何作用,只要没有额外的代码,组件仍然会存在。
处理此问题的推荐方法是取消异步函数(例如使用 AbortController API)或忽略它。
事实上,React 开发团队认识到避免误报太难的事实,并且has removed the warning in v18 of React。
当您在导航到其他组件后对当前组件执行状态更新时会发生此错误:
例如
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
声明您的 useState,并且在 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]);
我在 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
获取一些数据,然后用它更新状态:
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();
}
}, []);
在上面的代码中,我使用了 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();
};
}, []);
不定期副业成功案例分享
name
和surname
属性。