товіаѕ
товіаѕ

Reputation: 3244

vue.js - dynamically generated component in v-for does not update bound property correctly

I came upon a problem in vue.js and I'm not sure how I should solve it. I'll try to explain the gist of it as clear and short as possible.

Situation

I load data from a rest API. I created a class using the javascript prototype syntax to create a class called DataLoader which handles all the loading and internal state changes. More often than not, the loaded data is displayed in a repetitive way (e.g. list or cards). I created a new component called DataLoaderWrapper for this purpose, taking a DataLoader object as property. The DataLoaderWrapper displays loading spinners according to the loading state of the DataLoader object. In addition, the component markup includes the following tag:

<slot :dataLoader='dataLoader' />

.. to allow for easy access to the loaded data. The idea is to use the component like so:

<DataLoaderWrapper :dataLoader="myDataLoader">
  <template slot-scope="props">
    <template v-for="item in props.dataLoader.data">
       {{item}}
    </template>
  </template>
</DataLoaderWrapper>

Now just to clear this up right away; why do I use props.dataLoader instead of myDataLoader within the template? The basic idea is that the DataLoader object sometimes is generated dynamically and therefore the most direct access to the object instance is via the component bound property. (I also tried direct access to the loader and found no different behavior, but this way makes the component easier to use for me)


Now let's extend this basic concept.

Imagine you have a DataLoader object which is configured to load a list of music releases. This would look something like this:

<DataLoaderWrapper :dataLoader="myReleaseLoader">
...
</DataLoaderWrapper>

Furthermore, every release is published by one or more artists. The list of artists needs to be loaded separately. I can extend the example like this:

<DataLoaderWrapper :dataLoader="myReleaseLoader">
  <template slot-scope="props">
    <template v-for="release in props.dataLoader.data">
      <h1>release.Title</h1>
      Artists: 
      <DataLoaderWrapper :dataLoader="getArtistsLoader(release)">
        <template slot-scope="nestedProps">
          {{nestedProps.dataLoader.data.map(artist => artist.Name).join(', ')}}
        </template>
      </DataLoaderWrapper>
    </template>
  </template>
</DataLoaderWrapper>

The Problem

The releases are loaded correctly and the templates of myReleaseLoader are rendered correctly. As soon as this happens, a new DataLoader instance is created dynamically and ordered to load the artists for a given release. This happens for every release. While the artist loaders are loading, the loading spinner is displayed. The problem is, that the nestedProps variable does not get updated as soon as the DataLoader instance is done loading the data.

I've been trying to solve this issue for at least two days, meaning I've debugged and checked everything that came to my mind.

I am sure that the data is correctly loaded and existent. But the loading spinner of the DataLoaderWrapper component is still displayed and printing nestedProps.dataLoader reveals data: [] which does not line up with the state shown when inspecting the component. This brings me to the conclusion, that the nestedProps property is not updated correctly.

I think this could be because vue bindings are one-directional and an update in a child component will not trigger the parent to update.

Having the page loaded on my local machine with 'hot-reloading' on performs an update to the UI every time I save changes in my files without performing a full reload of the page. The loaders are in a different state when updating the UI now than they are when loading the page for the first time. The data does not get loaded again, because the getArtistsLoader(..) function implements a cache which guarantees that always the same loader is returned for a given release when requested, this means that the data is essentially already existent when re-rendering the page, resulting in the desired output being displayed. I wasn't able to get into the correct state otherwise.

I tried to work with computed properties, direct access to the loader and more direct access to the desired property (e.g. props.data instead of props.dataLoader.data), none of which solved the update issue.

How can I do this?

Upvotes: 4

Views: 1589

Answers (1)

Paul Tsai
Paul Tsai

Reputation: 1009

The problem is getArtistsLoader does not return data synchronously

const data = getArtistsLoader(release);
// data === undefined

so inside the DataLoaderWrapper, it gets undefined for the dataLoader props

Solution

DataLoaderWrapper can accept a promise as data, so it can update when promise resolved.

1. update DataLoaderWrapper to accept promise

I use an async prop to distinguish dataLoader is asynchronous or not

<template>
  <div>
    <slot :dataLoader="realData"/>
  </div>
</template>

<script>
export default {
  props: {
    dataLoader: null,
    async: Boolean
  },
  data() {
    let realData = this.dataLoader;
    if (this.async) {
      realData = [];
      this.dataLoader
        .then(data => {
          this.realData = data;
        })
        .catch(error => {
          console.log(error);
        });
    }
    return {
      realData: realData
    };
  }
};
</script>

2. make getArtistsLoader return promise

function getArtistsLoader(release) {
  retrun axios.get('/artists?release=' + release.id)
    .then((response) => response.data)
}

3. add async prop

<DataLoaderWrapper async :dataLoader="getArtistsLoader(release)">

full demo

Edit vue async prop

Upvotes: 1

Related Questions