Reputation: 3244
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.
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 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
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
DataLoaderWrapper can accept a promise
as data, so it can update when promise resolved.
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>
function getArtistsLoader(release) {
retrun axios.get('/artists?release=' + release.id)
.then((response) => response.data)
}
async
prop<DataLoaderWrapper async :dataLoader="getArtistsLoader(release)">
Upvotes: 1