I wondered how to update a observable inside a subscription without triggering to many events.
In this example I subscribe to area: Observable<Area>, if the area changes I'd like to update the theme: Observable<Theme>. distinctUntilChanged() should do that the subscription is only triggered if the value has changed, but everytime the area gets updated, the amount of theme updates increases by one.
observeArea(): void {
this.area
.pipe(distinctUntilChanged())
.subscribe(area => {
this.themeService.updateTheme({primaryColor: area.color}); // should happen only once per area change
})
}
Is there any "correct" way of doing this, without triggering endless theme updates?
I think the problem might be with the way you are updating the theme.
If you are trying to update the theme by calling observeArea() method.
Every time you call the method a new subscription will be created.
Event will be passed to every subscription. So each time you call the method one subscription will be increasing.
Solution
Use an async pipe
area$!: Observable<any>;
observeArea(): void {
this.area$ = this.area
.pipe(
distinctUntilChanged(),
tap(area => this.themeService.updateTheme({primaryColor: area.color});)
)
}
and in html use area$ | async to subscribe.
example:
<ng-container *ngIf="area$ | async">...</ng-container>
Else you should unsubscribe every time your subscription completes
observeArea(): void {
const sub = this.area
.pipe(distinctUntilChanged())
.subscribe(area => {
this.themeService.updateTheme({primaryColor: area.color});
sub.unsubscribe();
})
}
Better to use async pipe.
I believe this solves your issue.
Related
iam trying to use async pipes instead of manually subscribing in components, in case, iam just displaying data which i get from server, everything works fine. But what if i need to change displayed part of displayed data later, based on some event?
For example, i have a component which displays timetable, which can be later accepted by user. I create timesheet$ observable and i use it in my template with async pipe. For accepting i created subject, so i can emit, when user accepts the timesheet. But how do i combine these two streams(approvementChange$ and timesheet$), so the $timesheet gets updated? I was trying combineLatest, but it returns the latest values, so i cant decide, if the value from approvementChange stream is new or old. Any ideas how to solve this?
export class TimesheetComponent {
errorMessage: string = '';
timesheet$: Observable<Timesheet>;
private approvementSubject = new Subject<TimesheetApprovement>();
approvementChange$ = this.approvementSubject.asObservable();
constructor(
private planService: PlanService,
private statusService: StatusService,
private notifService: NotificationService
) {}
this.timesheet$ = this.statusService.appStatus.pipe(
switchMap((status) => {
return this.planService.getTimesheet(
status.selectedMonth,
status.selectedAgency.agNum
);
})
); //get data every time user changed selected month
approveTimesheet(isApproved: boolean = true) {
const approvement: TimesheetApprovement = {
isApproved: isApproved,
approveTs: new Date(),
isLock: true,
};
this.approvementSubject.next(approvement);
}
}
But what if i need to change displayed part of displayed data later, based on some event?
RxJS provides lots of operators for combining, filtering, and transforming observables.
You can use scan to maintain a "state" by providing a function that receives the previous state and the newest emission.
Let's look at a simplified example using a generic Item interface:
interface Item {
id : number;
name : string;
isComplete : boolean;
}
We will use 3 different observables:
initialItemState$ - represents initial state of item after being fetched
itemChanges$ - represents modifications to our item
item$ - emits state of our item each time a change is applied to it
private itemChanges$ = new Subject<Partial<Item>>();
private initialItemState$ = this.itemId$.pipe(
switchMap(id => this.getItem(id))
);
public item$ = this.initialItemState$.pipe(
switchMap(initialItemState => this.itemChanges$.pipe(
startWith(initialItemState),
scan((item, changes) => ({...item, ...changes}), {} as Item),
))
);
You can see we define item$ by piping the initialItemState$ to the itemChanges$ observable. We use startWith to emit the the initialItemState into the changes stream.
All the magic happens inside the scan, but the set up is really simple. We simply provide a function that accepts the previous state and the new change and returns the updated state of the item. (In this case, I'm just naively apply the changes to the previous state; it's possible this logic would need to be more sophisticated for your case.)
This solution is completely reactive. The end result is a clean item$ observable that will emit the updated state of the current item (based on id), whenever the id changes or the changes occur on the item.
Here's a StackBlitz where you can see this behavior.
you need to track only approvementChange
after that you can pick the latest timesheet via withLatestFrom
approvementChange$
.pipe(withLatestFrom(this.timesheet$))
.subscribe(([accepted, latestTimesheet]) => accepted ? save(latestTimesheet) : void 0)
I have a component with 2 inputs (or mor) and I want to:
Trigger a method X the first time, when both value are set and exists
Trigger a method X each time, if either one of the both value changes
<some-cmp [itemid]="activeItemId$ | async" [userId]="activeUserId$ | async"></some-cmp>
Both values can change at any time, so I figured using rxjs to build a stream lets me control everything. My current solution seems a bit hacky and is difficult to test. I use 2 BehaviourSubjects and combineLatest with a debounceTime.
#Input() set itemId (id){this.itemId$.next(id)};
#Input() set userId (id){this.userId$.next(id)};
itemId$ = new BehaviourSubject$(null);
userId$ = new BehaviourSubbject$(null);
ngOnInt(){
combineLatest([
this.itemId$.pipe(filter(item=>item!===null)),
this.userId$.pipe(filter(item=>item!===null))
]).pipe(
debounceTime(10),
switchMap(...)
).subscribe(...)
}
So my question are
Is there a more elegant way to achieve this behavior?
Is there a way to avoid the debounceTime, which makes testing difficult?
The debounceTime is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
You are right in using combineLatest, it will only emit the first time after each source has emitted once and then will emit any time any source emits.
Is there a way to avoid the debounceTime. [It] is used in case both value do arrive at the same time and I don't want combineLatest to trigger the method twice.
Maybe debounceTime isn't necessary due to the initial behavior of combineLatest; it will not emit the first time until all sources emit. However if you typically receive subsequent emissions from both sources that occur within a short timeframe, use of debounceTime may be an appropriate optimization.
Is there a more elegant way to achieve this behavior?
I think your code is fine. However, it may not be necessary to use BehaviorSubject since you aren't really using the default value. You could use plain Subject or ReplaySubject(1).
You could assign the result of your combineLatest to another variable and subscribe to that inside ngOnInit or use the async pipe in the template:
#Input() set itemId (id){ this.itemId$.next(id) };
#Input() set userId (id){ this.userId$.next(id) };
itemId$ = new Subject<string>();
userId$ = new Subject<string>();
data$ = combineLatest([
this.itemId$.pipe(filter(i => !!i)),
this.userId$.pipe(filter(i => !!i))
]).pipe(
debounceTime(10),
switchMap(...)
);
ngOnInit() {
this.data$.subscribe(...);
}
Angular provides ngOnChanges hook which can be used in this scenario. It'll trigger ngOnChanges method whenever there's a change in any of the inputs of the component.
Below is an example of how this can be achieved:
export class SomeComponent implements OnChanges {
#Input() itemId: any;
#Input() userId: any;
ngOnChanges(changes: SimpleChanges) {
const change = changes.itemId || changes.userId;
if (change && change.currentValue !== change.previousValue) {
this.doSomething();
}
}
private doSomething() {
// Your logic goes here
}
}
Your HTML will now look clean and you can get rid of async as well:
<some-cmp [itemid]="itemId" [userId]="userId"></some-cmp>
I was trying to figure out that for a long time by myself but I stuck.
My case: I have a table with data, when user enters the page the request goes to REST API. Above the table there is a form that specify/filters results of the table. When user clicks a button, new request with query parameters goes to a service.
I'm trying to find a way to stop a previous request when user clicked a button, to search a table with specific parameters
Here is what I have done so far stackblitz. Maybe my approach is wrong and someone could show me how to write it better, or maybe there are just few thing to correct.
Thank you for any help
You can use the takeUntil operator inside your pipe, and pass a subject to it, and everytime you apply a new filter you can emit something to that subject to unsubscribe from the previous subscription and subscribe again! let me give you an example on how to use this operator:
regardless if your code was working or not!! I added just the part on how to use the takeUntil operator!
...
unsubscribe = new Subject<boolean>();
constructor(private _service: ServiceConnectService) {}
ngOnInit() {
this.clickStream
.pipe(
map(() => console.log(1)),
switchMap(() => {
return this.getMethodServicesInstances();
}),
takeUntil(this.unsubscribe) // this will be executed once you call next with a value!! and the subscription will be stopped!
)
.subscribe(results => {
console.log(2);
});
}
applyFilter(){
this.unsubscribe.next(true);
/*
you can then rerun the subscription and reset the unsubscribe subject state to null!
*/
}
...
I want to set setInterval inside an onChange event, like mostly it is done on componentDidMount.
I have 2 dropdowns that filter data and then render a table, the dropdowns are controlled by onChange methods. The last on change method has a request that needs to be re-polled every X # seconds to get updated information from the server. However, these methods are outside of cDM so I'm not sure how to handle the setInterval like I previously have.
cDM() {
//axios call, update state with fetched data
}
onChange1 () {
// uses data from cDM to render the options in a dropdown.
// another axios call based on the selection, fetches data to render more options in subsequent dropdown
}
onChange2 () {
//dropdown 2 use data from onChange1 axios call. After the selection from the dropdown is made, it makes an api call that then renders data to a table.
//Some of this data updates every 5 seconds, so I need to re-poll this service to get updated information from the server.
}
if all the data was in cDM I'd normally change the data requests in cDM to an arrow function to avoid setState issues, call the function inside/outside of the setInterval callback with the following:
componentDidMount() {
this.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.interval);
}
getData = () => {
//data requests
}
SetInterval does not wait for the AJAX response before polling starts over again. This can become extremely buggy/memory intensive in the event of network problems.
I would suggest you use a setTimeOut and every update I would put a piece of response data in state and start the timer upon state changes inside your render function. That way you always ensure you get your result back before pounding the server again and bogging down your client's UI.
Place the code of componentDidMount body into both onChange events.
Set Interval equal to some state variable, so on componentWillUnmount you can access that interval and remove it.
onChange () {
// Change Logic
....
// Interval logic
clearInterval(this.state.interval); //Clear interval everytime.
this.state.interval = setInterval(() => this.getData(), 10000);
this.getData();
}
componentWillUnMount() {
clearInterval(this.state.interval);
}
You can indeed place this in your update event handler - event handlers do not have to be pure. There is the minor issue that doing this means that your view is no longer a function of state + props, but in reality that's not usually a problem.
It seems your main concern is how you model this data.
One way to do this might be to use a higher order component or some kind of composition, splitting the display of your component from the implementation of the business logic.
interface Props<T> {
intervalMilliseconds: number
render: React.ComponentType<ChildProps<T>>
}
interface State<T> {
// In this example I assume your select option will be some kind of string id, but you may wish to change this.
option: string | undefined
isFetching: boolean
data: T
}
interface ChildProps<T> extends State<T> {
setOption(option: string | undefined): void
}
class WithPolledData<T> extends React.Component<Props<T>, State<T>> {
interval: number
componentDidMount() {
this.setupSubscription()
}
componentDidUnmount() {
this.teardownSubscription()
}
setupSubscription() {
this.interval = setInterval(() => this.fetch(), this.props.intervalMilliseconds)
}
teardownSubscription() {
clearInterval(this.interval)
}
componentDidUpdate(previousProps: Props, previousState: State) {
if (previousProps.intervalMilliseconds !== this.props.intervalMilliseconds) {
this.teardownSubscription()
}
if (previousState.option !== this.state.option && this.state.option !== undefined) {
this.fetch()
}
}
fetch() {
if (this.props.option === undefined) {
// There is nothing to fetch
return
}
// TODO: Fetch the data from the server here with this.props.option as the id
}
setOption = (option: string | undefined) => {
this.setState({ option })
}
render() {
return React.createElement(
this.props.render,
{
...this.state,
setOption: this.setOption
}
)
}
}
You could use this component like so:
<WithPolledData intervalMilliseconds={5000} render={YourComponent} />
YourComponent would call setOption() whenever a drop down option was selected and the fetching would happen in the background.
This is how you would technically do this. I agree with #ChrisHawkes that using setInterval like this is likely a mistake - You would at the very least need to cancel in-flight HTTP requests and you run the risk of causing issues on devices with poor network performance.
Alternatively, a push rather than a pull model may be better here - This scenario is exactly what WebSockets are designed for and it wouldn't require too much modification to your code.
I have two child components and i want pass data between them, to do this i use service.
In service i have
headerObject = new Subject<HeaderObject>();
In first component i have method
onClick() {
this.listImage = this.imageService.getImages();
let headers = new HeaderObject();
headers.file = this.listImage[0].data;
this.documentService.getOcrDocument(headers).subscribe(res => {
headers = res;
this.ocrService.headerObject.next(headers);
})
In second component
headerSubcribed: HeaderObject;
private headerSubscription: Subscription;
ngOnInit() {
this.headerSubscription =
this.ocrService.headerObject.subscribe((ocrHeader: HeaderObject) => {
this.headerSubcribed = ocrHeader;
}
But ngOnInit() only once after render view i want to move this code to outside ngOnit and exeute it but i don't know how ? Some tips how execute that code? I set breakpoint at next() method that emit value but later method subscribe isn't execute? What could I do wrong?
There are 2 parts which you observable/subscribe
Observing something and notifying.
In your service, you have a Subject type variable and you are pushing values to it by calling next(). when this method called, it notifies all the observers means who ever is subscribing it.
listening to the observer and do action
When you listen to an observer, you get an update when they change the value. It does not matter if you put your subscription in onNginit or constructor, if the code is there, it will notify you when there is a change into it.
You can also explore BevavourSubject if it fits to your situation.