0

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:

  1. Get user location
  2. After getting user location, get deals from API (mocked with axios-mock-adapter)
  3. After getting API results, convert the Objects to Deals, which includes waiting for a call to the Google Maps API to get the coordinates based on a text address.
  4. After converting all the deals, dispatch the Deals to the store.
  5. After all this is done, (re)render the list of deals with their distance from the user's location.

I've put async/awaits 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
Jonathan Tuzman
  • 11,568
  • 18
  • 69
  • 129

1 Answers1

0

It looks like my problem was awaiting inside of forEach.

As mentioned in this answer, awaiting inside of forEach doesn't work as I expected. Using for...in, though just a tiny bit more verbose, works:

  // in Deal.js
  async setLocation() {
    this.location = await Deal.getLocationForAddress(this.address);
  }

  // inside of getDealsApi()
  for (const id in deals) {
    const deal = deals[id];
    if (!deal.location && deal.address) await deal.setLocation();   
  }
Jonathan Tuzman
  • 11,568
  • 18
  • 69
  • 129