I'm working on a React Native app and I'm having a really hard time getting all my async calls to work correctly.
A rough outline of what I need to happen is:
- Get user location
- After getting user location, get
deals
from API (mocked withaxios-mock-adapter
) - After getting API results, convert the
Object
s toDeal
s, which includes waiting for a call to the Google Maps API to get the coordinates based on a text address. - After converting all the deals, dispatch the
Deal
s to the store. - After all this is done, (re)render the list of deals with their distance from the user's location.
I've put async/await
s everywhere I can, and kicked off step 3 from a yield call()
in my saga, but things are still happening out of order. Here's my code, followed by the log of the steps as they happen. Sorry, the numbers in the code don't exactly match up with the numbers in my list above.
It looks like the first relevant waiting failure occurs in Deal.collectionFromArray
or in its forEach
callback. Although await getLocationAsync()
in AppNavigator.js
also fails to wait first.
Can anybody help figure out why these aren't working and waiting? Much thanks.
Step 1, start of step 2. AppNavigator.js
(excerpt)
const TabNavigator = createBottomTabNavigator(
{
Map: MapStack,
List: ListStack
},
{ initialRouteName }
);
class TabContainer extends Component<Object> {
static router = TabNavigator.router;
async componentDidMount() {
console.log("getting location");
await this.props.getLocationAsync(); <-- await
console.log("location:", this.props.location);
console.log("getting deals");
await this.props.getDeals(); <-- await
}
render() {
return <TabNavigator navigation={this.props.navigation} />;
}
}
const SwitchNavigator = createSwitchNavigator({
// Auth: AuthNavigator,
Main: connect(
({ location }) => ({ location: location.currentRegion }),
{ getLocationAsync, getDeals }
)(TabContainer)
});
const AppNavigator = createAppContainer(SwitchNavigator);
type AppProps = {
navigation: Object,
getLocationAsync: () => void,
loadingMessage: string
};
class AppContainer extends Component<AppProps> {
static router = TabNavigator.router;
componentDidMount() {
// this.props.getLocationAsync();
}
render() {
return (
<View style={{ flex: 1 }}>
<LoadingIndicator message={this.props.loadingMessage} />
<AppNavigator navigation={this.props.navigation} />
</View>
);
}
}
export default connect(({ location, deals, auth }) => ({
loadingMessage:
auth.loadingMessage || location.loadingMessage || deals.loadingMessage
}))(AppContainer);
Step 2 & 4, dealActions.js
export async function getDealsApi(): Promise<Object[] | Error> {
try {
const res = await axios.get(ApiUrls.getProductsAuthorized); <-- await
console.log("2. API got", res.data.length, "deals (from getDealsApi)");
const deals = await Deal.collectionFromArray(res.data); <-- await
console.log("3. converted", Object.keys(deals).length, "deals with Deal.fromApi (from getDealsApi)" );
return deals;
} catch (error) {
console.warn("get products failed. is axios mock adapter running?");
return error;
}
}
export function* getDealsSaga(): Saga<void> {
try {
console.log("1. calling API (from getDealsSaga)");
const deals: DealCollection = yield call(getDealsApi); <-- yield call()
console.log("4. dispatching", Object.keys(deals).length, "deals (from getDealsSaga)" );
yield put({ type: "GET_DEALS_SUCCESS", deals });
} catch (error) {
console.warn("getDealsSaga", "error:", error);
yield put({ type: "GET_DEALS_FAILURE", error });
}
}
Step 3, Deal.js
static async collectionFromArray(array: Object[]) {
const deals: DealCollection = {};
await array.forEach(async p => { . <--await
deals[p.id] = await Deal.fromApi(p); <--await
});
console.log("converted:", Object.keys(deals).length);
return deals;
}
static async fromApi(obj: Object): Promise<Deal> {
const deal = new Deal();
deal.id = obj.id;
deal.name = obj.name;
// ...set other properties
if (deal.address) {
const location = await Deal.getLocationForAddress(deal.address); <-- await
deal.location = location;
}
// console.log(deal.location);
return deal;
}
static async getLocationForAddress(address: string): Promise<?Location> {
try {
const search = await fetch(ApiUrls.mapsSearch(address)); <-- await
const { predictions, ...searchData } = await search.json(); <-- await
if (searchData.error_message) throw Error(searchData.error_message);
if (!predictions.length) throw Error("No places found for " + address);
const details = await fetch(ApiUrls.mapsDetails(predictions[0].place_id)); <-- await
const { result, ...detailsData } = await details.json(); <-- await
if (detailsData.error_message) throw Error(searchData.error_message);
const location = result.geometry.location;
return {
latitude: location.lat,
longitude: location.lng
};
} catch (error) {
console.log("Error getting location for address in deal:", error);
}
}
Leaving out the code for step 5, pretty self-explanatory.
Logs, with comments:
getting location
location: null // even this first call isn't waiting!
getting deals
1. calling API (from getDealsSaga)
2. API got 10 deals (from getDealsApi()) // will this wait when we're calling the actual web API?
converted: 0 // not waiting
3. converted 0 deals with Deal.fromApi (from getDealsApi) // previous log didn't wait so of course this is empty
4. dispatching 0 deals (from getDealsSaga) // ditto. perhaps yield call() is working but all the non-waiting above makes that irrelevant
// after all this is done, the Google Maps calls finally return, nothing is dispatched or rerendered.
lat: 40.78886889999999, long: -73.9745318
lat: 40.78886889999999, long: -73.9745318
lat: 40.78886889999999, long: -73.9745318
// ... and the rest of the coordinates, finally returned