i have a question regarding Javascript Promises in a VueJS setting, i have an application that uses an Action to either fetch a list of Countries from IndexedDB (if it's set ) or from an API by making an Axios HTTP Request.
Now, i'm returning a promise from the action because i want to be able to trigger some popups in the UI when this task is completed, and on top of that both Axios and Dexie(which im using for IndexedDB) run asyncronously through Promises themselves.
getCountries({commit, dispatch}) {
commit(types.MUTATIONS.SET_LOADING, true, {root: true})
commit(types.MUTATIONS.SET_LOADER_MESSAGE, "Loading Countries Data...", {root: true})
return new Promise((resolve, reject) => {
db.countries.count().then(value => {
if(value > 0) {
console.log("Loading Countries from IndexedDB")
db.countries.toArray().then(collection => {
commit(types.MUTATIONS.COUNTRIES.SET, collection, {root: true})
resolve(collection);
})
} else {
fetchCountriesData().then(data => {
console.log("Loading Countries from API Call")
commit(types.MUTATIONS.COUNTRIES.SET, data, {root: true})
db.countries.bulkAdd(data)
resolve(data)
}).catch(error => {
reject(error)
})
}
})
})
}
That is the code for the action, it simply does what i described above, the problem is that this results in an infinite loop where the LOADER Mutations get triggered over and over again.
Why exactly is this going on? Can anyone help me make sense of this? It seems that it runs the initial API action, but THEN after that, with the countries already loaded, it loops and runs again this time invoking the indexeddb mutation as well, which is strange, if i resolve it shouldn't it just end there?
EXTRA::
The action is invoked in a view that i have in my application, i invoke this in the created() hook so that i make sure that the list of countries is always loaded in my Vuex State.
created() {
this.getAllCountries().then(response => {}).catch(error => {
snackbar("Unable to load countries!", "error")
}).then(() => {
this.setLoadingStatus(false);
})
}
In this context, it does nothing if it's ok, but that might change in the future, on errors it should show a popup informing the users that the data could not be loaded, and in either case it should hide the loading bar (which is also handled through Vuex)
Could this be the cause of the problem?
Nevermind, i had a logic error in my code, basically in order to prevent anyone being able to click items while loading i set the view conditionally with a v-if="loading" so that if loading only show the Loader div and otherwise show the actual layout.
The problem with this approach is that it will re-trigger the created hook each time the main view is shown again, thus causing my silly loop.
Hope this helps someone in the future.
Related
how to subscribe to response when tap operator is used in the service.
Have anyone know how to resolve this?
edit(status) {
dataObj.val = status;
// call post service with status..
this.service
.update(dataObj)
.pipe(takeUntil(this._ngUnsubscribe$))
.subscribe(() => {
//i would like to wait until response come from backend and then navigate to the page so i get data over there.
if (res.status === 'Success') {
this.router
.navigate(['../../success'], {
relativeTo: this.route,
})
.then(() => {});
} else {
this.location.back();
}
});
}
//akita store service
update(
obj: any,
): Observable < any > {
return this.service.update(obj).pipe(
delay(800),
map((data: RestfulResponse < any > ) => data.data),
tap((data: anny) => {
this.store.update((state) => {
state.updateValue = data; // value is not updating and it is navigating to route
});
}),
);
}
//post service
update(obj){
//post call
}
Is there any way I can use tap and in service side and subscribe on component side?
I know I can use finalize but it is not helping for writing conditions inside.
The tap operator, by design, handles side effects which don't happen within the context of your observable pipeline. This means that your pipeline will never wait for results from the tap itself. I don't recommend using it in this manner. Under most circumstances, I only use tap for debugging.
If you are waiting for a particular state change, you should create a separate observable, selecting from your store, to watch the state for the expected change.
If you want to trigger an additional action when something happens, I recommend using ngrx Effects to achieve this.
Have a look at this post, where I talked about how to implement a similar use case:
https://stackoverflow.com/a/64491398/166850
You should also strive to set up reducers that apply your state changes, rather than updating the store directly.
Consider each of the following as a separate concern that you can implement independently of the others:
When user does an edit, trigger an edit action.
The reducer should update the state based on the edit action (for example, to show that a save is in progress)
When the edit action is triggered, trigger an effect. The app should make an HTTP call to save the change, then trigger a save finished action.
When the save is finished, the router navigation should be triggered.
This separates your code into multiple units which are easy to test and verify independently.
If #1 produces an action which is consumed by your reducer (#2), you can also create an ngrx Effect for #3 which listens for the same action, handles the HTTP call using switchMap, then triggers another action to signal that it's done.
Edit
Here's a simple example. The first time an action called APP_LOADED is triggered (from the AppComponent), this Effect makes an HTTP call to get data from the server, then triggers an action using the response data as the action payload.
The actual HTTP call is delegated to another service, the HttpMyConfigDataService, which simply calls HttpClient and returns an Observable.
#Injectable({
providedIn: 'root'
})
export class LoadMyConfigEffect {
constructor(
private httpMyConfigDataService: HttpMyConfigDataService,
private action$: Actions
) {
}
loadMyConfigData$ = createEffect(() => {
return this.action$.pipe(
filter((action) => action.type === 'APP_LOADED'),
take(1),
switchMap(() => this.httpMyConfigDataService.get().pipe(
map(data => {
return {type: 'MY_CONFIG_DATA_LOADED', payload: data};
}),
catchError(err => {
console.error('Error loading config data.', err);
return of({type: 'CONFIG_LOAD_ERROR', payload: err.message, isError: true);
})
))
);
});
}
I'm building a React + Redux app with a Node.js backend, and one of the features is that a user can view profiles of other users. To do this, I have a section in the Redux state called users that looks like:
{
...,
users: {
user: {},
isLoading: true
}
}
Every time the /users/:id route is rendered, a getUser(id) action is dispatched and fills the state with the received information.
The main issue is when a user views user1's profile page (therefore redux state is now filled with user1's profile) and then views user2's profile page, the dispatch getUser(2) action is called right after the page is rendered. Since user1's info is still in the state, it will flash their info for a very short time, and then show the loading spinner until the new user is loaded.
I read about dispatching a resetUser(id) action on every unmount of the page, but I'm not sure if this is the right way to go. Also, one of the features is if a user is viewing their own page, they have an edit button which redirects them to /edit-profile. If I reset the state on every unmount, I'll have to fetch their profile again (in the edit page), even though I just did that when they viewed their page.. And that doesn't seem like it makes sense.
Any ideas how to solve this? Thanks!
The render phase runs after mounting. And you stated that previous data is being shown before new data. It seems that you have asynchronous mounting:
async componentDidMount() {}
It will continue rendering even before mounting phase is completed. To avoid issue, you may use synchronous nature of mounting:
componentDidMount(){}
Where you'll call api data.
Now, when you reach to rendering phase it will have new data available before rendering and won't flash you old data.
You now may be wondering how to call api asynchronously. You can create a asynchronous function and call that function inside the synchronous componentDidMount.
componentDidMount() {
yourAsyncFunc()
}
async yourAsyncFunc() {} // api call
How can I do this with React hooks?
While using useEffect, don't implement async:
useEffect(async () =>
Implement it simply:
useEffect(() => {
// you can still use asynchronous function here
async function callApi() {}
callApi()
}, []) // [] to run in similar to componentDidMount
If you miss second parameter to useEffect then you are not ensuring it to run on mounting phase. It will run before, after, and in the rendering phase depending on case.
Implementing something like resetUser(id) seems to be the right way here. I use this approach in my current project and this does solve the problem. The other approach of removing async keyword from useEffect callback as mentioned in another answer didn't work for me (I use hooks, redux-toolkit, Typescript).
After dispatching this action, your state should look something like
{
...,
users: {
user: null,
isLoading: false,
}
}
If you are using hooks, you can dispatch the action this way:
useEffect(() => {
const ac = new AbortController();
...
return () => {
dispatch(resetUser(null));
ac.abort();
};
}, []);
Action could look something like this:
resetListingDetails(state, action) {
// Immer-like mutable update
state.users = {
...state.users,
user: null,
};
}
I'm hitting this route: http://site.dev/person/1
And my component looks something like this:
// PeopleComponent.vue
<template>
<div>
<template v-if="person == null">
<b>Error, person does not exist.</b>
</template>
<template v-else>
<pre> {{ person }} </pre>
</template>
</div>
</template>
<script>
export default {
mounted() {
this.$store.dispatch('getPeople');
}
computed: {
// represents the person whose id potentially matches the value of `/:id` in route.
person(){
let id = parseInt(this.$route.params.id)
let person = null
if(id > 0)
person = this.$store.state.people.find(p => p.id == id)
return person;
}
}
}
</script>
// [styles]
What I'm doing in this component:
I get an ID in my URL. The ID represents a specific resource being shown on this page. I have a person() computed property to retrieve the object whose ID matches the parameter in the URL from my Vuex store.
The desired outcome:
At the top of the page, I would like to display an error message if the object cannot be found (if say, for example, someone types an ID that doesn't exist in the array of objects in the store). If the item is found, a simple dump of the object is fine. This currently works, but the delay between fetching the data from the API and finding the right object in the store seems to be just long enough to briefly show the error message as if the object could not be found when the page first loads. When tested with a slower network speed, this error message is visible for several seconds. I want to eliminate it such that it does not appear at all if the object exists.
What I've tried:
v-cloak - tried applying this to every parent div, even the div#app itself, to no avail. I must be doing something wrong.
v-if - as seen above in the example
Some pointers would be appreciated, thank you!
Update 31/03/2020
As per Phil's suggestion, I tried to incorporate a flag to indicate when the page is ready. I did it in two different ways.
Method #1
Made the mounted() 'async' and added an await on the action that retrieves people from my API. Setting flag to true after it:
async mounted() {
await this.$store.dispatch('getPeople');
this.loaded = true;
}
But I still see the error message, briefly.
Method #2
Use a then callback on the action, and set the flag to true inside the callback
mounted() {
let vm = this
vm.$store.dispatch('getPeople').then(() => {
vm.loaded = true;
})
}
Both methods don't stop the message from appearing.
I suspect that this is what's happening:
Core rule
Error should ONLY show if loaded=true and person=null
Page loads, loaded is false and person is null. Error won't show.
Page does a call to get people from API. Error still isn't showing.
After the call, loaded has been set to true
[At this point, loaded is true and person hasn't been resolved by the computed property yet, THIS is where I believe I'm seeing the error.]
Computed property finds the relevant record from the store on the next tick.
Person is no longer null, so the error disappears.
Edit 01/04/2020
Answering Phil's question: What does your getPeople() action look like?
getPeople({ commit }) {
axios.get('/get/people')
.then(response => {
commit("getPeople", response.data);
})
.catch(error => {
console.log(error);
})
},
Sounds like you need one more piece of state, for example loading.
Change your getPeople action to be composable, ie return the Axios Promise so that it waits for the async task to complete...
getPeople ({ commit }) {
// make sure you return the promise
return axios.get('/get/people').then(response => {
commit("getPeople", response.data)
}).catch(error => {
console.error(error)
// also make sure you don't accidentally convert this to a successful response
return Promise.reject(error)
})
},
then try something like this
export default {
data: () => ({ loading: true }),
async created () {
// "created" fires earlier than "mounted"
await this.$store.dispatch('getPeople')
this.loading = false
},
computed: {
// etc
}
}
Now you can use loading in your template
<div v-if="loading">Loading...</div>
<div v-else>
<!-- your "person" template stuff goes here -->
</div>
Note
Although this was my initial solution - Phil's answer seems to explain the problem best, and give a more suitable solution. So I'm still using my loader, but instead of a configured timer, I'm now simply waiting for the Promise.
In the end I realised I'm asking the site to wait for an indefinite amount of time until information is retrieved. In that time, something has to happen. I had to ask myself "well, what do you want displayed there before the resource is retrieved?" as this can't happen instantaneously... so I added spinner in.
The problem is the "indefinite" part, though. I don't know how long to display the spinner for. so I configured a global timeout variable.
So
1) If timeout hasn't been reached and resource is empty, display loader
2) If resource has loaded, show resource
3) If resource is empty and timeout has been reached, display error message
I'm pleased with this as a solution.
I had the same problem I used Use Vue-progressbar here is the link
http://hilongjw.github.io/vue-progressbar/index.html
I have implemented in my project if you need help just let me know.
I'm sure this is a beginner question, but actually I can't figure out, why this is undefined.
But let's start from the beginning. I'm using react with react-redux to load data. In this special case I first want to load all positions of a project and then load the verfication files. That are 2 queries, so I want to be sure, that the positions are loaded completely before start loading the verifications. So I thought - oohh, let's use a promise here.
It starts with the method loadVerifications(event). In this method, the method loadProjectPositions(event) is being called. I can see that the project positions are loaded correctly.
When the positions are loaded it should load then the verification files (.then(()...).
Actually in this .then() method, this is undefined, but why?
class Kundenkommunikation extends Component {
constructor(p) {
super(p);
this.state = {
};
this.loadProjectPositions = this.loadProjectPositions.bind(this);
this.loadVerifications = this.loadVerifications.bind(this);
}
loadProjectPositions(event) {
return new Promise((resolve,reject) => {
this.props.getProjektPositionenByProjektIdForModulId(13, event.value);
resolve(this)
})
}
loadVerifications(event) {
this.loadProjectPositions(event)
.then(() => {
this.props.projektpositionen && Object.values(this.props.projektpositionen).map((position) => {
if(position.nachweis != null) {
this.props.createBericht(position.nachweis.id,'SherpaVerbinder');
}
});
})
}
UPDATE:
Even when I'm bind loadVerifications in the constructor, it doesn't work. this stays undefined.
You should use bind(this) method for loadVerifications to get a component's context from the external function or
Another is arrow functions loadVerifications= (event) => {...}
Ok, thanks for all hints and tipps. Lately it turned out, that Chrome did not show the data for whatever reason, but the data was available. I could find out by simply add an console.log(this.props) into the code, then it showed me all props and I was able to see that this is not undefined.
But the real issue was then, that the data was not loaded at this time. So I needed to add a then into the method loadProjectPositions to be sure, that the data is returned before resolving the Promise.
loadProjectPositions(event) {
return new Promise((resolve,reject) => {
this.props.getProjektPositionenByProjektIdForModulId(13, event.value)
.then(() => {
resolve(this)
});
})
}
After adding this, all data was loaded correctly at that time and then all worked.
I have a React Native App which on componentWillMount() calls a function to save everything in that list.
var list = []
const getMatchList = (logKey) => {
global.socket.emit("getMatches", logKey, (data) => {
//adding to list logic
})
console.log("Matches Loaded");
}
class MatchesScreen extends React.Component {
async componentWillMount() {
logKey = await getPreferences("logKey");
await getMatchList(logKey)
}
componentDidMount() {
console.log(list);
}
}
When I access that screen, it shows the empty list first, then I get the message "Matches Loaded". How can I first load matches?
Thank you.
whats wrong
I looks like you expect that await will turn your async function to a sync one, but this is not the case. Your componentWillMount will be called before rendering, but since it loads data asynchronously and since react will not wait for the promise to resolve, the page will be rendered with no data and will be rerendered later whenever data is available.
how to solve
this need more context to have a good answer, but you have at least following options:
Use the same logic, but add a loading spinner/progress bar for that view.
Switch to the view when data is loaded - but it will be perceived sluggish by users.
If possible preload and cache the data while in previous view.