Wait until firebase has finished loading data (vue) / 'await' not working - javascript

In my vue app, I am fetching some data from firebase in the created-hook.
I want the code afterwards to be exectuted after the data was finished with loading. But I can not make it work.
Here is my code:
<template>
<div>
<h1>Test 2</h1>
</div>
</template>
<script>
import { institutesCollection } from '../firebase';
export default {
name: 'Test2',
data() {
return {
settings: null,
}
},
async created() {
await institutesCollection.doc('FbdYJ5FzQ0QVcQEk1KOU').onSnapshot(
await function(doc) {
this.settings = doc.data();
console.log(this.settings);
}
)
console.log('Done with fetching Data');
console.log(this.settings)
},
methods: {
}
}
</script>
But one the console, first 'Done with fetching Data' is displayed, then null (because this.settings is still null) and only after that the Objekt settings is printed.
But I need this.settings to be defined there (and not null anymore) to work with there.
What I tried so far:
Putting in awaits
Adding a loaded parameter
Adding the code afterwards in a then()
Nothing worked.

The onSnapshot() method is not an asynchronous method. As explained in the doc, it "attaches a listener for DocumentSnapshot events".
So, you should use it if you want to set a continuous listener to the Firestore document: if something changes in the document (e.g. a field gets a new value) the listener will be triggered. Note that "an initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document" (see the doc).
You get the Firestore document data only in the callback function that you pass to onSnapshot(), as follows:
created() {
institutesCollection.doc('FbdYJ5FzQ0QVcQEk1KOU').onSnapshot(doc => {
this.settings = doc.data();
console.log(this.settings);
});
}
As Nilesh Patel mentioned, note that you need to use an arrow function in order to use this in this callback. Also note that the created function does NOT need to be async.
Finally, note that it is a good practice to call the unsubscribe function returned by onSnapshot() when you destroy the Vue.js component. Use the beforeDestroy hook as follows:
// ...
data() {
return {
settings: null,
firestoreListener: null
}
},
created() {
this.firestoreListener = institutesCollection.doc('FbdYJ5FzQ0QVcQEk1KOU').onSnapshot(doc => {
this.settings = doc.data();
console.log(this.settings);
});
},
beforeDestroy() {
if (this.firestoreListener) {
this.firestoreListener();
}
},
On the other hand, if you just want to read the document data ONLY ONCE in the created Vue.js hook, you should use the get() method, as follows:
async created() {
const doc = await institutesCollection.doc('FbdYJ5FzQ0QVcQEk1KOU').get();
if (doc.exists) {
this.settings = doc.data();
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
}
Note that, in this case, the created function NEEDS to be async, since get() is asynchronous.
More details on the difference between get() and onSnapshot() in this SO answer.

try using arrow function
async created() {
await institutesCollection.doc('FbdYJ5FzQ0QVcQEk1KOU').onSnapshot(
doc => {
this.settings = doc.data();
console.log(this.settings);
}
)
console.log('Done with fetching Data');
console.log(this.settings)
},

Related

fetch the data from multiple api - axios and get it in the page on load vue.js 3 [duplicate]

This question already has answers here:
How to access the correct `this` inside a callback
(13 answers)
Closed 1 year ago.
I want to fetch the data from multiple API with axios in Vue.js 3. The backend is Laravel API.
I am able to get data on response1.data and response2.data on console but how can I assign this data to the object defined on data() function of Vue.js.
I need this data to load on page on loaded. The Vue.js code is -
<script>
export default {
name:'Followup',
data() {
return {
patient:{},
labGroup:{},
}
},
created() {
this.$axios.get('/sanctum/csrf-cookie').then(response => {
this.$axios.all([
this.$axios.get(`/api/patients/showSomeDetails/${this.$route.params.id}`),
this.$axios.get('/api/labitems/getGroup')
])
.then(this.$axios.spread(function (response1, response2) {
console.log(response1.data) // this has given result on console.
this.patient = response1.data // It is giving error on console. => TypeError: Cannot set property 'patient' of null
this.labGroup = response2.data // same
console.log(response2.data)
}))
.catch(function (error) {
console.error(error);
});
})
},
computed() {
},
methods: {
}
}
</script>
I got error on console - this- app.js:20666 TypeError: Cannot set property 'patient' of null
How can I assign data to the patient and labGroup, so it will applied to my webpage via v-model
This is my patient followup up form, where I need to get the patient details from patient table, and labitems to fill lab reports from labitems table.
I hope, someone will give me a solution for this.
Accessing this in function (response1, response2) { wont be the vue instance.
Either use an arrow function or bind this to the function (response1, response2) { ... }.bind(this)
First Option:
You can use watch property of vue.js and before rendering patient property you need add v-if as below so that it won't be rendered on DOM until it has some truthy value.
template:
<div v-if="patient">{{patient}} </div>
script:
watch: {
// whenever patient changes, this function will run
patient: function (newValue, oldValue) {
this.patient= newValue;
}
}
Second Option:
Wrap your get calls inside getPatient method by awaiting it render patient once data is bound to it.
template:
<div v-if="patient">{{patient}} </div>
<script>
export default {
name: 'Followup',
data() {
return {
patient: null,
labGroup: null,
};
},
created() {
await this.getPatient();
},
computed() {},
methods: {
async getPatient() {
await this.$axios.get('/sanctum/csrf-cookie').then((response) => {
this.$axios
.all([this.$axios.get(`/api/patients/showSomeDetails/${this.$route.params.id}`), this.$axios.get('/api/labitems/getGroup')])
.then(
this.$axios.spread(function (response1, response2) {
console.log(response1.data); // this has given result on console.
this.patient = response1.data; // It is giving error on console. => TypeError: Cannot set property 'patient' of null
this.labGroup = response2.data; // same
console.log(response2.data);
})
)
.catch(function (error) {
console.error(error);
});
});
},
},
};
</script>

Vue3 reactivity lost when using async operation during object creation

I'm working with some objects (classes) in my TS codebase which perform async operations right after their creation. While everything is working perfectly fine with Vue 2.x (code sample), reactivity breaks with Vue3 (sample) without any errors.
The examples are written in JS for the sake of simplicity, but behave the same as my real project in TS.
import { reactive } from "vue";
class AsyncData {
static Create(promise) {
const instance = new AsyncData(promise, false);
instance.awaitPromise();
return instance;
}
constructor(promise, immediate = true) {
// working, but I'd like to avoid using this
// in plain TS/JS object
// this.state = reactive({
// result: null,
// loading: true,
// });
this.result = null;
this.loading = true;
this.promise = promise;
if (immediate) {
this.awaitPromise();
}
}
async awaitPromise() {
const result = await this.promise;
this.result = result;
this.loading = false;
// this.state.loading = false;
// this.state.result = result;
}
}
const loadStuff = async () => {
return new Promise((resolve) => {
setTimeout(() => resolve("stuff"), 2000);
});
};
export default {
name: "App",
data: () => ({
asyncData: null,
}),
created() {
// awaiting promise right in constructor --- not working
this.asyncData = new AsyncData(loadStuff());
// awaiting promise in factory function
// after instance creation -- not working
// this.asyncData = AsyncData.Create(loadStuff());
// calling await in component -- working
// this.asyncData = new AsyncData(loadStuff(), false);
// this.asyncData.awaitPromise();
},
methods: {
setAsyncDataResult() {
this.asyncData.loading = false;
this.asyncData.result = "Manual data";
},
},
};
<div id="app">
<h3>With async data</h3>
<button #click="setAsyncDataResult">Set result manually</button>
<div>
<template v-if="asyncData.loading">Loading...</template>
<template v-else>{{ asyncData.result }}</template>
</div>
</div>
The interesting part is, that the reactivity of the object seems to be completely lost if an async operation is called during its creation.
My samples include:
A simple class, performing an async operation in the constructor or in a factory function on creation.
A Vue app, which should display "Loading..." while the operation is pending, and the result of the operation once it's finished.
A button to set the loading flag to false, and the result to a static value manually
parts commented out to present the other approaches
Observations:
If the promise is awaited in the class itself (constructor or factory function), the reactivity of the instance breaks completely, even if you're setting the data manually (by using the button)
The call to awaitPromise happens in the Vue component everything is fine.
An alternative solution I'd like to avoid: If the state of the AsyncData (loading, result) is wrapped in reactive() everything works fine with all 3 approaches, but I'd prefer to avoid mixing Vue's reactivity into plain objects outside of the view layer of the app.
Please let me know your ideas/explanations, I'm really eager to find out what's going on :)
EDIT: I created another reproduction link, which the same issue, but with a minimal setup: here
I visited the code sample you posted and it it is working, I observed this:
You have a vue component that instantiates an object on its create hook.
The instantiated object has an internal state
You use that state in the vue component to render something.
it looks something like this:
<template>
<main>
<div v-if="myObject.internalState.loading"/>
loading
</div>
<div v-else>
not loading {{myObject.internalState.data}}
</div>
</main>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'App',
data(){
return {
myObject:null
}
},
created(){
this.myObject = new ObjectWithInternalState()
},
});
</script>
ObjectWithInternalState is doing an async operation when instantiated and changing its internalState but when internalState is a plain object then nothing is reactive. This is the expected behavior since changing any internal value of internalState is not a mutation on myObject (vue reactive value), but if instead of using a plain object for internalState yo use a reactive object (using the composition API) and since you are accessing that value on the template then all the changes made to that object are observed by the template (reactivity!!). If you don't want to have mixed things then you need to wait for the async operation in the component.
export default defineComponent({
name: 'App',
data(){
return {
remoteData:null,
loading:false
}
},
created(){
this.loading = true
// Option 1: Wait for the promise (could be also async/await
new ObjectWithInternalState().promise
.then((result)=>{
this.loading = false
this.remoteData = result
})
// Option 2: A callback
new ObjectWithInternalState(this.asyncFinished.bind(this))
},
methods:{
asyncFinished(result){
this.loading = false
this.remoteData = result
}
}
});
My recommendation is to move all state management to a store, take a look at Vuex It is the best practice for what are you intending
Szia Ábel,
I think the problem you're seeing might be due to the fact that Vue 3 handles the reactivity differently. In Vue2, the values sent were sort of decorated with additional functionality, whereas in Vue 3, reactivty is done with Proxy objects. As a result, if you do a this.asyncData = new AsyncData(loadStuff());, Vue 3 may replace your reactive object with the response of new AsyncData(loadStuff()) which may loose the reactivity.
You could try using a nested property like
data: () => ({
asyncData: {value : null},
}),
created() {
this.asyncData.value = new AsyncData(loadStuff());
}
This way you're not replacing the object. Although this seems more complicated, by using Proxies, Vue 3 can get better performance, but loses IE11 compatibility.
If you want to validate the 👆 hypothesis, you can use isReactive(this.asyncData) before and after you make the assignment. In some cases the assignment works without losing reactivity, I haven't checked with the new Class.
Here's an alternate solution that doesn't put reactive into your class
created() {
let instance = new AsyncData(loadStuff());
instance.promise.then((r)=>{
this.asyncData = {
instance: instance,
result: this.asyncData.result,
loading: this.asyncData.loading,
}
});
this.asyncData = instance;
// or better yet...
this.asyncData = {
result: instance.result,
loading: instance.loading
};
}
But it's not very elegant. It might be better to make the state an object you pass to the class, which should work for vue and non-vue scenarios.
Here's what that might look like
class withAsyncData {
static Create(state, promise) {
const instance = new withAsyncData(state, promise, false);
instance.awaitPromise();
return instance;
}
constructor(state, promise, immediate = true) {
this.state = state || {};
this.state.result = null;
this.state.loading = true;
this.promise = promise;
if (immediate) {
this.awaitPromise();
}
}
async awaitPromise() {
const result = await this.promise;
this.state.result = result;
this.state.loading = false;
}
}
const loadStuff = async () => {
return new Promise((resolve) => {
setTimeout(() => resolve("stuff"), 2000);
});
};
var app = Vue.createApp({
data: () => ({
asyncData: {},
}),
created() {
new withAsyncData(this.asyncData, loadStuff());
// withAsyncData.Create(this.asyncData, loadStuff());
// let instance = new withAsyncData(this.asyncData, loadStuff(), false);
// instance.awaitPromise();
},
methods: {
setAsyncDataResult() {
this.asyncData.loading = false;
this.asyncData.result = "Manual data";
},
},
});
app.mount("#app");
<script src="https://unpkg.com/vue#3.0.11/dist/vue.global.prod.js"></script>
<div id="app">
<div>
<h3>With async data</h3>
<button #click="setAsyncDataResult">Set result manually</button>
<div>
<template v-if="asyncData.loading">Loading...</template>
<template v-else>{{ asyncData.result }}</template>
</div>
</div>
</div>

Call API in another page in nuxtjs

I would like to put my calls to my API in a separate page and not in the template page of my app. So, I create a file "customersAPI.js" and I put this code :
export function findAllCustomers () {
axios.get('http://127.0.0.1:8000/api/customers')
.then((reponse)=>{
console.log(reponse.data['hydra:member'])
return reponse.data['hydra:member']
}).catch(err=>console.log(err))
}
So I try to retrieve my data in my template page and put these data in data but It does not work because of the asynchronous thing of api call and because I don't know how to pass the data...
I do this in my template page :
data() {
return {
customer: [],
}
},
mounted() {
this.getAllCustomers();
},
getAllCustomers() {
this.customer = findAllCustomers();
}
I know it is not the good way to do this but I don't know how to do... So I need clarification about that. And, every time I go into the documentation, there are no examples with an API call outside of the part where there is the page template. Is it a good practice to want to put the api call apart? And in general calls to functions so that the code is not too long?
Thanks for help
In your case I advise you to try add async in mounted or in func.
async mounted() {
this.customers = await this.findAllCustomers();
},
------
methods: {
async getAllCustomers(){
this.customer = await findAllCustomers();
}
}
But better practice to fetch information from store:
COMPONENT
<script>
import {mapActions} from 'vuex'
export default {
data() {
return {
customer: [],
}
},
mounted() {
this.customer = this.fetchAll();//better to get via getters
},
methods() {
...mapActions('customers', ['fetchAll']),
//OR
// fetchAllCustomers(){
// this.$store.dispath('customers/fetchAll')
// }
}
}
</script>
STORE
// async action that put all customers in store
const fetchAll = async ({ commit }) => {
commit(types.SET_ERROR, '')
commit(types.TOGGLE_LOADING, true)
try {
const { data} = await customerAPI.findAll(namespace)
commit(types.SET_ALLIDS, data['hydra:member'])
commit(types.TOGGLE_LOADING, false)
return data['hydra:member']
} catch (error) {
commit(types.TOGGLE_LOADING, false)
commit(types.SET_ERROR, error)
}
},
API
// func that receive promise
export function findAll () {
return axios.get('http://127.0.0.1:8000/api/customers')
}
Please read about vuex
https://vuex.vuejs.org/guide/actions.html

Vue updates data without waiting for state file to update it

When the page is being loaded for the first time, vue component is not waiting for my custom store file to process it. I thought it might fix it with promises but I am not sure on how to do so on functions that do not really require extra processing time.
I am not including the entire .vue file because I know it surely works just fine. My store includes couple of functions and it is worth mentioning it is not set up using vuex but works very similarly. Since I also tested what causes the issue, I am only adding the function that is related and used in MainComp.
Vue component
import store from "./store";
export default {
name: "MainComp",
data() {
return {
isLoading: true,
storageSetup: store.storage.setupStorage,
cards: Array,
};
},
created() {
this.storageSetup().then(() => {
this.cards= store.state.cards;
});
this.displayData();
},
methods: {
displayData() {
this.isLoading = false;
},
}
My custom store.js file
const STORAGE = chrome.storage.sync;
const state = {
cards: []
};
const storage = {
async setupStorage() {
await STORAGE.get(['cards'], function (data) {
if (Object.keys(data).length === 0) {
storage.addToStorage('ALL');
// else case is the one does not work as required
} else {
data.cards.forEach((elem) => {
// modifies the element locally and then appends it to state.cards
actions.addCard(elem);
});
}
});
}
};
export default {
state,
storage
};
Lastly, please ignore the case in setupStorage() when the length of data is equal to 0. If there is nothing in Chrome's local space, then a cards is added properly(state.cards is an empty array every time the page loads). The problem of displaying the data only occurs when there are existing elements in the browser's storage.
How can I prevent vue from assuming cards is not an empty array but instead wait until the the data gets fetched and loaded to state.cards (i.e cards in MainComp)?
Sorry if the problem can be easily solved but I just lost hope of doing it myself. If any more information needs to be provided, please let me know.
Your main issue is that chrome.storage.sync.get is an asynchronous method but it does not return a promise which makes waiting on it difficult.
Try something like the following
const storage = {
setupStorage() {
return new Promise(resolve => { // return a promise
STORAGE.get(["cards"], data => {
if (Object.keys(data).length === 0) {
this.addToStorage("All")
} else {
data.cards.forEach(elem => {
actions.addCard(elem)
})
}
resolve() // resolve the promise so consumers know it's done
})
})
}
}
and in your component...
export default {
name: "MainComp",
data: () => ({
isLoading: true,
cards: [], // initialise as an array, not the Array constructor
}),
async created() {
await store.storage.setupStorage() // wait for the "get" to complete
this.cards = store.state.cards
this.isLoading = false
},
// ...
}

Data fetched from API correctly; app gives TypeError with 'undefined' variable while processing with 'computed'

I ran into troubles trying to process data fetched from remote API.
The app is running VueJS with Vuetify, data is formatted with Vuetify's data table component.
This is my code:
export default {
data () {
return {
headers: [
{ text: 'City', value: 'city' },
{ text: '#Citizens', value: 'citizens' },
{ text: '#Schools', value: 'schools' },
{ text: 'Schools per Citizen', value: 'schoolsPerCitizen' },
(...)
API URL is defined as a variable on the root level of the app.
Then, there is this method launched when created() kicks in:
methods: {
loadData() {
axios.get(citiesApiUrl)
.then((response) => {
console.log(response.data) // data displayed correctly
return response.data
})
.catch(error => {console.error(error)})
}
},
created () {
this.loadData()
}
As you noticed in the comment, response.data does display desired values.
Problems start from this point:
computed: {
stats() {
return this.loadData().map(item => {
item.schoolsPerCitizen = (item.schools / item.citizens).toFixed(2)
return item
})
}
}
I get an error: TypeError: Cannot read property 'map' of undefined.
Any ideas what is wrong with my code?
Issues
When loadData is called in created, the axios promise is consumed but nothing happens with the returned data except it's logged and returned to the promise resolver.
When loadData is called in stats (computed), .map is chained off of the return value from loadData, but loadData has no return value.
Even if loadData returned the axios promise, that promise would have to be consumed in stats first before accessing the data (needs .then)
The design is flawed because the computed will make an identical API call every time it recalculates, which is likely unnecessary.
Also, the promise returned by stats wouldn't be resolved by the template render function anyway.
Fix
Create a variable for the loaded data (I'll call it mydata):
data() {
return {
// ...
mydata: []
}
}
Change loadData to:
loadData() {
axios.get(citiesApiUrl).then((response) => {
this.mydata = response.data // <--- Set the data to `mydata`
}).catch(error => {
console.error(error)
})
}
Change stats to:
stats() {
// This is also not designed properly, it's going to mutate `mydata`...
// You should study Vue and learn what the purpose for computeds are before using them
return this.mydata.map(item => { // <-- Once `mydata` is populated, this will recalculate
item.schoolsPerCitizen = (item.schools / item.citizens).toFixed(2)
return item
})
}
loadData does not return any value.
loadData() {
return axios.get(citiesApiUrl)
.then((response) => {
console.log(response.data) // data displayed correctly
return response.data
})
.catch(error => {console.error(error)})
}

Categories

Resources