Vue updates data without waiting for state file to update it - javascript

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
},
// ...
}

Related

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

Having Trouble Loading Earlier Messages in React-native GiftedChat Chat App with Firebase

I have been working on a chat app using Gifted-Chat and a Firebase RealTime database (and running it with Expo). At this point, the basic messaging works, but I am trying to enable to app to load earlier messages when the user scrolls up and hits the button that appears (I am aware of the GiftedChat prop for this). Unfortunately, I have been having trouble doing this and am a bit stumped.
There are two separate problems I have been running up against that I am aware of.
Clicking the loadEarlier button gives me an undefined is not a function (near '...this.setState...' runtime error (clearly, something is wrong with the skeleton function I put there).
The bigger issues is that I am still not clear on how to download the n number of messages before the oldest messages currently loaded. I have looked at the GiftedChat example and this post for help, but must confess that I am still lost (the best I can figure is that I need to sort the messages, possibly by timestamp, somehow get the right range, then parse them and prepend them to the messages array in state, but I cannot figure out how to do this, especially the later parts).
The relevant parts of the code for my chat screen are below, as is a screenshot of the structure of my firebase database. I would appreciate any help regarding both of these issues.
// Your run of the mill React-Native imports.
import React, { Component } from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import * as firebase from 'firebase';
// Our custom components.
import { Input } from '../components/Input';
import { Button } from '../components/Button';
import { BotButton } from '../components/BotButton';
// Array of potential bot responses. Might be a fancy schmancy Markov
// chain like thing in the future.
import {botResponses} from '../Constants.js';
// Gifted-chat import. The library takes care of fun stuff like
// rendering message bubbles and having a message composer.
import { GiftedChat } from 'react-native-gifted-chat';
// To keep keyboard from covering up text input.
import { KeyboardAvoidingView } from 'react-native';
// Because keyboard avoiding behavior is platform specific.
import {Platform} from 'react-native';
console.disableYellowBox = true;
class Chat extends Component {
state = {
messages: [],
isLoadingEarlier: false,
};
// Reference to where in Firebase DB messages will be stored.
get ref() {
return firebase.database().ref('messages');
}
onLoadEarlier() {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
});
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
}
// Get last 20 messages, any incoming messages, and send them to parse.
on = callback =>
this.ref
.limitToLast(20)
.on('child_added', snapshot => callback(this.parse(snapshot)));
parse = snapshot => {
// Return whatever is associated with snapshot.
const { timestamp: numberStamp, text, user } = snapshot.val();
const { key: _id } = snapshot;
// Convert timestamp to JS date object.
const timestamp = new Date(numberStamp);
// Create object for Gifted Chat. id is unique.
const message = {
_id,
timestamp,
text,
user,
};
return message;
};
// To unsubscribe from database
off() {
this.ref.off();
}
// Helper function to get user UID.
get uid() {
return (firebase.auth().currentUser || {}).uid;
}
// Get timestamp for saving messages.
get timestamp() {
return firebase.database.ServerValue.TIMESTAMP;
}
// Helper function that takes array of messages and prepares all of
// them to be sent.
send = messages => {
for (let i = 0; i < messages.length; i++) {
const { text, user } = messages[i];
const message = {
text,
user,
timestamp: this.timestamp,
};
this.append(message);
}
};
// Save message objects. Actually sends them to server.
append = message => this.ref.push(message);
// When we open the chat, start looking for messages.
componentDidMount() {
this.on(message =>
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, message),
}))
);
}
get user() {
// Return name and UID for GiftedChat to parse
return {
name: this.props.navigation.state.params.name,
_id: this.uid,
};
}
// Unsubscribe when we close the chat screen.
componentWillUnmount() {
this.off();
}
render() {
return (
<View>
<GiftedChat
loadEarlier={true}
onLoadEarlier={this.onLoadEarlier}
isLoadingEarlier={this.state.isLoadingEarlier}
messages={this.state.messages}
onSend={this.send}
user={this.user}
/>
</View>
);
}
}
export default Chat;
For your first issue, you should declare your onLoadEarlier with => function so as to get the current instance this i.e. your code should look like below:
onLoadEarlier = () => {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
}, () => {
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
});
}
Also, setState is asynchronous in nature, so you should rather depend on the second parameter of the setState i.e. the callback to ensure that the next lines of code execute synchronously.
Lastly, if you are using class syntax then you should declare the state in constructor like below:
class Chat extends Component {
constructor (props) {
super (props);
state = {
messages: [],
isLoadingEarlier: false,
};
}
......
onLoadEarlier = () => {
this.setState((previousState) => {
return {
isLoadingEarlier: true,
};
}, () => {
console.log(this.state.isLoadingEarlier)
this.setState((previousState) => {
return {
isLoadingEarlier: false,
};
});
});
}
...
}
For loading the last messages from firebase , I recommend using limitToLast function on your reference. You should afterwards order the results by date before calling append in gifted chat.
For the second question, it should be the same with this question How Firebase on and once differ?
You can using filter feature in Firebase for example using createdAt field to compare with last loaded message to load more.

Vuex Mutation running, but component not updating until manual commit in vue dev tools

I have a vue component that I can't get to update from a computed property that is populated from a service call.
Feed.vue
<template>
<div class="animated fadeIn">
<h1 v-if="!loading">Stats for {{ feed.name}}</h1>
<h2 v-if="loading">loading {{ feedID }}</h2>
</div>
</template>
<script>
export default {
data: () => {
return {
feedID: false
}
},
computed: {
feed(){
return this.$store.state.feed.currentFeed
},
loading(){
return this.$store.state.feed.status.loading;
}
},
created: function(){
this.feedID = this.$route.params.id;
var fid = this.$route.params.id;
const { dispatch } = this.$store;
dispatch('feed/getFeed', {fid});
}
}
</script>
That dispatches 'feed/getFeed' from the feed module...
feed.module.js
import { feedStatsService } from '../_services';
import { router } from '../_helpers';
export const feed = {
namespaced: true,
actions: {
getFeed({ dispatch, commit }, { fid }) {
commit('FeedRequest', {fid});
feedStatsService.getFeed(fid)
.then(
feed => {
commit('FeedSuccess', feed);
},
error => {
commit('FeedFailure', error);
dispatch('alert/error', error, { root: true });
}
)
}
},
mutations: {
FeedRequest(state, feed) {
state.status = {loading: true};
state.currentFeed = feed;
},
FeedSuccess(state, feed) {
state.currentFeed = feed;
state.status = {loading: false};
},
FeedFailure(state) {
state.status = {};
state.feed = null;
}
}
}
The feedStatsService.getFeed calls the service, which just runs a fetch and returns the results. Then commit('FeedSuccess', feed) gets called, which runs the mutation, which sets state.currentFeed=feed, and sets state.status.loading to false.
I can tell that it's stored, because the object shows up in the Vue dev tools. state.feed.currentFeed is the result from the service. But, my component doesn't change to reflect that. And there is a payload under mutations in the dev tool as well. When manually commit feed/feedSuccess in the dev tools, my component updates.
What am I missing here?
In the same way that component data properties need to be initialised, so too does your store's state. Vue cannot react to changes if it does not know about the initial data.
You appear to be missing something like...
state: {
status: { loading: true },
currentFeed: {}
}
Another option is to use Vue.set. See https://vuex.vuejs.org/guide/mutations.html#mutations-follow-vue-s-reactivity-rules...
Since a Vuex store's state is made reactive by Vue, when we mutate the state, Vue components observing the state will update automatically. This also means Vuex mutations are subject to the same reactivity caveats when working with plain Vue
Hey for all the people coming to this and not being able to find a solution. The following was what worked for me:
Declaring base state:
state: {
mainNavData: [],
}
Then I had my action which is calling the now fixed mutation:
actions : {
async fetchMainNavData({ commit }) {
var response = await axios.get();
commit('setMainNavData', response));
},
};
Now my mutation is calling this updateState() function which is key to it all
mutations = {
setMainNavData(state, navData) {
updateState(state, 'mainNavData', navData);
},
};
This is what the updateState function is doing which solved my issues.
const updateState = (state, key, value) => {
const newState = state;
newState[key] = value;
};
After adding updateState() my data reactively showed up in the frontend and I didn't have to manually commit the data in Vue tools anymore.
please note my store is in a different file, so its a little bit different.
Hope this helps others!
Sometimes updating property that are not directly in the state is the problem
{
directprop: "noProblem",
indirectParent: {
"test": 5 // this one has a problem but works if we clone the whole object indirectParent
}
}
but it is a temporary solution, it should help you to force update the state and discover what is the real problem.

Correct syntax for importing axios method in Vue js

I am trying to separate my axios calls from my main vue instance by importing them instead of calling them directly in the created hook.
I have this in a separate file called data.js
import axios from 'axios'
export default{
myData() {
return axios.get(`http://localhost:8080/data.json`)
.then(response => {
// JSON responses are automatically parsed.
return response.data;
})
.catch(e => {
return this.myErrors.push(e)
});
},
And in my vue instance I have the following:
import myDataApi from '#/api/data.js'
export default {
name: 'app',
components: {
myDataApi, // not sure if this is correct
},
data: function () {
return {
myInfo: '',
}
},
created() {
this.myInfo = myDataApi.myData();
console.log('this.myInfo= ', this.myInfo)
},
I am trying to populate myInfo with the json called by myData. This returns [object Promise] in Vue devtools and the as PromiseĀ {<pending>} in the console.
All the data I need is inside that PromiseĀ {<pending>} in an array called [[PromiseValue]]:Object so I know it is working, I just need to know the correct way implementing this.
I don't have a development environment enabled to test this at the moment, but I do notice that you are trying to assign a variable the moment that the component is initialized. This object is a promise, but you're not handling the promise after it is resolved inside the component where you have imported it.
I would recommend trying to handle the promise inside of the actual component, something like:
import myDataApi from '#/api/data.js'
export default {
name: 'app',
components: {
myDataApi, // not sure if this is correct
},
data: function () {
return {
myInfo: '',
}
},
created() {
myDataApi.myData()
.then((data) => {
this.myInfo = data
console.log('this.myInfo= ', this.myInfo);
});
.catch((e) => handleError) // however you want to handle it
},
Just to add to #LexJacobs answer. I omitted the parenthesis around data in .then() as seen below. Vue was squawking about data not being available even though it was. This solved that problem, although to be honest I don't know why.
myDataApi.myData()
.then(data => {
this.dataHasLoaded = true;
this.myInfo = data;
})
.catch(e => {
this.myErrors.push(e)
});

Categories

Resources