2

I have an array in which another array of agents is stored. In this agents array only the id of each agent is located. Using the id's, I fetch the data I need for each agent and I want to replace the original agent array with the new, completed agent data. Or at least push the new data to the specific agent. Here is what I have tried so far. Is there a simple way to do it?

How to use the fetched agent in filteredEvents? As seen in my expected result

filteredEvents: [
 { 
  agents: ['id', 'id'], // the id's are as plain text located
  eventData: ''
 }
]


// Expected result
filteredEvents: [
 { 
  agents:
  [
    { id: '', name: '', ...}, // the data from the fetched id
    { id: '', name: '', ...},
  ],
  eventData: ''
 }
]
otherAgentsEvents() {
  // Not that relevant currently
  // const events = this.events
  // const user = this.getUser._id
  // const filteredEvents = events.filter(event => !event.agents.includes(user));

  filteredEvents.forEach(event => {
    const agents = event.agents

    agents.forEach(agent => {
      const data = this.fetchAgentData(agent)
      // replace 'event.agents[agent]' with this data
    })
  })

  return filteredEvents
}
async fetchAgentData(agentID) {
  try {
    const agent = await this.$axios.$get(`/api/get-specific-agent/${agentID}`)

    if (agent.success) {
      return agent.data
    }
  } catch (err) {
    console.log(err)
  }
}

Html for better understanding

<div v-for="(event, key) in otherAgentsEvents" :key="key">

  <div v-for="agent in event.agents" :key="agent.id">
    // Currently I haven't access to first_name etc. 
    // only to agent where only the plain ID is stored
    <p>{{ agent.first_name }}</p>
  </div>
  
  <div>
    {{ event.eventData }}
  </div>

</div>

Update based on tao's answer

I have tried to implement the code, but am probably doing something wrong. Here's what I did.

I use vuex, I have never worked with pinia. Reactive is not defined, I left it out. I don't know where to get it.

import axios from 'axios'

const store = {
  agents: [],
  getAgent: (id) =>
    new Promise((resolve) => {
      console.log(id)
      const agent = store.agents.find(({ id: i }) => i === id)
      if (agent) {
        resolve(agent)
      } else {
        axios.get(`/api/get-specific-agent/${id}`).then(({ data }) => {
          store.agents.push(data)
          resolve(data)
        })
      }
    }),
}

export const useAgents = () => store
otherAgentsEvents() {
  // const events = this.events
  // const user = this.getUser._id
  // const filteredEvents = events.filter(event => 
  // !event.agents.includes(user))

  filteredEvents.map(async (event) => {
    const agents = event.agents

    event.agents = await Promise.all(
      agents.map((id) => useAgents().getAgent(id))
    )
    return event
  })
    
  return filteredEvents
},

Here I actually achieve the wanted result for a short time, but then my app crashes with following errors.

Console Console

PhilippeUn
  • 190
  • 2
  • 16
  • 1
    A `.map` or even `.reduce` + [async calls](https://stackoverflow.com/a/40140562/8816585) could be a tidier function. – kissu Oct 20 '22 at 20:15
  • Don't forget the `async/await` here. In your `return data` you could have the whole agent object returned that will loop and generate an array out of. I mean, it's already doing that almost. What don't you like in the shape of your final object? – kissu Oct 21 '22 at 08:08
  • Thank you! I had unfortunately not much time the last few days. My problem is that I don't know exactly how to push the new data into a new array (considering all the other data, how the above expected array should look like) or just replace it at the existing array. – PhilippeUn Oct 22 '22 at 09:50
  • Can you please show me how async/await should look like in `return data`? I use async/await in fetchAgentData(). Is there anything missing here? – PhilippeUn Oct 22 '22 at 09:50
  • This is not how to make an async map, check that one again: https://stackoverflow.com/a/71970498/8816585 – kissu Oct 23 '22 at 10:51
  • 1
    `reactive` is importable from `vue`, on `v2.7` and above. Prior to that, it was available from `@vue/composition-api` plugin. From the errors you're showing, it looks like you have a backend problem. You need to solve it, but it's a different problem. Perhaps another question :) – tao Oct 24 '22 at 10:45

4 Answers4

2

If i didn't misunderstand you have a problem with async operation, forEach method should not use with async operations, I prefer you to use for .. of .. loop instead of it. It can be done with;

for (const event of filteredEvents) {
    for (let agent of event.agents) {
       const data = this.fetchAgentData(agent);
       agent = data;
    }
}
2

As per my understanding, You want to basically replace the agent id's with the whole agent object based on the id's. If Yes, There could be a two scenario :

  1. If you want to replace all the id's with the respective objects on component load itself. Then you can fetch all the agents data in a single go with a single API and then replace the id's with the objects at client side. (Recommended) as it will improve the performance.
  2. If you want to load the single agent data at a time. i.e. on some event against each id, then you can go for getting the single agent data.

Now, let's talk about the implementation (As per 1st scenario). You can achieve this requirement with the help of Array.map() along with the Array.find() method.

Live Demo :

new Vue({
  el: '#app',
  data: {
    filteredEvents: [
      { 
        agents: [1, 2, 3]
      }
    ]
  },
  mounted() {
    // I am just use mock response, You can replace it with actual API call.
    const apiResponse = [{
      id: 1,
      first_name: 'Agent 1'
    }, {
      id: 2,
      first_name: 'Agent 2'
    }, {
      id: 3,
      first_name: 'Agent 3'
    }, {
      id: 4,
      first_name: 'Agent 4'
    }, {
      id: 5,
      first_name: 'Agent 5'
    }];
    this.filteredEvents = this.filteredEvents.map(obj => {
      const agents = obj.agents;
      obj.agents = agents.map(agentId => {
        return apiResponse.find(({ id }) => id === agentId);
      });
      return obj;
    });
    console.log(this.filteredEvents);
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <div v-for="(event, key) in filteredEvents" :key="key">
    <div v-for="agent in event.agents" :key="agent.id">
      <p>{{ agent.first_name }}</p>
    </div>
  </div>
</div>
Debug Diva
  • 26,058
  • 13
  • 70
  • 123
  • Thank you so much! This is exactly what I was looking for. It almost works! I get the agent data loaded for a second, then unfortunately it becomes undefined. Do you know what could be the reason for this? I adapted your code a bit to my structure. – PhilippeUn Oct 23 '22 at 19:59
  • @PhilippeUn It should work, once data came from an API you can store that in a data object property and then perform the array methods on that. – Debug Diva Oct 24 '22 at 02:36
  • @PhilippeUn As per the code snippet shared by me, it's persistent . To look into the exact issue "why it is only loaded for a second" I need to look into your code. Can you please share the jsfiddle or codepen link. – Debug Diva Oct 24 '22 at 10:35
1

From the comment you placed under Rohit's answer, I suspect you have a state management problem.

Here's how to solve it:

  • if you don't already have one, create a store (I recommend pinia; you can also use vuex; below I'll use a plain reactive object, for brevity)
  • place an empty agents array in store's state and create a getAgent action.
    The action takes an agent id as parameter and returns a promise, resolving the agent with that id.
    If the agent exists in state, it's returned from there. if not, it's fetched from server and also added to state, so it won't need fetching when/if it's requested again.

Proof of concept:

const store = reactive({
  agents: [],
  getAgent: (id) =>
    new Promise((resolve) => {
      const agent = store.agents.find(({ id: i }) => i === id);
      if (agent) {
        resolve(agent);
      } else {
        axios.get(`/api/get-specific-agent/${id}`).then(({ data }) => {
          store.agents.push(data);
          resolve(data);
        });
      }
    }),
});

export const useAgents = () => store;

Wherever you need to render agents:

import { useAgents } from "path/to/it";

// ...
const agents = await Promise.all(
  event.agents.map((id) => useAgents().getAgent(id))
);
// now use `agents` in <template>

Based on request in comment, here's how a vuex implementation would look like: https://codesandbox.io/s/eager-haze-xpldzp?file=/src/components/HelloWorld.vue.


Alternate solution: abstract away the loading into a dedicated component, featuring a slot, in which the agent object becomes available once loaded.

Note: this can also be done using a directive, but it's slightly more cumbersome. I find the slotted approach neat and flexible.

The difference between the two approaches is that in the first one the parent loads all agents and then renders them. In the second, each child is responsible for loading its own data and can exit loading phase as soon as its data is available, not depending on the other children.

tao
  • 82,996
  • 16
  • 114
  • 150
  • Thanks for your time! Unfortunately I didn't manage to implement your solution cleanly and extended my question with my attempt... – PhilippeUn Oct 24 '22 at 09:26
  • @PhilippeUn, I've updated my answer with a `vuex` implementation. If you can't get it working, I'll need to look at what you have. Consider [importing](https://codesandbox.io/docs/learn/getting-started/your-first-sandbox#import-local-projects-via-cli) your local into codesandbox using CLI. – tao Oct 24 '22 at 10:12
  • I've made some more tweaks to it. Should work. Check it for typos, it's written without actually testing. Although I do believe it should work. – tao Oct 24 '22 at 10:39
  • 1
    Tao, thanks again so much for your incredible help! I'm in the process of implementing it right now. In the method I still had to add `Promise.all(ids.map((id) => ...`. Yes, I think it works! – PhilippeUn Oct 24 '22 at 10:47
  • Glad I could help. Now, I'll tell you this: You should use `pinia` because it provides the same functionality as `vuex` (in fact, it's written by the same team and it's the officially recommended solution for state management, going forward), but, the code would look very close to my initial `reactive` example. Compare it with the `vuex` boilerplate. `pinia` makes store easier to use and a lot more readable. You don't have to do it in this project, but you should definitely consider using it in your next one. You'll love it. – tao Oct 24 '22 at 10:53
  • You're right, i messed up the mapping in Promise.all. Updated. – tao Oct 24 '22 at 10:56
  • I wanted to switch to Nuxt 3 after this project anyway and learn all the new features. Unfortunately I don't get it right now with `vuex`. Also I don't quite understand why we listen to 'event.agents' in watch or how it should work. Before the method is called, nothing changes in event.agents, right? There is still only the plain agent id stored. My plan is a little confusing... But I'll try it with `pinia` – PhilippeUn Oct 24 '22 at 11:45
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/249012/discussion-between-tao-and-philippeun). – tao Oct 24 '22 at 11:48
0

Ok, so I'm not hugely knowledgeable on object orientated programming or asynchronous JavaScript and don't completely understand the context, but here's my take regardless.

You may need to do a bit of editing to get this working in your project, but the structure of each of the modified methods should work.

const obj = {
  filteredEvents: [
    {
      agents: ['id', 'id'],
      eventData: '',
    },
  ],

  async fetchAgentData(agentID) {
    const agent = await axios
      .get(`/api/get-specific-agent/${agentID}`)
      .then(response => response.data.abilities)
      .catch(error => console.log(error));

    return agent;
  },

  async otherAgentsEvents() {
    for (const event of this.filteredEvents) {
      event.agents = await Promise.all(
        event.agents.map(async agent => this.fetchAgentData(agent))
      );
    }

    return this.filteredEvents;
  },
};

Calling the otherAgentsEvents() method should return the object you want.

Tested this with the pokeapi (linked here) and it seemed to work fine.

I wont waste your time trying to explain what's going on under the hood here, but it at least works, so I hope this helps.

damonholden
  • 1,062
  • 4
  • 17