I have a couple of questions about Angular. I recently started to experiment with Angular and really cant get a grip of when I should unsubscribe, there is obviously recommended to use the AsyncPipe but in some cases there is no way to use it.
If I subscribe to a HTTP request in a service, does the Observable unsubscribe itself or is it persistent through out the application lifetime?
When I subscribe (without the AsyncPipe) to a HTTP request in a component I could manually unsubscribe in the ngOnDestroy lifecycle hook which is fine but in my case I have a submit method to create account
account.component.html
<account-item>
*ngFor="let account of collection$"
[data]="account"
</account-item>
account.component.ts
public collection$: Observable<Account[]>;
private subscription: Subscription;
constructor(
private service: AccountService
) {}
ngOnInit() {
this.collection$ = this.service.getAll()
}
createAccount(obj) {
this.subscription = this.service.create(obj)
.subscribe(
success => this.collection$ = this.service.getAll(),
error => Observable.throw(error)
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
From what I know the subscription is now persistent until my AccountComponent is destroyed, but is there a way to use AsyncPipe here instead or is it better for me to subscribe in the service itself?
I've read something about finite and infinite Observables but haven't really understand when a Observable is finite or not.
Another problem I'm facing is that in success => this.collection$ = this.service.getAll() my UI doesn't update with the new account list when I use ChangeDetectionStrategy.OnPush but works just fine with ChangeDetectionStrategy.Default
This is the service method that fetches the account data
getAll() {
return this.http.get(ENDPOINT_ACCOUNT)
.map((response: Response) => response.json().data)
}
What you need to think when dealing with Observables and functionnal programming more globaly is that you don't describe how things are done but describe what things are.
In your example, you collection is the combination of on the one hand the initial fetch from the service and on the other hand, all updates that may occur, so if you want to avoid subscribing in the component, you can do such a thing:
class Foo {
public collection$: Observable < Account[] > ;
private createAccount$ = new Subject<Account>();
constructor(
private service: AccountService
) {}
ngOnInit() {
let initialAccounts = this.service.getAll().share();
let accountUpdate = initialAccounts.switchMap(()=>this.createAccount$.switchMap(account=>{
return this.service.create(account).switchMap(()=>this.service.getAll())
}))
this.collection$ = Observable.merge(initialAccounts,accountUpdate);
}
createAccount(obj:Account) {
this.createAccount$.next(obj);
}
}
We are using here the merge operator to take the data from either initialAccounts or createAccount$. It is always a good thing to combine your observables together and subscribe once, because this way you don't have to imperatively manage your subscription.
The fact is most of the time, you don't need to subscribe() at all.
Related
I have this init function that is called in componentDidMount or other functions that are called when interacting with the user:
protected async init() {
if (this.props.requiredSource)
) {
this.setState({ imageSource: this.props.requiredSource }); // task I want to cancel in componentWillUnmount;
return;
}
// ... more code here ....
const exists = await RNFS.exists(uri);
if (exists) {
this.cropImageForDisplayView(uri); // async that needs canceling in componentWillUnmount
this.iAmUploading(); // async that needs canceling
return;
}
this.setState({ imageSource: { uri: httpUri } }); // same here
return;
}
try {
// ...more code here...
this.setState({ imageSource: { uri: `${httpUri.split('?')[0]}?w=${size.w}&h=${size.h}&min=1` } }); // same here
} catch (e) {
this.setState({ imageSource: { uri: httpUri } }); // same here
}
}
private iAmUploading = async () => {
if (this.props.tempFileId != null) {
this.setState({ tempfileId: this.props.tempFileId }); // same here
}
};
The above code needs some refactoring because it generates some classical update component that doesn't exist errors.
I would need an answer about best practices how to use abort controller or cancelable promises to cancel the multiple setStates calls and the async tasks when the component doesn't exist anymore.
I've looked into:
https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
but that article is 5 years old it is not very clear to me if that is the best practice in these days and how I could adapt that to cancelling multiple async tasks and setState calls.
Any help that does not include the classic anti-pattern this.isMounted will be greatly appreciated.
Note: current app is written with classic components (without hooks) and a refactoring towards hooks is not possible at this moment.
"react": "16.13.1",
"react-dom": "16.11.0",
"react-native": "0.63.3",
Standard approach described in multiple react tutorials (including the one linked in the official react docs) is to set a flag when component is unmounted, and check this flag after each async step of your code. This is probably ok for a single async call - but becomes quite verbose if you have multiple async calls that are chained one after another.
A bit cleaner solution for those more complex cases could be achieved with a library that provides cancellation out of the box. Abovementioned cancelable-promises or bluebird provide promise-compatible implementations that allow cancellation.
A different approach would be switching to using Observable-based api with rxjs instead of
promise-based. Observables are cancelable by design. A drawback though is that you won't be able to use async/await with them.
You can use a library like cancelable-promise to make your promises cancelable. Like this:
import { cancelable } from 'cancelable-promise';
protected async init() {
// ... code here ....
const exists = await RNFS.exists(uri);
if (exists) {
this.cancelableCropImageForDisplayView = cancelable(this.cropImageForDisplayView(uri));
this.cancelableIAmUploading = cancelable(this.iAmUploading());
return;
}
// ... code here ....
}
Then in componentWillUnmount you just need to cancel your promises.
componentWillUnmount() {
this.cancelableCropImageForDisplayView.cancel();
this.cancelableIAmUploading.cancel();
}
Bear in mind that your promises will still be executed but the result will be discarded. If you want to really cancel the execution as soon as componentWillUnmount runs you need to make sure that your async calls support some kind of interrupt signal (something similar to Axios cancellation).
If you're using axios, you need to generate a cancel token for each request to make it cancellable.
Ref: https://codesandbox.io/s/cancel-requests-with-axios-reactjs-forked-6v9rf
I do not recommend the following approach yet, because the CPromise library is currently in beta stage, so it's just for awareness.
When you use CPromise instead of native to build promise chains, all asynchronous tasks, including nested ones, become completely cancelable/abortable- when the cancel method is called, they will be rejected with the special CanceledError reason. The internal async jobs built on callbacks will be terminated as well if the user code supports the cancel event processing. If you use ECMA asynchronous functions, you need to subscribe the promise chain to the external AbortController signal.
Decorators usage demo
import { async, listen, cancel, timeout } from "c-promise2";
import cpFetch from "cp-fetch";
export class TestComponent extends React.Component {
state = {
text: ""
};
#timeout(5000)
#listen
#async
*componentDidMount() {
console.log("mounted");
const response = yield cpFetch(this.props.url);
// more tasks
this.setState({ text: `json: ${yield response.text()}` });
}
render() {
return <div>{this.state.text}</div>;
}
#cancel()
componentWillUnmount() {
console.log("unmounted");
}
}
Here an example how to write cancelable code (live demo):
I'm building an ordering app, where I do the backend with Laravel and the front end with both ReactJS and React Native
I want real-time updates whenever a customer posts an order and whenever an order gets updated.
Currently, I managed to get WebSocket running that uses the pusher API using devmarketer his tutorial.
I'm successful in echoing the order in the console, but now I want to access the channel using my react app
And at this step is where I am facing difficulties.
As I'm unsure how to create a route that is accessible to both my apps and how to access the channel through this route.
The official laravel documentation gives an example of how to access pusher but not how to connect to it with for example an outside connection (example: my react native app)
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'rapio1',
host: 'http://backend.rapio',
authEndpoint: 'http://backend.rapio/broadcasting/auth',
auth: {
headers: {
// Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
}
// encrypted: true
});
window.Echo.channel('rapio.1').listen('OrderPushed', (e) =>{
console.log(e.order)
})
So my question is how can I access a broadcasting channel on my React apps?
ADDED BACKEND EVENT
class OrderPushed implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $neworder;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct(Order $neworder)
{
$this->neworder = $neworder;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
//return new Channel('Rapio.'.$this->neworder->id);
return new Channel('Rapio');
}
public function broadcastWith()
{
return [
'status' => $this->neworder->status,
'user' => $this->neworder->user->id,
];
}
}
Are you using the broadcastAs() method on the backend?
It's important to know this in order to answer your question properly because if you are, the Laravel echo client assumes that the namespace is App\OrderPushed.
When using broadcastAs() you need to prefix it with a dot, to tell echo not to use the namespacing so in your example, it would be:
.listen('.OrderPushed')
Also, you don't need to do any additional setup on the backend in order for each client application to connect to the socket server unless you want to have a multi-tenancy setup whereby different backend applications will make use of the WebSockets server.
I also use wsHost and wsPort instead of just host and port, not sure if that makes a difference though
If you can access the data on the frontend by simply console.log'ing to the console you should already be most of the way there.
The way you would actually get the data into your react components depends on if you're using a state management library (such as redux) or just pure react.
Basically, you would maintain a local copy of the data on the frontend and then use the Echo events to update that data. For example, you could have a list of orders in either redux, one of your react components, or somewhere else, that you could append to and modify based on creation, update, and deletion events.
I would personally create an OrderCreated, OrderUpdated, and OrderDeleted event on the backend that would contain the given order model.
class OrdersList extends React.Component {
componentDidMount() {
this.fetchInitialDataUsingHttp();
//Set up listeners when the component is being mounted
window.Echo.channel('rapio.1').listen('OrderCreated', (e) =>{
this.addNewOrder(e.order);
}).listen('OrderUpdated', (e) =>{
this.updateOrder(e.order);
}).listen('OrderDeleted', (e) =>{
this.removeOrder(e.order);
});
}
componentWillUnmount() {
//#TODO: Disconnect echo
}
}
I'm trying to create a caching function in angular using RxJS Observable. Originally I've created this method using angularjs $q's deferred promise. Observables and RxJS are new to me and I find this method of working still somewhat confusing.
This is my current implementation of a getOrCreate caching function. Retrieve a single value for a key from storage (this.get()) and if it's not in there you retrieve it elsewhere (fetcher).
Assume fetcher is a slower data source than this.get(). Multiple requests for the same key could fire while we're still retrieving from this.get() so I put in an aggregator: only a single observable is created for multiple requests of the same key.
protected observableCache : {[key: string] : Observable<any>} = {};
get<T>(key : string): Observable<T> { /* Async data retrieval */ }
getOrCreate<T>(key : string, fetcher: () => Observable<T>) : Observable<T> {
const keyHash = this.hash(key);
// Check if an observable for the same key is already in flight
if (this.observableCache[keyHash]) {
return this.observableCache[keyHash];
} else {
let observable : Observable<T>;
this.get(key).subscribe(
// Cache hit
(result) => { observable = Observable.of(result); },
// Cache miss. Retrieving from fetching while creating entry
() => {
fetcher().subscribe((fetchedResult) => {
if(fetchedResult) {
this.put(key, fetchedResult);
}
observable = Observable.of(fetchedResult);
});
}
);
// Register and unregister in-flight observables
this.observableCache[keyHash] = observable;
observable.subscribe(()=> {
delete this.observableCache[this.hash(key)];
});
return observable;
}
}
This is my current version of that code but it doesn't look like I'm properly handling async code:
Observable will be returned before it's instantiated: return observable fires before observable = Observable.of(result);
There's probably a much better pattern of aggregating all requests for the same key while this.get() is still in-flight.
Can someone help with finding the Observer patterns should be used?
I think this might work. Rewritten as:
getOrCreate<T>(key : string, fetcher: () => Observable<T>) : Observable<T> {
const keyHash = this.hash(key);
// Check if an observable for the same key is already in flight
if (this.observableCache[keyHash]) {
return this.observableCache[keyHash];
}
let observable : ConnectableObservable<T> = this.get(key)
.catch(() => { // Catch is for when the source observable throws error: It replaces it with the new Rx.Observable that is returned by it's method
// Cache miss. Retrieving from fetching while creating entry
return this.fetchFromFetcher(key, fetcher);
})
.publish();
// Register and unregister in-flight observables
this.observableCache[keyHash] = observable;
observable.subscribe(()=> {
delete this.observableCache[keyHash];
});
observable.connect();
return observable;
},
fetchFromFetcher(key : string, fetcher: () => Observable<T>) : Observable<T> {
// Here we create a stream that subscribes to fetcher to use `this.put(...)`, returning the original value when done
return Rx.Observable.create(observer => {
fetcher().subscribe(fetchedResult => {
this.put(key, fetchedResult);
observer.next(fetchedResult);
},
err => observer.error(err),
() => observer.complete())
});
}
Explanations:
Observables are very different from promises. They are to work with async stuff, and there are some similarities, but they are quite different
As this.get(...) seems asynchronous, your let observable won't get filled until it yields a value, so when you assign it to your cache it's normal that's null.
A great thing about observables (and the main difference with promises) is that you can define a stream before anything gets executed. In my solution, nothing gets called until I call observable.connect(). This avoids so many .subscriptions
So, in my code I get the this.get(key) stream, and tell it that if it fails (.catch(...)) it must fetch the result, but once that's fetched then put it into your cache (this.put(key, fetchedResult)
Then I publish() this observable: This makes it so it behaves more like promises do, it makes it "hot". This means that all subscribers will get the values from the same stream, instead of creating a new stream that starts from 0 everytime one subscribes to it.
Then I store it in the observable pool, and set to delete it when it finishes.
Finally, I .connect(). This is only done if you publish() it, it's the thing that actually subscribes to the original stream, executing everything you want.
To make it clear, because this is a common error coming from Promises, in angular if you define a stream as:
let myRequest = this.http.get("http://www.example.com/")
.map((result) => result.json());
The request it's not sent yet. And everytime you do myRequest.subscribe(), a new request to the server is made, it won't reuse the "first subscription" result. That's why .publish() is very useful: It makes that when you call .connect() it creates a subscription that triggers the request, and will share the last result received (Observables support streams: Many results) with all incoming subscriptions to the published observable.
Basically what i try to do is to hit my API once and save the result inside global variable in my Service, and then share and modify this value in my parent and child component with two helpers functions.
repairs.service.ts
public myItems:any[];
public GetRepairs = ():Observable<any> => {
this.headers = new Headers();
this.headers.set('Authorization', 'Bearer' + ' ' + JSON.parse(window.localStorage.getItem('token')));
return this._http.get(this.actionUrl +'repairs'{headers:this.headers})
.map((res) => {return res.json();
}).map((item) => {
let result:Array<any> = [];
if (item.items) {
item.items.forEach((item) => {
result.push(item);
});
}
this.myItems = result;
return this.myItems;
});
};
public GetItems() {
return this.myItems;
};
public UpdateItems(data:any[]) {
this.myItems = data;
};
And then in my main component i do
repairs.component.ts
export class RepairsComponent implements OnInit {
public myItems:any[];
constructor(private _userService:UserService,
private _RepairsService:RepairsService,
public _GlobalService:GlobalService) {
}
ngOnInit() {
this._userService.userAuthenticate();
this.getAllItems();
}
private getAllItems():void {
this._RepairsService
.GetRepairs()
.subscribe((data) => {
this._RepairsService.UpdateItems(data);
},
error => console.log(error),
() => {
this.myItems = this._RepairsService.GetItems();
});
}
}
This work just fine but when i try to invoke GetItems() in child component i get undefinded. I try to do it inside constructor and ngOnInit with the same result.
child.component.ts
export class ChildComponent {
private items:any[] = [];
constructor(private _RepairsService:RepairsService,
private _Configuration:Configuration) {
this.items = this._RepairsService.GetItems();
// undefinded
}
ngOnInit() {
this.items = this._RepairsService.GetItems();
// undefinded
}
}
From what i can see in the limited amount of code you shared, it would seem you are trying to get the items before the http get call finishes and saves the data. I think a better design pattern would be to make the GetItems() function also an observable or promise, and check if the data is there, if not call the http get call, and once that completes send the data back to the different components that need it.
As #MSwehli mentioned with async code execution you can't rely on the order of code lines. In this code:
ngOnInit() {
this.items = this._RepairsService.GetItems();
// undefinded
}
the async code in GetItems(); is scheduled for later execution into the event queue and then continued with the sync code. The scheduled code will be executed eventually but it's not determined when. It depends on the response of the server in this example.
If you return a Promise you can use .then(...) the chain the execution so that your code is only executed when the async execution is completed.
There are two errors/inconsistencies in your code:
userAuthenticate() call followed with getAllItems() call. These calls are async, user is not yet authenticated by the time getAllItems() is called, getAllItems will fail.
Solution here is to chain calls using rxjs flatMap:
//assuming userAuthenticate returns Observable
userService.userAuthenticate().flatMap(()=>{
return repairsService.GetRepairs();
}).subscribe(..process repairs..);
getAllItems() is called nearly at the same time as GetItems(). In most cases it fails also, because previous http request is not completed when GetItems() is called.
In my opinion early initialization is not necessary here, use service directly:
//ChildComponent
ngOnInit() {
this._RepairsService.GetRepairs().subscribe(..do anything with list of repairs i.e. assign to bindable property..);
}
You could add console.log statements in each part of the code to see the order of events in your app.
I have a service that does an http request to save some data. When the data comes from the backend I am doing some manipulation on the data and then return it so that controllers can use them. Something like:
public savePerson = (person: Model.IPerson): ng.IPromise<Model.IMiniPerson> => {
return this.api.persons.save({}, person).then((savedPerson) => {
this.enrichWithLookups(savedPerson);
var miniPerson = new Model.MiniPerson();
angular.extend(miniPerson, savedPerson);
miniPerson.afterLoad();
this.persons.unshift(miniPerson);
this.notifyOfChanges();
return miniPerson;
});
}
In order to clean up the code a bit and make it more testable I wanted to remove the private manipulation functions into decorating/intercepting services. Problem is I do not know how to hook on the promise data before the success function is executed and after it is returned.
For example enrichWithLookups must be applied first just after the data arrives and not after the miniPerson is returned.
you can create a local promise and call the "resolve" method when you have completed your operations on the http response. Look at the code down here:
public savePerson = (person: Model.IPerson): ng.IPromise<Model.IMiniPerson> => {
var waiter = $q.defer();
this.api.persons.save({}, person).then((savedPerson) => {
this.enrichWithLookups(savedPerson);
var miniPerson = new Model.MiniPerson();
angular.extend(miniPerson, savedPerson);
miniPerson.afterLoad();
this.persons.unshift(miniPerson);
this.notifyOfChanges();
waiter.resolve(miniPerson);
});
return waiter.promise;
}
I've wrote the code directly with angularjs, but I think that you can easily adapt it to fit your needs.
Bye.