JS async function awaits forever - javascript

I have read a lot about async await, but apparently I still don't get it. ;-)
I am trying to transform the following .then promise structure into async await:
componentDidMount() {
const { store } = this.props
Promise.all([
API.fetchTodos(),
API.fetchGoals(),
]).then(([ todos, goals ]) => {
store.dispatch(receiveDataAction(todos, goals))
})
store.subscribe(() => this.forceUpdate())
console.log('test')
}
My result is:
async componentDidMount() {
const { store } = this.props
const [todos, goals] = await Promise.all([
API.fetchTodos(),
API.fetchGoals(),
])
store.dispatch(receiveDataAction(todos, goals))
store.subscribe(() => this.forceUpdate())
console.log('test')
}
The result is that this function never ends. It calls everything including the console.log, but then the program just stops (no error). I'm not showing you any other parts of the application because according to my understanding these two functions should be equivalent - so the rest should not matter. Apparently I am wrong! :-) What am I doing wrong and why doesn't my solution work?

The difference between your two snippets is that in the second async/await example, you don't subscribe to the store until after you've fetched the goals and todos, whereas in the first, you subscribe immediately.
So your second example isn't working since now you've guaranteed that
store.dispatch(receiveDataAction(todos, goals))
is called before
store.subscribe(() => this.forceUpdate())
and since the action has already been dispatched by that point, the subscription callback is never called.
To fix that, you might just want to move the subscription part so that it occurs before the await call. That way you are already subscribed before the promise has resolved. So something like this:
async componentDidMount() {
const { store } = this.props
// Move to the front so this happens before the await.
store.subscribe(() => this.forceUpdate())
const [todos, goals] = await Promise.all([
API.fetchTodos(),
API.fetchGoals(),
])
store.dispatch(receiveDataAction(todos, goals))
console.log('test')
}

Related

Can someone help me understand how async + await + useEffect work in React?

I have a React app built with the Minimal template and I'm trying to follow along with one of their tutorials, in order to create a Redux slice that feeds some data to a custom component. The data itself is collected from Firebase. Below is my code:
firebase.js - helper
export function getDocuments(col) {
const colRef = collection(db, col);
const q = query(colRef, where('uid', '==', auth.currentUser.uid));
getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
// return [1,2,3]
}
product.js - Redux slice
export function getProducts() {
return async (dispatch) => {
dispatch(slice.actions.startLoading());
try {
const products = await getDocuments('products');
dispatch(slice.actions.getProductsSuccess(products));
} catch (error) {
dispatch(slice.actions.hasError(error));
}
};
}
ProductList.js - component
const dispatch = useDispatch();
const { products } = useSelector((state) => state.client);
useEffect(() => {
dispatch(getProducts());
}, [dispatch]);
useEffect(() => {
if (products.length) {
// setTableData(products);
}
}, [products]);
If I console log data in the helper function (firebase.js), I get the values I expect, once the promise is resolved/fulfilled. However, if I console.log clients in the product.js slice or later in the component, I get undefined.
I assume my problem is not being able to understand how async + await + useEffect work together in order to fix this. My assumption is that I am trying to access the value before the promise is resolved and therefore before the helper function returns it. I confirmed that by returning a simple array [1, 2, 3] in my helper function as a test.
I think I am missing something fundamental here (I am not very experienced with React and JS in general and still learning things on the go). Can someone help me understand what am I doing wrong?
Thank you!
With await you can await the fulfillment or rejection of a promise, but your getDocuments Function does not return a promise. Change the last line of the function to the following:
return getDocs(q).then((snap) => {
const data = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
return data;
});
Async and Await are no different in React than in plain JavaScript:
When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method
useEffect():
By using this Hook, you tell React that your component needs to do something after rendering. This function will run every time the component is re-rendered.

Dependency between two async actions in Vue

My Home.vue needs two async actions:
created() {
this.$store.dispatch('GET_LATEST_POLL');
this.$store.dispatch('INIT_STREAM');
},
This is their Vuex implementation:
GET_LATEST_POLL: async (context) => {
const pollData = await axios.get(`${BFF_ENDPOINT}/polls/last`, getAuthHeader(context));
const item = pollData.data.data;
context.commit('SET_LATEST_POLL', item);
},
INIT_STREAM: async (context) => {
const streamData = await axios.get(`${API_ENDPOINT}/polls/?obd=date`, getAuthHeader(context));
const items = streamData.data.data;
items.filter(item => item._id !== context.state.latestPoll._id);
context.commit('SET_STREAM', items);
},
I realized there is a dependency of INIT_STREAM to LATEST_POLL on context.state.latestPoll. I do not want to serialize both actions, I want they both talk to the backend paralelly. But then I need INIT_STREAM to wait for the LATEST_POLL result.
How can I achieve it? Do I have to merge the logic into the single action which fires two promises and then await Promise.all([latestPoll, items])? Is this correct approach?
I would suggest you to have a single action dispatched from your component which in turn dispatches other 2 actions which do not mutate anything.
LatestPollAndStreamAction: async (context) => {
let pollDataRequest = axios.get(...);
let streamDataRequest = axios.get(...);
Promise.all(pollDataRequest , streamDataRequest).then(([latestPoll, items]) => {
const pollItem = latestPoll.data.data;
context.commit('SET_LATEST_POLL', pollItem);
//logic for the other commit
const streamItems = streamData.data.data;
streamItems.filter(item => item._id !== context.state.latestPoll._id);
context.commit('SET_STREAM', streamItems);
})
}
This will ensure that both of your requests are fired in parallel and once you have the result from both using do the commits in desired order.
PS: The code is not tested but is just you give a gist of the desired approach
As I interpreted the question, you were suggesting creating a third action that merged the two actions. That approach sounds okay, but I think you could also adjust INIT_STREAM to make the calls in parallel. You need to:
Make sure GET_LATEST_POLL returns its promise.
Dispatch the action to GET_LATEST_POLL from INIT_STREAM, but don't await the result - just store the return value as const getLatestPollPromise.
Make the axios call for const streamData = await axios.get... but again, change it from await to const streamDataPromise = axios.get...
Use await Promise.all([getLatestPollPromise, streamDataPromise]) like you proposed in the question.
If INIT_STREAM always needs latestPoll, I see no need for a third action as that qualifies as a leaky abstraction and will probably confuse other developers.

Functions not firing in correct order with async / await

In an Angular service, I have created the following function:
getListKey(user) {
firebase.database().ref(`userprofile/${user.uid}/list`).once('value').then(snapshot => {
console.log(snapshot.val())
this.listKey = snapshot.val()
return this.listKey
})
}
I want to call this function in another file on load, and assign the value brought back to a global listKey variable in the service to be used for another function from the component. However, the second function is firing before the data has been retrieved even with using async/await.
This is the relevant piece from my component:
this.afAuth.authState.subscribe(async (user: firebase.User) => {
await this.fire.getListKey(user);
this.fire.getUserList(this.fire.listKey).subscribe(lists => {...})
...
}
How can I make getUserList() wait for listKey?
Add a return statement to getListKey to return the promise. Otherwise, you're returning undefined, and awaiting undefined will not wait for the database snapshot to be ready.
getListKey(user) {
return firebase.database().ref(`userprofile/${user.uid}/list`).once('value').then(snapshot => {
console.log(snapshot.val())
this.listKey = snapshot.val()
return this.listKey
})
}
Also, you probably want a left side to your await:
this.afAuth.authState.subscribe(async (user: firebase.User) => {
const listKey = await this.fire.getListKey(user);
this.fire.getUserList(listKey).subscribe(lists => {...})
...
}

How do a resolve a promise to setstate in react

I am new to react and javascript and having trouble trying to retrieve a value from a promise so that it can be used for operations. I have did some research and seen a lot of promise tutorials that can return a console log or run a function. But I have yet to see one that can allow me to save to a const/var so I can use for other operations like a setstate.
I have tried a different ways to resolve a promise from an async function so I can do a setstate but they all failed, I have narrowed it down to 3 ways that I have tried which console logs the right information, but when I setstate it fails.
This is a sample of my react component
state = {
user: {}
}
getCurrentUser = async () => {
// to save the user details if they are logged in
const jwt = localStorage.getItem('token')
return jwtDecode(jwt)
}
componentDidMount() {
// method 1
// returns a promise instead of a value so setstate fails
let user = this.getCurrentUser()
console.log(user)
this.setState({user: user})
console.log(this.state)
// method 2
// trying to resolve a promise and return a value so I save to a variable and then setstate
user = this.getCurrentUser()
user = user.then((value) => {
//console log prints out exactly what I need
console.log(value)
return value
})
console.log(user)
this.setState({user: user})
console.log(this.state)
// method 3
// trying to do setstate inside the promise also fails
user = this.getCurrentUser()
user.then((value) => {
this.setState({user: value})
})
console.log(this.state)
}
Thank you for any tips anyone might have on how to resolve this, or if I am misunderstanding concepts on async or promises.
setState is async operation, Where second parameter is callback function which is executed after setState function is performend.
you can do
let user = this.getCurrentUser();
user.then(userData => {
console.log(userData)
this.setState({user: userData}, () => {
console.log(this.state)
})
})
I don't know exactly what you need, but this.setState takes second argument as a callback, so something like this will display the updated state
this.setState({user: user}, () => console.log(this.state));
Also something like this should work:
user = this.getCurrentUser()
user = user.then((value) => {
//console log prints out exactly what I need
console.log(value)
return value
}).then((user) =>
this.setState({user}, () => console.log(this.state))
);
And you should use await in your async function to wait the data.

Mocking APi calls jest

I have a DataService which is responsible for my API calls - I am trying to mock the api call in jest but it does not seem to be working. I am not sure what I am doing wrong - my DataService seems to be undefined.
Here is the function
const getStepData = (id) => {
return async dispatch => {
try {
dispatch(fetchStepBegin());
const res = await DataService.fetchStepData(id);
const sortedTask = sortedTaskData(res)
const sortedStepData = sortStepData(res)
const newData = createSortedDataForDragAndDrop(sortedTask, sortedStepData)
dispatch(fetchRawStepDataSuccess(res.data))
dispatch(fetchStepDataSuccess(newData))
}
catch (err) {
dispatch(fetchStepError(err))
throw (err)
}
}
}
Here is the test that I have written - I am pretty sure I am mocking incorrectly
it('Data Api end point called with corrent studyId', () => {
jest.mock(DataService);
DataService.fetchStepData() = jest.fn()
CellStepManagementOperations.getStepData(5);
expect(DataService.fetchStepData).toHaveBeenCalledWith(5);
});
I think the problem here is that you are trying to test asynchronous action creators, synchronously. So your expect function doesn't wait for your getStepData to finish before running.
I've had to something very similar to what you're trying to do and I used a library called redux-testkit. Please see the part about testing async action creators with services here. You can even set mock return values for your API services which I've found very helpful when testing.
Using this library, you will be able to await for your getStepData async action creator to complete before running your expect function.
You will have to play around with your code but it might look something like this:
it('Data Api end point called with corrent studyId', () => {
jest.mock(DataService);
DataService.fetchStepData() = jest.fn()
const dispatches = await Thunk(CellStepManagementOperations.getStepData).execute(5);
expect(DataService.fetchStepData).toHaveBeenCalledWith(5);
});

Categories

Resources