cancel multiple promises inside a promise on unmount? - javascript

hi i want to cancel promise on unmount since i received warning,
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 the componentWillUnmount method.
My code:
const makeCancelable = (promise: Promise<void>) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
(val) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
(error) => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
useEffect(() => {
const initialize = async () => {
const getImageFilesystemKey = (remoteUri: string) => {
const [_, fileName] = remoteUri.split('toolbox-talks/');
return `${cacheDirectory}${fileName}`;
};
const filesystemUri = getImageFilesystemKey(uri);
try {
// Use the cached image if it exists
const metadata = await getInfoAsync(filesystemUri);
if (metadata.exists) {
console.log('resolve 1');
setFileUri(filesystemUri);
} else {
const imageObject = await downloadAsync(uri, filesystemUri);
console.log('resolve 2');
setFileUri(imageObject.uri);
}
// otherwise download to cache
} catch (err) {
console.log('error 3');
setFileUri(uri);
}
};
const cancelable = makeCancelable(initialize());
cancelable.promise
.then(() => {
console.log('reslved');
})
.catch((e) => {
console.log('e ', e);
});
return () => {
cancelable.cancel();
};
}, []);
but i still get warning on fast press, help me please?

You're cancelling the promise, but you are not cancelling the axios call or any of the logic that happens after it inside initialize(). So while it is true that the console won't print resolved, setFileUri will be called regardless, which causes your problem.
A solution could look like this (untested):
const makeCancelable = (promise: Promise<void>) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val)),
error => (hasCanceled_ ? reject({ isCanceled: true }) : reject(error))
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
}
};
};
const initialize = async () => {
const getImageFilesystemKey = (remoteUri: string) => {
const [_, fileName] = remoteUri.split("toolbox-talks/");
return `${cacheDirectory}${fileName}`;
};
const filesystemUri = getImageFilesystemKey(uri);
try {
// Use the cached image if it exists
const metadata = await getInfoAsync(filesystemUri);
if (metadata.exists) {
console.log("resolve 1");
return filesystemUri;
} else {
const imageObject = await downloadAsync(uri, filesystemUri);
console.log("resolve 2");
return imageObject.uri;
}
// otherwise download to cache
} catch (err) {
console.error("error 3", err);
return uri;
}
};
useEffect(() => {
const cancelable = makeCancelable(initialize());
cancelable.promise.then(
fileURI => {
console.log("resolved");
setFileUri(fileURI);
},
() => {
// Your logic is such that it's only possible to get here if the promise is cancelled
console.log("cancelled");
}
);
return () => {
cancelable.cancel();
};
}, []);
This ensures that you will only call setFileUri if the promise is not cancelled (I did not check the logic of makeCancelable).

Related

workbox Cache processing does not work with PrecacheController in v6

I was using workbox5.14 (#nuxtjs/pwa) to precache the 'message' event by passing the URL to PrecacheController when received. (from workbox-window)
At this time, PrecacheController.addToCacheList() would not execute the cache, and for some reason PrecacheController.install() would do so.
However, with v6, PrecacheController.install() now requires an 'install' event or an 'activate' event, and I cannot execute PrecacheController.install(event) with a 'message' event. This results in an error.
"sw.js:238 Uncaught (in promise) DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished and no extend lifetime promises are outstanding."
How can we execute PrecacheController's caching process on the 'message' event?
Library Affected:
workbox-precaching
Browser & Platform:
Google Chrome 106.0.5249.103(Official Build)
Issue or Feature Request Description:
v5 ok
/* global importScripts, workbox, consola, processFuncPromise */
const cacheName = workbox.core.cacheNames.precache
const precacheController = new workbox.precaching.PrecacheController(cacheName)
addEventListener('message', async (event) => {
if (event.data.type === 'ADD_PRECACHE') {
const cacheTargetFiles = event.data.payload
const addCaches = async (cacheTargetFiles) => {
for (const file of cacheTargetFiles) {
const cacheKey = precacheController.getCacheKeyForURL(file.filePath)
const cached = await caches
.match(cacheKey)
.then((response) => response !== undefined)
if (!cached) {
precacheController.addToCacheList([{ url: file.filePath, revision: file.revision }])
}
}
}
const checkCaches = async () => {
const cachedList = cacheTargetFiles.map(async (file) => {
const cacheKey = precacheController.getCacheKeyForURL(file.filePath)
const cached = await caches
.match(cacheKey)
.then((response) => response !== undefined)
return cached
})
const cachedListResult = await Promise.all(cachedList)
const cachedListResultFilterd = cachedListResult.filter((response) => {
return response
})
if (cachedListResultFilterd.length === cacheTargetFiles.length) {
return Promise.resolve({ isCompleted: true })
} else {
return Promise.resolve(null)
}
}
await addCaches(cacheTargetFiles)
// ★Caching is started with this INSTALL
precacheController.install()
await processFuncPromise(checkCaches)
self.clients.matchAll().then((clients) =>
clients.forEach((client) => {
client.postMessage({ type: 'FINISHED_ADD_PRECACHE' })
})
)
}
})
addEventListener('install', (event) => {
event.waitUntil(precacheController.install())
event.waitUntil(self.skipWaiting())
})
addEventListener('activate', (event) => {
workbox.precaching.cleanupOutdatedCaches()
event.waitUntil(precacheController.activate())
event.waitUntil(self.clients.claim())
})
addEventListener('fetch', (event) => {
const cacheKey = precacheController.getCacheKeyForURL(event.request.url)
event.respondWith(
caches.match(cacheKey).then(function (response) {
// Cache hit - return the response from the cached version
if (response) {
return response
}
// Not in cache - return the result from the live server
// `fetch` is essentially a "fallback"
return fetch(event.request)
})
)
})
// ↓processFuncPromise()
// export const processFuncPromise = (func, interval = 500) => {
// const retryFunc = (resolve, reject) =>
// func()
// .then((result) => ({ result, isCompleted: result !== null }))
// .then(({ result, isCompleted }) => {
// if (isCompleted) {
// return resolve(result)
// } else {
// return setTimeout(() => retryFunc(resolve, reject), interval)
// }
// })
// .catch(reject)
// return new Promise(retryFunc)
// }
v6 ng
/* global importScripts, workbox, consola, processFuncPromise */
const cacheName = workbox.core.cacheNames.precache
const precacheController = new workbox.precaching.PrecacheController(cacheName)
addEventListener('message', async (event) => {
if (event.data.type === 'ADD_PRECACHE') {
const cacheTargetFiles = event.data.payload
const addCaches = async (cacheTargetFiles) => {
for (const file of cacheTargetFiles) {
const cacheKey = precacheController.getCacheKeyForURL(file.filePath)
const cached = await caches
.match(cacheKey)
.then((response) => response !== undefined)
if (!cached) {
precacheController.addToCacheList([{ url: file.filePath, revision: file.revision }])
}
}
}
const checkCaches = async () => {
const cachedList = cacheTargetFiles.map(async (file) => {
const cacheKey = precacheController.getCacheKeyForURL(file.filePath)
const cached = await caches
.match(cacheKey)
.then((response) => response !== undefined)
return cached
})
const cachedListResult = await Promise.all(cachedList)
const cachedListResultFilterd = cachedListResult.filter((response) => {
return response
})
if (cachedListResultFilterd.length === cacheTargetFiles.length) {
return Promise.resolve({ isCompleted: true })
} else {
return Promise.resolve(null)
}
}
await addCaches(cacheTargetFiles)
// ★ERROR
precacheController.install(event)
await processFuncPromise(checkCaches)
self.clients.matchAll().then((clients) =>
clients.forEach((client) => {
client.postMessage({ type: 'FINISHED_ADD_PRECACHE' })
})
)
}
})
addEventListener('install', (event) => {
precacheController.install(event)
event.waitUntil(self.skipWaiting())
})
addEventListener('activate', (event) => {
workbox.precaching.cleanupOutdatedCaches()
precacheController.activate(event)
event.waitUntil(self.clients.claim())
})
addEventListener('fetch', (event) => {
const cacheKey = precacheController.getCacheKeyForURL(event.request.url)
event.respondWith(
caches.match(cacheKey).then(function (response) {
// Cache hit - return the response from the cached version
if (response) {
return response
}
// Not in cache - return the result from the live server
// `fetch` is essentially a "fallback"
return fetch(event.request)
})
)
})
// ↓processFuncPromise()
// export const processFuncPromise = (func, interval = 500) => {
// const retryFunc = (resolve, reject) =>
// func()
// .then((result) => ({ result, isCompleted: result !== null }))
// .then(({ result, isCompleted }) => {
// if (isCompleted) {
// return resolve(result)
// } else {
// return setTimeout(() => retryFunc(resolve, reject), interval)
// }
// })
// .catch(reject)
// return new Promise(retryFunc)
// }
I asked the question on githubbut could not get an answer, so I came here.

I get Error: Can't perform a React state update on an unmounted component even though i created cleanup

An error keeps bothering me on my app says
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.
I do declare a useEffect in my Context so that I can have a realtime data storing and getting for my app.
Here is my code in Context;
const FetchProvider = ({ children }) => {
const [userList, setUserList] = useState([]);
const [teamList, setTeamList] = useState([]);
const authContext = useContext(AuthContext);
const authAxios = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
useEffect(() => {
let isCancelled = false;
if (
authContext.isAuthenticated &&
authContext.authState.userInfo !== null
) {
const getUsers = async () => {
try {
const users = await authAxios.get('/admin/get-all-users');
if (!isCancelled) {
setUserList(users.data);
}
} catch (error) {
if (!isCancelled) {
console.log(error);
}
}
};
const getTeams = async () => {
try {
const teams = await authAxios.get('/get-all-teams');
if (!isCancelled) {
setTeamList(teams.data);
}
} catch (error) {
if (!isCancelled) {
console.log(error);
}
}
};
getUsers();
getTeams();
}
return () => {
isCancelled = true;
};
}, [authAxios, authContext]);
return (
<Provider
value={{
authAxios,
userList,
setUserList,
teamList,
setTeamList,
}}
>
{children}
</Provider>
);
};
And I get this error in my Login.jsx and in my even though I don't declare useEffect in submitting and declaring it in .
Here is my code;
const submitCredentials = async (credentials, resetForm) => {
try {
setLoginLoading(true);
const { data } = await publicFetch.post('signin', credentials);
authContext.setAuthState(data);
setSignupSuccess(data.message);
setSignupError('');
setOpen(true);
setTimeout(() => {
setLoginLoading(false);
setredirectOnlogin(true);
resetForm(true);
}, 400);
} catch (error) {
setLoginLoading(false);
const { data } = error.response;
setSignupError(data.message);
setSignupSuccess('');
setOpen(true);
}
return setLoginLoading(false);
};
And I have tried many ways the internet has offered to fix this up but unfortunately it does not fix my problem.
I do have useEffect in my UsersTable.jsx and TeamTables.jsx.
Here is my code in UsersTable.jsx;
useEffect(() => {
let isCancelled = false;
const getUsers = async () => {
try {
const users = await fetchContext.authAxios.get('/admin/get-all-users');
setIsLoaded(true);
if (isLoaded === true) {
if (!isCancelled) {
fetchContext.setUserList(users.data);
}
}
return () => {
isCancelled = true;
};
} catch (error) {
if (!isCancelled) {
console.log(error);
}
}
};
getUsers();
return () => {
isCancelled = true;
};
}, [fetchContext, isLoaded]);
Here is my useEffect code in my TeamTable.jsx;
useEffect(() => {
let isCancelled = false;
const getTeams = async () => {
try {
const teams = await fetchContext.authAxios.get('get-all-teams');
setIsLoaded(true);
if (isLoaded === true) {
if (!isCancelled) {
fetchContext.setTeamList(teams.data);
}
}
} catch (error) {
if (!isCancelled) {
console.log(error);
}
}
};
getTeams();
return () => {
isCancelled = true;
};
}, [fetchContext, isLoaded]);
The isLoaded is used as an AJAX
Well, you can use the React recommended way to fix this issue. All you need to do is wrap your api call within makeCancellable method and cancel them when your component is unmounting.
Ref: https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
To do that create
const makeCancelable = (promise) => {
let isCancelled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => isCancelled ? reject({isCanceled: true}) : resolve(val),
error => isCancelled ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
isCancelled = true;
},
};
};
create a variable for the request outside your useEffect
let fetchTeamsRequest = null;
and updated your useEffect function like below.
useEffect(() => {
const getTeams = async () => {
if (fetchTeamsRequest) {
try {
await fetchTeamsRequest.promise;
return;
} catch (error) {
return;
}
}
fetchTeamsRequest = makeCancellable(fetchContext.authAxios.get('get-all-teams'));
try {
const teams = await fetchTeamsRequest.promise;
fetchTeamsRequest = null;
setIsLoaded(true);
if (isLoaded === true) {
if (!fetchTeamsRequest.isCancelled) {
fetchContext.setTeamList(teams.data);
}
}
} catch (error) {
if (!fetchTeamsRequest.isCancelled) {
fetchTeamsRequest = null;
console.log(error);
}
}
};
getTeams();
return () => {
if (fetchTeamsRequest) {
fetchTeamsRequest.cancel();
}
};
}, [fetchContext, isLoaded]);

How to handle specific errors code in a geolocation promise?

I have a function that fetches the user's location. It was working this way:
const fetchGeoLocation: SearchService["fetchGeoLocation"] = async () => {
const geo = navigator.geolocation;
if (!geo) throw new Error("error.geolocation-unavailable");
const handleError = (err: any) => {
if (err.code === 1) throw new Error("error.geolocation-permission_denied");
if (err.code === 2) throw new Error("error.geolocation-unavailable");
if (err.code === 3) throw new Error("error.geolocation-timeout");
};
const handleSuccess = (position) => {
return { location: [position.coords.longitude, position.coords.latitude] };
};
geo.getCurrentPosition(handleSuccess, handleError, { maximumAge: 10000 });
};
const onUpdateLocation = async () => {
onLoad();
fetchGeoLocation()
.then((res) => onSave(res.data))
.catch(({ message }) => onError(message));
};
Because it was not a promise, the onSave() function triggered before fetchGeolocation ended. So I have to promisify it. Writing this would work:
function fetchGeolocation () {
return new Promise((resolve, reject) =>{
navigator.geolocation.getCurrentPosition(resolve, reject);
});
};
fetchGeolocation()
.then(res => onSave(res)
.catch(err => onError(err.message);
But I would need to handle all the error codes in the catch callback. I want to handle everything inside the fetchGeolocation function. How to do it?
Thanks!
If I followed your idea properly, then the next snippet might help you out:
const fetchGeoLocation: SearchService["fetchGeoLocation"] = async () => {
return new Promise((resolve, reject) => {
const { geolocation } = navigator;
if (!geolocation) reject("error.geolocation-unavailable");
const handleError = ({ code }) => {
if (code === 1) reject("error.geolocation-permission_denied");
if (code === 2) reject("error.geolocation-unavailable");
if (code === 3) reject("error.geolocation-timeout");
};
const handleSuccess = (position) => {
resolve({ location: [position.coords.longitude, position.coords.latitude] });
};
geo.getCurrentPosition(handleSuccess, handleError, { maximumAge: 10000 });
});
};
Notice instead of throw'ing, it's reject'ing the promise with the error string.

get the return value from async function without calling it again

Here is the code:
const onStartRecord = async() => {
try {
const path = Platform.select({
ios: `file:///audio/${filenameGenerator}.m4a`,
android: `file:///audio/${filenameGenerator}.mp4`,
});
const audioSet: AudioSet = {
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVNumberOfChannelsKeyIOS: 2,
AVFormatIDKeyIOS: AVEncodingOption.aac,
};
console.log('audioSet', audioSet);
const uri = await audioRecorderPlayer.startRecorder(path, audioSet);
audioRecorderPlayer.addRecordBackListener((e: any) => {
setAudioProp(audioProp => {
return { ...audioProp,
recordSecs: e.current_position,
recordTime: audioRecorderPlayer.mmssss(Math.floor(e.current_position)),
}
});
});
console.log(`uri: ${uri}`);
return uri
} catch (err) {
console.log(err);
return;
}
};
const audioPath = async() => {
const result = await onStartRecord();
return result;
}
const onSubmit = async() => {
const audiopath = await audioPath();
console.log("this is the audiopath", audiopath)
}
};
I can get what I want when I trigger the onSubmit function, but the problem is, it also trigger the onStartRecord function again which will cause error in my case, I just want to get the uri generated when the onStartRecord resolved, but I don't want to trigger it again, so what can I do if I need to use the onSubmit function and get the value from onStartRecord? thx !
Instead of returning uri, onStartRecord should assign it to a global variable.
Then audioPath() can return that variable.
let savedAudioPath;
const onStartRecord = async() => {
try {
const path = Platform.select({
ios: `file:///audio/${filenameGenerator}.m4a`,
android: `file:///audio/${filenameGenerator}.mp4`,
});
const audioSet: AudioSet = {
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVNumberOfChannelsKeyIOS: 2,
AVFormatIDKeyIOS: AVEncodingOption.aac,
};
console.log('audioSet', audioSet);
const uri = await audioRecorderPlayer.startRecorder(path, audioSet);
audioRecorderPlayer.addRecordBackListener((e: any) => {
setAudioProp(audioProp => {
return { ...audioProp,
recordSecs: e.current_position,
recordTime: audioRecorderPlayer.mmssss(Math.floor(e.current_position)),
}
});
});
console.log(`uri: ${uri}`);
savedAudioPath = uri;
} catch (err) {
console.log(err);
return;
}
};
const audioPath = async () => savedAudioPath;

Possible Unhandled Promise Rejection (id: 0) React Native AsyncStorage

I am promisifying the React Native AsyncStorage getItem method but I am being warned that it is returning a possible unhandled promise rejection. Here's what I'm doing, what's wrong with my code?
In App.js ComponentDidMount()
componentDidMount() {
ConnectyCube.init(...config);
authInitialization = async () => {
const locallyStoredPhoneNumber = await getStoredPhoneNumber();
console.log(locallyStoredPhoneNumber);
authorizeFirebase(this.getFirebaseAccessToken);
this.props.authorizing(true);
}
authInitialization();
}
Then in localStorage.js
export const getStoredPhoneNumber = () => {
return new Promise((resolve, reject) => {
AsyncStorage.getItem('#phone_number', (error, result) => {
result ? resolve(result) : reject(error);
})
})
}
Thanks in advance.
UPDATE
I have now added error handling:
export const getStoredPhoneNumber = () => {
return new Promise((resolve, reject) => {
AsyncStorage.getItem('#phone_number', (error, result) => {
result ? resolve(result) : reject(error);
})
}).catch(error => console.error(error))
}
Seems to work - here's my extra logic that depends on the result of the AsyncStorage call:
componentDidMount() {
ConnectyCube.init(...config);
authInitialization = async () => {
const locallyStoredPhoneNumber = await getStoredPhoneNumber();
locallyStoredPhoneNumber !== undefined
? authorizeFirebase(this.getFirebaseAccessToken) && this.props.authorizing(true)
: this.setState({ newUser: true })
}
authInitialization();
}
Seems like this should work:
async componentDidMount() {
ConnectyCube.init(...config);
try {
const locallyStoredPhoneNumber = await AsyncStorage.getItem('#phone_number');
locallyStoredPhoneNumber !== undefined
? authorizeFirebase(this.getFirebaseAccessToken) && this.props.authorizing(true)
: this.setState({ newUser: true })
} catch (e){
// handle error
}
}
One way to handle promise rejection would be to use try...catch block where your promise is being returned.
try{
const locallyStoredPhoneNumber = await getStoredPhoneNumber();
} catch(error){
//Error handling code here
}
You need to 'catch' any errors which might be thrown and handle them (otherwise React will complain):
componentDidMount() {
authInitialization = async () => {
try {
const locallyStoredPhoneNumber = await getStoredPhoneNumber();
...
} catch (e) {
console.log(e) //handle error }
}
authInitialization();
}
}

Categories

Resources