Clean up async function in an useEffect React hook - reactjs

I have the following useEffect function and trying to find the best way to clean this up when the component unmounts.
I thought it would be best to follow the makeCancelable from the React docs, however, the code still executes when the promise is cancelled.
const makeCancelable = (promise) => {
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;
},
};
};
//example useEffect
useEffect(() => {
const getData = async () => {
const collectionRef_1 = await firestore.collection(...)
const collectionRef_2 = await firestore.collection(...)
if (collectionRef_1.exists) {
//update local state
//this still runs!
}
if (collectionRef_2.exists) {
//update local state
//and do does this!
}
}
const getDataPromise = makeCancelable(new Promise(getData))
getDataPromise.promise.then(() => setDataLoaded(true))
return () => getDataPromise.cancel()
}, [dataLoaded, firestore])
I have also tried const getDataPromise = makeCancelable(getData) without any luck. The code executes fine, just doesn't clean up correctly when the component unmounts.
Do I need to also cancel the two await functions?

In your makeCancelable function you are just checking the value of hasCanceled_ after the promise has finished (meaning getData has already executed entirely):
const makeCancelable = (promise) => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
// AFTER PROMISE RESOLVES (see following '.then()'!), check if the
// react element has unmount (meaning the cancel function was called).
// If so, just reject it
promise.then(
val => hasCanceled_ ? reject({isCanceled: true}) : resolve(val),
error => hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
Instead, in this case I would recomend you to go for a simpler and more classic solution and use a isMounted variable to create the logic you want:
useEffect(() => {
let isMounted = true
const getData = async () => {
const collectionRef_1 = await firestore.collection(...)
const collectionRef_2 = await firestore.collection(...)
if (collectionRef_1.exists && isMounted) {
// this should not run if not mounted
}
if (collectionRef_2.exists && isMounted) {
// this should not run if not mounted
}
}
getData().then(() => setDataLoaded(true))
return () => {
isMounted = false
}
}, [dataLoaded, firestore])

Related

Why am i getting an undefined output when I try to access value returned from async method

I have the following method which returns an object with 3 fields inside a different file named localStorage:
const getUserProfileData = async () => {
try {
await AsyncStorage.getItem(CONSTANTS.USER_PROFILE).then((item) => {
let retrievedProfile = JSON.parse(item);
return retrievedProfile;
});
} catch (e) {
throw e;
}
};
here is my file profile.js:
useEffect(() => {
const retrieveProfileData = async () => {
let retProfile = await localStorage.getUserProfileData();
console.log("check what: ",retProfile);
};
retrieveProfileData();
}, []);
inside the use effect, when I attempt to log out the result I get an output of:
check what: undefined
I have read other forums on similar problems to this but I can't seem to notice where I'm going wrong?
I think it has to do with you mixing async and .then(). Try this way:
const getUserProfileData = async () => {
try {
const result = await AsyncStorage.getItem(CONSTANTS.USER_PROFILE)
const retrievedProfile = JSON.parse(result);
return retrievedProfile;
} catch (e) {
throw e;
}
};
const getUserProfileData = async () => {
return AsyncStorage.getItem(CONSTANTS.USER_PROFILE);
};
useEffect(() => {
const retrieveProfileData = async () => {
try {
let retProfile = JSON.parse(await localStorage.getUserProfileData());
console.log("check what: ",retProfile);
} catch (error) {
// handle error
}
};
retrieveProfileData();
}, []);

ReactJs Unable to setSate in componentDidMount from async function

I'm calling an async function (getData()) in componentDidMount, and I'm trying to use this.setState with result of that function.
componentDidMount() {
let newData = getData();
newPodData.then(function (result) {
console.log('result', result)
this.setState({result})
})
}
However, I'm having issues getting my state to properly update. Some additional context - I'm trying to set my initial state with data I am receiving from a database. Is my current approach correct? What's the best way to accomplish this? Here's my async function for more context:
const getTeamData = async () => {
const getTeamMembers = async () => {
let res = await teamMemberService.getTeamMembers().then(token => { return token });
return res;
}
const getActiveTeams = async () => {
let res = await teamService.getActiveTeams().then(token => { return token });
return res;
}
const teamMemberResult = await getTeamMembers()
const activeTeamsResult = await getActiveTeams();
// get team member data and add to teamMember object
let teamMemberData = teamMemberResult.reduce((acc, curr) => {
acc.teamMembers[curr.id] = curr;
return acc;
}, {
teamMembers: {}
});
// get team ids and add to teamOrder array
let activeTeamsData = activeTeamsResult.map(team => team.id)
let key = 'teamOrder'
let obj = []
obj[key] = activeTeamsData;
const newObject = Object.assign(teamMemberData, obj)
return newObject;
}
export default getTeamData;
Changing the function inside the then handler to an arrow function should fix it. e.g:
componentDidMount() {
let newData = getData();
newPodData.then((result) => {
console.log('result', result)
this.setState({result})
})
}
But I'll like to suggest a better way to write that.
async componentDidMount() {
let result = await getData();
this.setState({result})
}

using axios with promise API

I am using a promise based hook in a React app to fetch async data from an API.
I am also using a Axios, a promise based http client to call the API.
Is it an anti-pattern to use a promise based client inside another promise? The below code does not seem to work.
const getData = () => {
return new Promise((resolve, reject) => {
const url = "/getData";
axios.get(url)
.then(function(response) {
resolve(response);
})
.catch(function(error) {
reject(error);
});
});
const useAsync = (asyncFunction) => {
const [value, setValue] = useState(null);
const execute = useCallback(() => {
setPending(true);
setValue(null);
setError(null);
return asyncFunction()
.then(response => setValue(response))
.catch(error => setError(error))
.finally(() => setPending(false));
}, [asyncFunction]);
useEffect(() => {
execute();
}, [execute]);
return { execute, pending, value, error };
};
};
const RidesList = () => {
const {
pending,
value,
error,
} = useAsync(getData);
Oh man. I think you have a fundamental misunderstanding about how Promises work.
First, axios already returns a Promise by default. So your whole first function of getData can be reduced to:
const getData = () => {
const url = "/getData"
return axios.get(url)
}
But the meat of your code seems to indicate you want a querable Promise - so you can check the status of it for whatever reason. Here's an example of how you would do it, adapted from this snippet:
function statusPromiseMaker(promise) {
if (promise.isResolved) return promise
let status = {
pending: true,
rejected: false,
fulfilled: false
}
let result = promise.then(
resolvedValue => {
status.fulfilled = true
return resolvedValue
},
rejectedError => {
status.rejected = true
throw rejectedError
}
)
.finally(() => {
status.pending = false
})
result.status = () => status
return result
}
In this way, you can then do something like let thing = statusPromiseMaker(getData()) and if you look up thing.status.pending you'll get true or false etc...
I didn't actually run what's above, I may have forgotten a bracket or two, but hopefully this helps.
I have to admit - I haven't seen anything like this ever used in the wild. I am interested in knowing what you're actually trying to accomplish by this.
Axios itself returns a promise but if you want to make a custom class having your custom logic after each API call then you can use interceptors I was having the same requirement and this is how I am returning promises after applying my custom logic on each API call.
Interceptors will get executed separately after and before each request you made so we can simply use them if we want to modify our request or response.
here is my working solution have a look at it.
callApi = (method, endpoint, params) => {
this.apiHandler.interceptors.request.use((config) => {
config.method = method
config.url = config.baseURL + endpoint
config.params = params
return config
})
return new Promise((resolve, reject) => {
this.apiHandler.interceptors.response.use((config) => {
if (config.status == 200) {
resolve(config.data)
} else {
reject(config.status)
}
// return config
}, error => reject(error))
this.apiHandler()
})
}
Below is the code to call this function
helper.callApi("get", "wo/getAllWorkOrders").then(d => {
console.log(d)
})

Promise not working in React useEffect in combination with Firebase

I want to get data from firebase inside a useEffect function like this:
useEffect(() => {
/** nope */
async function fetchData() {
let dataObject = {};
let dataArray = [];
setAttendees({});
// You can await here
if (newData[listRedux]) {
const request = await Object.keys(newData[listRedux] .
[1].attendees).map(
user => {
usersRef.child(user).on('value', snap => {
dataObject[snap.key] = snap.val();
setAttendees(dataObject);
console.log(dataObject);
let comp = (
<Avatar
key={snap.key}
size="small"
source={snap.val().avatar}
alt={snap.val().name}
/>
);
dataArray.push(comp);
setAttendeesComp(dataArray);
});
}
);
// Wait for all requests, and then setState
await Promise.all(request).then(() => {
console.log('done');
});
}
}
fetchData();
}, [newData, listRedux]);
Now the second console.log inside the promise all will first show then the first console.log, meaning the request was not done yet.
How can i improve my code so the request and the states are first being set and then continue with the rest?
export default function Example() {
const [data, dataSet] = useState(false)
const [attendees, setAttendees] = useState(false)
async function fetchMyAPI() {
let response = await fetch('api/data')
response = await res.json()
console.log(response);
dataSet(response)
}
useEffect(() => {
if (!attendees) return
fetchMyAPI();
}, [attendees, newData, listRedux]);
useEffect(() => {
setAttendees({})
}, [])
More examples here:

need to know when all async operation completed

I have the following code:
callAPI() {
return this.myModel.map(action(retValue => {
return this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
})).catch(error => {
throw(error);
});
}));
if my client code I am doing something like this:
myStore.callAPI().then(() => {
console.log("completed");
this.setState({ loading: false });
});
I am getting an error
.then() is not a function
I want to get a callback when all async operations are completed.
Please use Promise.all to await multiple promises and return the result which is another Promise which you can call .then on.
In order to always return a promise after an imperative block of code you can return Promise.resolve() that will make the code chainable.
callAPI() {
return Promise.all(
this.myModel.map(action(retValue => (
this.myApi.doWork(retValue.Input).then(action(model => {
this.model.Input = Object.assign({}, model);
this.saveState();
return Promise.resolve();
}))
)));
);
}
Please see an example:
const sleep = timeout => new Promise(resolve =>
setTimeout(() => resolve(timeout), timeout)
);
const tap = timeout => {
console.log(`Task completed after ${timeout}ms`);
return timeout;
}
const tasks = [
sleep(1000),
sleep(2000),
sleep(500),
sleep(30),
sleep(2500),
sleep(450)
].map(task => task.then(tap));
Promise.all(tasks).then(x => {
console.log(x);
});

Resources