What happens to property values on an object in JavaScript when its property values change during asynchronous function calls? - javascript

I am wondering what happens to a javascript object that is passed to an asynchronous function (like a database save), and then immediately has its property's value changed.
The reason I am interested in this is that the asynchronous code takes a long time to finish and so waiting for it to callback would take longer than I would like to respond.
Take the following code:
function asyncDatabaseSave(user) {
// Save the user asynchronously in the database
// Do things that take a lot of time before calling back
}
app.get(function (req, res) {
// In real code user is an object that gets passed in.
user = {cart: [1, 2, 3]};
// What is the value of user.cart going to be when it reaches the databaseSave call?
// At the time of the function call, user.cart has options but it is set to null immediately after
asyncDatabaseSave(user);
user.cart = null;
res.json(user);
})
I think that the "correct" way to ensure that the user object was saved with the cart not null and then clear it would be to pass a callback into asyncDatabaseSave to set it to null after the save.
However I am interested in calling res.json as soon as possible. I have also experimented with this code:
app.get(function (req, res) {
var items = user.cart;
user.cart = null;
res.json(user);
asyncDatabaseSave(user, items);
})
This way items is stored as a separate var and passed into a slightly-modified database save function.
What happens to user.cart when asyncDatabaseSave is called and then it is set to null immediately after?
If I am trying to save user with cart, but also return the user with cart set to null what is the best way to do this?

What happens to user.cart when asyncDatabaseSave is called and then it
is set to null immediately after?
You'd have to know what actually happens in the async database call to know whether it's safe to modify the passed in object immediately after making the call or not. There are many cases where the operative parts of the user object would already be copied into native code and sent off to the database before asyncDatabaseSave() returns so further modifying the user object would not affect the async call at all.
But, there are situations where you can't assume that, particular if asyncDatabaseSave() is actually made up of several async calls and some Javascript runs between them (such as opening the db, then writing to the db. In those cases, modifying the user object where you are could affect things.
If I am trying to save user with cart, but also return the user with
cart set to null what is the best way to do this?
So, to be safe, don't modify the user object right after asyncDatabaseSave(user).
If you really just want to call res.json(user) as fast as possible, then you can just make a copy of the user object, modify that copy and then use each separate copy in your two async operations. That's the general answer that works in all cases and it's the safe way to do things.
If the whole cart object is not needed in asyncDatabaseSave(), then picking out just the parts that are needed and passing those (like your last suggestion), allows you to freely assign cart properties afterwards without any risk. You do have to be careful that you aren't reaching into the cart object and changing objects in the cart object because that might be changing the same objects that you passed to asyncDatabaseSave(), but you can certainly assign properties to the cart object and that will not affect asyncDatabaseSave() if you didn't pass it the cart object.
Here's a simple example to show how it's not safe to assume you can modify the object you passed in:
function someAsyncSave(u) {
setTimeout(function() {
log(u.name)
}, 50);
}
var user = {name: "Joe"};
someAsyncSave(user);
user.name = "Alice";
<script src="http://files.the-friend-family.com/log.js"></script>
Run the snippet. It will log "Alice", even though the property was "Joe" when someAsyncSave() was called.
Even though the user object had user.name = "Joe" when it was passed to someAsyncSave(), by the time someAsyncSave() actually uses the property, it has already been changed by the outer code.

Related

Why does this hook call return undefined when I simply change the variable name?

I'm running an API call via redux - in the tutorial, I am following, they use the variable name "sales" to store the data. Following along, I kept getting undefined, and after some troubleshooting, it appears that the only way for me to get any data out of this API call is to save the result in a variable named exactly "data".
// Correctly retrieves and logs the data
const { data, isFetching } = useGetSalesQuery();
console.log(data);
// Returns "undefined" every time
const { anythingElse, isFetching } = useGetSalesQuery();
console.log(anythingElse);
data is not defined anywhere else within this component. So what's going on here? Was Redux updated to force us to always use the name "data"? This is doing my head in.
useGetSalesQuery returns an object that has data and isFetching. Attempting to access an arbitrary field from that object will get you undefined. What's going on in this component is that you are defining a variable data and assign it the value from the field data that is returned from useGetSalesQuery
See javascript's destructuring assignment

Caching observables causing problem with mergeMap

I have a caching method in a container:
get(): Observable<T[]> {
if (!this.get$) {
this.get$ = merge(
this.behaviorSubject.asObservable(),
this._config.get().pipe(shareReplay(1), tap(x => this.behaviorSubject.next(x))));
}
return this.get$;
}
This works fine with normal observables, however when I cache the bellow in a myContainer2 (e.g using cached observable's result to create another cached observable) method like:
// get is assigned to _config.get in the above function
const myContainer2 = new Container({get: () => myContainer1.get().pipe(mergeMap(res1 => getObs2(res1))});
// please note, the end goal is to resolve the first observable on the first subscription
// and not when caching it in the above method (using cold observables)
myContainer2.get().subscribe(...) // getObs2 gets called
myContainer2.get().subscribe(...) // getObs2 gets called again
myContainer2.get().subscribe(...) // getObs2 gets called for a third time, and so on
every time when the second cache is subscribed to getObs2 gets called (it caches nothing).
I suspect my implementation of get is faulty, since I am merging an behavior subject (which emits at the beginning), but I cant think of any other way to implement it (in order to use cold observables).
Please note that if I use normal observable instead of myContainer.get() everything works as expected.
Do you know where the problem lies?
Using a declarative approach, you can handle caching as follows:
// Declare the Observable that retrieves the set of
// configuration data and shares it.
config$ = this._config.get().pipe(shareReplay(1));
When subscribed to config$, the above code will automatically go get the configuration if it's not already been retrieved or return the retrieved configuration.
I'm not clear on what the BehaviorSubject code is for in your example. If it was to hold the emitted config data, it's not necessary as the config$ will provide it.

Issue with update timing for a state in React Component (2 clicks needed instead of 1)

I’m working on a solo project using React and I’ve been stuck on something for the past 2 days…I'm starting and I'm very beginner so it's maybe something very basic, but I'm struggling...
To try to be concise and clear:
I have a searchBar component, that searches through a local database, and returns objects associated with the search keyword. Nothing complicated so far.
Each rendered object has a button that triggers a function onClick. The said function is defined in my App component and is as follow:
changeState(term){
let idToRender=[];
this.state.dealersDb.map(dealer=>{
if(term===dealer.id){
idToRender=[dealer];
}});
let recoToFind=idToRender[0].reco;
recoToFind.map(item=>{
Discogs.search(item).then(response=>{idToRender[0].recoInfo.push(response)})
})
this.setState({
objectToRender: idToRender
});
to explain the above code, what it does is that first, it identifies which object’s button has been clicked on, and send said object to a variable called idToRender. Then, it takes the reco state of that object, and store it to another variable called recoToFind. Then it calls the map() method on recoToFind, make an API request (the discogs() method) for each element of the recoToFind array and push() the results into the recoInfo state of idToRender. So by the end of the function, idToRender is supposed to look like this:
[{
…
…
recoInfo: [{1stAPI call result},{2ndAPI call result}…]
}],
The array contains 1 object having all the states of the object that was originally clicked on, plus a state recoInfo equal to an array made of the results of the several API calls.
Finally, it updates the component’s state objectToRender to idToRender.
And here my problem is, onClick, I do get all the states values of the clicked on object that get rendered on screen (as expected with how I coded the nested components), BUT, the values of the recoInfo are not displayed as expected (The component who’s supposed to render those values is nested in the component rendering the clicked on object other states values). However, they get displayed properly after a SECOND click on the button. So it seems my problem boils down to an state update timing trouble, but I’m puzzled, because this function is calling setState once and I know for a fact that the state is updated because when I click on the button, the clicked on Object details get displayed, but somehow only the recoInfo state seems to not be available yet, but only becomes available on a second click…
Would anyone have a way to solve this issue? :(
It somehow feels like my salvation lies in async/await, but I’m not sure I understand them correctly…
thanks very much in advance for any help!
Is this someting you want to do?
changeState(term) {
let idToRender=[];
this.state.dealersDb.map(dealer=>{
if(term===dealer.id){
idToRender=[dealer];
}});
let recoToFind=idToRender[0].reco;
recoToFind.map(item=>{
Discogs.search(item).then(response=>{
idToRender[0].recoInfo.push(response)
this.setState({
objectToRender: idToRender
});
})
})
}
you can call setState once async call is done and result received.

Angular4 - how to ensure ngOnDestroy finishes before navigating away

I have a list of objects. The user can click on one, which then loads a child component to edit that component.
The problem I have is that when the user goes back to the list component, the child component has to do some cleanup in the ngOnDestroy method - which requires making a call to the server to do a final 'patch' of the object. Sometimes this processing can be a bit slow.
Of course what happens is the user arrives back on the list, and that api call completes before the database transaction from the ngOnDestroy completes, and thus the user sees stale data.
ngOnDestroy(){
this.destroy$.next();
this.template.template_items.forEach((item, index) => {
// mark uncompleted items for deletion
if (!item.is_completed) {
this.template.template_items[index]['_destroy'] = true;
};
});
// NOTE
// We don't care about result, this is a 'silent' save to remove empty items,
// but also to ensure the final sorted order is saved to the server
this._templateService.patchTemplate(this.template).subscribe();
this._templateService.selectedTemplate = null;
}
I understand that doing synchronous calls is not recommended as it blocks the UI/whole browser, which is not great.
I am sure there are multiple ways to solve this but really don't know which is the best (especially since Angular does not support sync requests so I would have to fall back to standard ajax to do that).
One idea I did think of was that the ngOnDestroy could pass a 'marker' to the API, and it could then mark that object as 'processing'. When the list component does its call, it could inspect each object to see if it has that marker and show a 'refresh stale data' button for any object in that state (which 99% of the time would only be a single item anyway, the most recent one the user edited). Seems a bit of a crap workaround and requires a ton of extra code compared to just changing an async call to a sync call.
Others must have encountered similar issues, but I cannot seem to find any clear examples except this sync one.
EDIT
Note that this child component already has a CanDeactive guard on it. It asks the user to confirm (ie. discard changes). So if they click to confirm, then this cleanup code in ngOnDestroy is executed. But note this is not a typical angular form where the user is really 'discarding' changes. Essentially before leaving this page the server has to do some processing on the final set of data. So ideally I don't want the user to leave until ngOnDestroy has finished - how can I force it to wait until that api call is done?
My CanDeactive guard is implemented almost the same as in the official docs for the Hero app, hooking into a general purpose dialog service that prompts the user whether they wish to stay on the page or proceed away. Here it is:
canDeactivate(): Observable<boolean> | boolean {
console.log('deactivating');
if (this.template.template_items.filter((obj) => { return !obj.is_completed}).length < 2)
return true;
// Otherwise ask the user with the dialog service and return its
// observable which resolves to true or false when the user decides
return this._dialogService.confirm('You have some empty items. Is it OK if I delete them?');
}
The docs do not make it clear for my situation though - even if I move my cleanup code from ngOnDestroy to a "YES" method handler to the dialog, it STILL has to call the api, so the YES handler would still complete before the API did and I'm back with the same problem.
UPDATE
After reading all the comments I am guessing the solution is something like this. Change the guard from:
return this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?');
to
return this._dialogService.confirm('You have some empty items.
Is it OK if I delete them?').subscribe(result => {
...if yes then call my api and return true...
...if no return false...
});
As you said, there are many ways and they depend on other details how your whole app, data-flow and ux-flow is setup but it feels like you might want to take a look at CanDeactivate guard method which ensures user cannot leave route until your Observable<boolean>|Promise<boolean> are resolved to true.
So, its a way for async waiting until your service confirms things are changed on server.
[UPDATE]
it depends on your user confirmation implementation but something along these lines...
waitForServiceToConfirmWhatever(): Observable<boolean> {
return yourService.call(); //this should return Observable<boolean> with true emitted when your server work is done
}
canDeactivate(): Observable<boolean> {
if(confirm('do you want to leave?') == true)
return this.waitForServiceToConfirmWhatever();
else
Observable.of(false)
}
One "workaround" I can think of is to have your list based in client. You have the list as a JS array or object and show the UI based on that. After editing in the details screen, have a stale flag on the item which the service called on ngOnDestroy clears while updating the other related data.

Wait for Observable to complete in order to submit a form

I have a 'new trip' form, where the user can write the names of the participants and then submit the form to create the trip.
On submit, I query a Firebase database with the names, in order to get the IDs of the participants (/users). I then add the IDs to the participantsID field of the trip object and then I push the new trip to Firebase.
The problem is that the Firebase query is async and returns an Observable, therefore my function will proceed to push the object before the Observable has completed, so the participantsID field of the new object is empty.
Is there any method to wait for the observable to complete (in a kind of synchronous way) so that i can manipulate the data and then proceed? All my attempts to fix this have failed so far.
Here's my simple code.
getUserByAttribute(attribute, value) {
return this.db.list('/users', {
query: {
orderByChild: attribute,
equalTo: value,
limitToFirst: 1
}
});
}
createTrip(trip) {
for(let name in participantsName.split(',')) {
getUserByAttribute('username', name)
.subscribe( user => trip.participantsID.push(user[0].$key) );
}
this.db.list('/trips').push(trip);
}
You could treat all Observables into a single Observable by doing forkJoin
createTrip(trip) {
var observableArray: any = participantsName.split(',')
.switchMap((name)=> getUserByAttribute('username', name))
Observable.forkJoin(observableArray).subscribe(
trips => trips.forEach((trip) => {
this.db.list('/trips').push(trip);
})
);
}
In the end I used part of #Pankaj Parkar's answer to solve the problem.
I forkJoin all the Observables returned by mapping the splitted names and I subscribe to that Observable which result contains an array of arrays, where the inner arrays contain a user object.
getUserByAttribute(attribute, value) {
return this.db.list('/users', {
query: {
orderByChild: attribute,
equalTo: value,
limitToFirst: 1
}
}).first();
}
createTrip(trip) {
Observable.forkJoin(
trip.participantsName.split(',')
.map(name => getUserByAttribute('name', name))
).subscribe(
participants => {
trip.participants = participants.map( p => p[0].$key);
this.tripService.createTrip(trip);
}
);
}
}
You have a difficult problem. You have to get users info before push a new trip.
You can't just make new subscriptions every time because of the memory leak problem (or be careful with unsubscribes). If you are using Firebase, you can use AngularFire subject support.
You can update a subscription by using a subject in your query (with the equal to) and then push a user to retrieve with .next(user).
Then you still have to wait for all users. For that, you can have only one subscription and get all IDs synchronously or have multiple subscriptions to get multiple results faster (but it's difficult).
To solve this problem, I created:
a queue of callbacks (just arrays but use push() and unshift() methods)
a queue of values
one subject for one subscription.
If you want an ID, you have to:
push the value
push the callback that will retrieve the value returned.
You should use functions to push because you'll have to call .next() if there is no value in the stack (to start !).
And in your subscription, in its callback, i.e when you receive the distant user object, you can call the first callback in the stack. Don't forget to pop your value and callback of the stacks and call the next() for the next value if there is one.
This way, you can push your trip in the last callback for the last user. And it's all callbacks, it means your app is not interrupted.
I still not decided if we should do that in a cloud function. Because the user have to stay connected, and this use his data / processor. But it's good to have all the code in the same place, and cloud functions are limited for a free version of Firebase. What would a Firebase developer advice?
I made a lot of searches to find a better solution, so please share it if you have one. It's a little complicated I think, but it's working very fine. I had the same problem when a user want to add a new flight, I need to get the airports information before (coords) and push multiple objects (details, maps, etc.)

Categories

Resources