Angular4 - how to ensure ngOnDestroy finishes before navigating away - javascript

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.

Related

How do I wait for users to click on certain buttons in component, then return a value from that in a function depending on which button was pressed?

I have an overlay component that appears when a user clicks on certain things in my page, and in this overlay it gives a warning and 2 buttons, one for yes and the other for no. What I want is to create a function that'll serve this component, and then it will wait for the user to respond, and subsequently return true or false based on what button was pressed. This boolean result can then be used to further progress to other code.
This is what I have tried already. It uses promises rather than rxjs observables.
A component will call this function to bring the overlay from the service, eg this.service.promptUser().then(res => if (res === true) { doSomething() }).
In the service:
didContinue: boolean = null;
async promptUser() {
this.showOverlay.next(true) //BehaviourSubject when true brings the popup
await waitForUser();
const decision = this.didContinue;
this.closeOverlay(); //sets didContinue back to null
return decision
}
The didContinue is a property inside of the service to indicate whether they have clicked yes or no using a boolean. Otherwise it will remain null. The click events from the overlay component will set the property didContinue to true or false.
The waitForUser function to wait for the user's input:
async waitForUser() {
while (this.didContinue === null) {setTimeout(() => {}, 50};
}
Currently it'll get stuck at the waitForUser() function but the popup will have not rendered at that stage, so the user can't input anything, the didContinue property will never change, and the application will freeze.
Please do send it forward if you know of an existing solution, I miss a lot of things with my google-foo. I am currently still new to Angular.
Create a service that will handle the result
inject service in your component where you want to use it like this
constructor(public popService: popService) { } // create functions accordingly in service
Call service method with the help of constructor on click() event like this
<button (click)="popService.success()">Done
<button (click)="popService.cancel()">Done
i hope this will help, let me know if you need further help :-)

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.

Prompt with callback when user tries to exit

I have a page which in it there is a Webrtc call between users.
And i want that when a user tries to go to another page/reload page/exit browser, there will be a prompt asking him if he's sure that he wants to leave the page, and if the user presses "yes", i want there to be a callback.
In the callback(not sure if relevant) i would close the peers and disconnect everything properly so that the call will get disconnected and the user won't hear audio anymore and the other side will know the user got disconnected also.
To do this on refresh i saw the window.onunloaded function
Is it even possible on browser exit?
I saw the Prompt option to do this on route change but i didn't see how to insert a callback on confirmation there.
I will be happy to get one working example as it feels to complicated for such a little thing which is weird..
Thanks.
Handling React's navigation events
I think what you're looking for is the getUserConfirmation prop of BrowserRouter component.
The function passed to the prop might look like (TypeScript example):
const getUserConfirmation = (
message: string,
callback: (ok: boolean) => void
): void => {
const answer = window.confirm("Do you want to navigate away?");
// do something depending on which button was pressed
callback(answer); // allow or disallow React's navigation
};
In the place where you define your routes:
<BrowserRouter getUserConfirmation={getUserConfirmation}>
... routes go here ...
</BrowserRouter>
You'll still need to use <Prompt>, though:
<Prompt
when={boolean}
message="This string will be supplied in the confirmation function above as second parameter"
/>
Handling browser refresh
You might want to add a callback like this
window.onbeforeunload = () => {
// some non-blocking logic
return true;
};
After you no longer need it, you can cleanup:
window.onbeforeunload = null;
Looking at this SO question, it doesn't seem to be possible out of the box to capture the user's response to the native confirm window in onbeforeunload.
Probably window.onunload would be the right place to clean up your stuff as it most probably means the user has opted to leave the page.

How to ensure I am reading the most recent version of state?

I may be missing something. I know setState is asynchronous in React, but I still seem to have this question.
Imagine following is a handler when user clicks any of the buttons on my app
1. ButtonHandler()
2. {
3. if(!this.state.flag)
4. {
5. alert("flag is false");
6. }
7. this.setState({flag:true});
8.
9. }
Now imagine user very quickly clicks first one button then second.
Imagine the first time the handler got called this.setState({flag:true}) was executed, but when second time the handler got called, the change to the state from the previous call has not been reflected yet -- and this.state.flag returned false.
Can such situation occur (even theoretically)? What are the ways to ensure I am reading most up to date state?
I know setState(function(prevState, props){..}) gives you access to previous state but what if I want to only read state like on line 3 and not set it?
As you rightly noted, for setting state based on previous state you want to use the function overload.
I know setState(function(prevState, props){..}) gives you access to previous state
So your example would look like this:
handleClick() {
this.setState(prevState => {
return {
flag: !prevState.flag
};
});
}
what if I want to only read state like on line 3 and not set it?
Let's get back to thinking why you want to do this.
If you want to perform a side effect (e.g. log to console or start an AJAX request) then the right place to do it is the componentDidUpdate lifecycle method. And it also gives you access to the previous state:
componentDidUpdate(prevState) {
if (!prevState.flag && this.state.flag) {
alert('flag has changed from false to true!');
}
if (prevState.flag && !this.state.flag) {
alert('flag has changed from true to false!');
}
}
This is the intended way to use React state. You let React manage the state and don't worry about when it gets set. If you want to set state based on previous state, pass a function to setState. If you want to perform side effects based on state changes, compare previous and current state in componentDidUpdate.
Of course, as a last resort, you can keep an instance variable independent of the state.
React's philosophy
The state and props should indicate things the components need for rendering. React's render being called whenever the state and props change.
Side Effects
In your case, you're causing a side effect based on user interaction which requires specific timing. In my opinion, once you step out of rendering - you probably want to reconsider state and props and stick to a regular instance property which is synchronous anyway.
Solving the real issue - Outside of React
Just change this.state.flag to this.flag everywhere, and update it with assignment rather than with setState. That way you
If you still have to use .state
You can get around this, uglily. I wrote code for this, but I'd rather not publish it here so people don't use it :)
First promisify.
Then use a utility for only caring about the last promise resolving in a function call. Here is an example library but the actual code is ~10LoC and simple anyway.
Now, a promisified setState with last called on it gives you the guarantee you're looking for.
Here is how using such code would look like:
explicitlyNotShown({x: 5}).then(() => {
// we are guaranteed that this call and any other setState calls are done here.
});
(Note: with MobX this isn't an issue since state updates are sync).

What is the purpose of having a didInvalidate property in the data structure of a react-redux app's state?

I'm learning from the react-redux docs on middleware and have trouble understanding the purpose of the didInvalidate property in the reddit example. It seems like the example goes through the middleware to let the store now the process of making an API call starting with INVALIDATE_SUBREDDIT then to REQUEST_POSTS then to RECEIVE_POSTS. Why is the INVALIDATE_SUBREDDIT necessary? Looking at the actions below, I can only guess that it prevents multiple fetches from happening in case the user clicks 'refresh' very rapidly. Is that the only purpose of this property?
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
}
You are close that didInvalidate is related to reducing server requests, however it is kind of the opposite of preventing fetches. It informs the app it should go and fetch new data; the current data did 'invalidate'.
Knowing a bit about the lifecycle will help explain further. Redux uses mapStateToProps to help to decide whether to redraw a Component when the global state changes.
When a Component is about to be redrawn, because the state (mapped to the props) changes for instance, componentDidMount is called. Typically if the state depends on remote data componentDidMount checks to see if the state contains a current representation of the remote data (e.g. via shouldFetchPosts).
You are correct that it is inefficient to keep making the remote call but it is shouldFetchPosts that guards against this. Once the required data has been fetched (!posts is false) or it is in the process of being fetched (isFetching is true) then the check shouldFetchPosts returns false.
Once there is a set of posts in the state then the app will never fetch another set from the server.
But what happens when the server side data changes? The app will typically provide a refresh button, which (as components should not change the state) issues an 'Action' (INVALIDATE_SUBREDDIT for example) which is reduced into setting a flag (posts.didInvalidate) in the state that indicates that the data is now invalid.
The change in state triggers the component redraw which, as mentioned, checks shouldFetchPosts which falls into the clause that executes return posts.didInvalidate which is now true, therefore firing the action to REQUEST_POSTS and fetching the current server side data.
So to reiterate: didInvalidate suggests a fetch of the current server side data is needed.
The most up-voted answer isn't entirely correct.
didInvalidate is used to tell the app whether the data is stale or not. If true, the data should be re-fetched from the server. If false, we will use the data we already have.
In the official examples, firing INVALIDATE_SUBREDDIT will set didInvalidate to true. This Redux action can be dispatched as a result of a user action (clicking a refresh button), or something else (a countdown, a server push etc.)
However, firing INVALIDATE_SUBREDDIT alone will not initiate a new request to the server. It is simply used to determine whether we should re-fetch the data or use the existing data when we call fetchPostsIfNeeded().
Because didInvalidate is set to true, the app will not let us fetch the data more than once. To refresh our data (e.g. after clicking a refresh button) we need to:
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
Because we called invalidateSubreddit(), didInvalidate is set to true and fetchPostsIfNeeded() will initiate a re-fetch.
(This is why danmux's answer isn't entirely correct. The life cycle method componentDidMount will not be called when the state (which is mapped to the props) changes; componentDidMount is only called when the component mounts for the first time. So, the effect of hitting the refresh button will not appear until the component has been remounted, e.g. from a route change.)

Categories

Resources