Bennett Dams
Bennett Dams

Reputation: 7033

Vuex: Fetch data with axios based on already fetched data

I'm fetching data from an external API via axios in the Vuex store, which gives me an array with objects - let's say - cities.

Each city has a zipCode, which I need to fetch city details with. I want to add that details-object as a new key to the cities array.

Right now I'm fetching all cities and have another Vuex action to fetch the city details.

ACTION to fetch all cities

fetchCities: ({ commit, state, dispatch }) => {
  let baseUrl = "https://my-url/cities";

  let config = {
    headers: {
      accept: "application/json",
      Authorization: ...
    },
    params: {
      param1: "a",
      param2: "b"
    }
  };

  axios
    .get(baseUrl, config)
    .then(function(response) {
      commit("UPDATE_CITIES_RAW", response.data.items);
    })
    .catch(function(error) {
      console.log(error);
    });
  dispatch("fetchCityDetails");
},

MUTATION after fetching all cities

UPDATE_CITIES_RAW: (state, payload) => {
  state.citiesRaw = payload;
},

Each object in that array has a zipCode, which I'm fetching details about this city with.

I tried to loop the citiesRaw array inside the action to fetch the details and commit a change for each iteration, but the array from the state is empty at this point, because the action gets called before the mutation.

ACTION to fetch city details

fetchCityDetails: ({ commit, state }) => {
  let baseUrl = "https://my-url/cities/"

  let config = {
    headers: {
      accept: "application/json",
      Authorization: ...
    }
  };

  // citiesRaw is empty at this point
  state.citiesRaw.forEach(e => {
    let url = baseUrl + e.zipCode;

    axios
      .get(url, config)
      .then(function(response) {
        commit("UPDATE_CITY_DETAILS", {
          response: response.data,
          zipCode: e.zipCode
        });
      })
      .catch(function(error) {
        console.log(error);
      });
  });
},

What are the best ways to wait for the first fetch and then update the array?

Should I even use the same array or create a new one to begin with?

Or is there even a better way to fetch based on fetched data in the Vuex store?

UPDATE

After fixing the dispatching before the async function even finished (thanks, @Y-Gherbi), I also refactored the way of fetching the details:

  1. Component dispatches  fetchCities
  2. in fetchCities action: commit  UPDATE_CITIES
  3. in UPDATE_CITIES mutation: .map on the payload and create new object -> push all to state.cities
  4. in fetchCities action: loop state.cities & dispatch  fetchCityDetails(zipCode) for each city
  5. in fetchCityDetails action: commit  UPDATE_CITY_DETAILS
  6. in UPDATE_CITY_DETAILS mutation: .map on state.cities  and add  cityDetails object  to the referred city object

new actions

fetchCities: ({ commit, state, dispatch }) => {
  let baseUrl = "https://my-url/cities";

  let config = {
    headers: {
      accept: "application/json",
      Authorization: ...
    },
    params: {
      param1: "a",
      param2: "b"
    }
  };

  let url = baseUrl;

  axios
    .get(url, config)
    .then(function(response) {
        commit("UPDATE_CITIES", response.data.items);
        state.cities.forEach(city => {
          dispatch("fetchCityDetails", city.zipCode);
        });
      }
    })
    .catch(function(error) {
      console.log(error);
    });
},
fetchCityDetails: ({ commit }, zipCode) => {
  let baseUrl = "https://my-url/cities";

  let config = {
    headers: {
      accept: "application/json",
      Authorization: ...
    },
  };

  let url = baseUrl + "/" + zipCode;

  axios
    .get(url, config)
    .then(function(response) {
      commit("UPDATE_CITY_DETAILS", {
        cityDetails: response.data,
        zipCode: zipCode
      });
    })
    .catch(function(error) {
      console.log(error);
    });
}

new mutations

UPDATE_CITIES: (state, cities) => {
  // I don't need all data & I want to rename the keys from the response,
  // so I create a new object
  cities = cities.map(city => {
    let obj = {};
    obj.zipCode = city.zip_code
    obj.key1 = city.key_1;
    obj.key2 = city.key_2;

    return obj;
  });
  state.cities.push(...cities);
},
UPDATE_CITY_DETAILS: (state, payload) => {
  let cities = state.cities;
  // add one details-object for each city
  cities = cities.map(city => {
    if (city.zipCode == payload.zipCode) {
      city.cityDetails = payload.cityDetails;
    }
    return city;
  });
  state.cities = cities;
}

The question remains: Is there a better/more optimized approach to this kind of fetching?

Upvotes: 3

Views: 830

Answers (1)

Producdevity
Producdevity

Reputation: 882

I think that there are much better and elegant ways to deal with this but to simply help you with the bug (looping over an empty array that's expected to be an array of cities)

fix

Try moving the dispatch function to the next line after the commit function. The array (citiesRaw) is empty because you are calling the action before the data is fetched (because axios.get is an async operation)


alternative solution

As you said, the cities are displayed in a table with expandable rows, which are used to display the city details. Fetching 100-1000 cities is quite a lot of data but it's still one call to the back-end. But looping over all these items and doing a request for every one of them could be problematic from a performance and bandwidth (if that's the correct term for large data usage) perspective.

Personally, I would handle each request whenever the user actually needs this data. In your case whenever the user clicked on a row to expand it.

Storing it in the store?

Whenever you want to store the city details in the store is up to you. You could just keep it in the component but personally, I would use it as some kind of cache mechanism.

When the user clicked on a row, check if the details are already fetched

if(!state.cityDetails[key]) {
    axios.get()
        .then(res => {
            // Commit a mutation that saves the fetched details
        })
        .catch((error) => {
            // Handle error
        })
} else {
    // Use the 'cached' cityDetails
}

Your store could look something like this:

{
    cityDetails: {
        keyCouldBeIdOrZipcode: {...},
        anOtherAlreadyFetchedCity: {...},
    }
}

sorry for the typos and bad formatting. Typing code on a phone is horrible

Upvotes: 3

Related Questions