I'm using Angular 4 and I have a component that lives at the 'player/:id' route in my application. When the player navigates to this route from within the application I use a currentPlayerService to sync the current player within the app. This works great with the below code.
this.currentPlayerService.getPlayer()
.subscribe((player) => this.currentPlayer = player);
However, when the application loads directly to the 'player/:id' route (from an external link) I can comment out the above and use the code below to set the currentPlayer from the route params.
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
});
What I'd like to do (and am having difficulty finding the "reactive function programming" way of saying) is to load the player from the params only if this.currentPlayerService.getPlayer() doesn't have a value on load. I'm having a hard time conceiving of a way to use the Observable in some kind of logic control flow, so any help would be appreciated.
Thanks!
Observables itself is stateless. To keep track of a value (or current value), you will need to use either Subject or BehaviorSubject.
In your currentPlayerService, create a BehaviorSubject called playerBSubject:
export class CurrentPlayerService{
public playerBSubject: BehaviorSubject<any>;//can be type of Player if you have such type
constructor(){
this.playerBSubject = new BehaviorSubject({})//any value that you want to initialize
}
getPlayer(){
//do your logic here, whatever that is.
//I am using an Observable.of to mimic a http request
Observable.of({})
.subscribe((player)=>{
this.playerBSubject.next(player)//this will update the subject with the latest value
});
return this.playerBSubject.asObservable();
}
}
Note that the .next() method will update the values of your subject, and you can return it as an observable using .asObservable().
Now, in your component controller, you can check if your BehaviourSubject exist (or has the value you want), and only call playerService.getPlayer() if need to.
this.route.params.subscribe(params => {
//check if BehaviorSubject exist or not
if (this.currentPlayerService.playerBSubject.getValue() === {}) {
this.playerService.getPlayer(params.id)
.subscribe((player) => {
this.currentPlayer = player;
this.currentPlayerService.setPlayer(player);
});
}
});
Suggestions:
I am not sure why you need two service namely currentPlayerService and playerService. If your currentPlayerService is just to keep track of "current" value of the player, then you do not need it at all, should you use BehaviorSubject to keep track of your current player. All of them can boil down into one single service.
export class PlayerService {
public playerBSubject: BehaviorSubject<any>;
constructor() {
this.playerBSubject = new BehaviorSubject({})
}
getPlayer(id) {
Observable.of({id}) // implement your own logic
.subscribe((player) => {
this.playerBSubject.next(player)
});
return this.playerBSubject.asObservable();
}
setPlayer(id) {
return Observable.of({id})//implement your own logic
.subscribe(newPlayer => this.playerBSubject.next(newPlayer))
}
}
And in your controller if you want to get the current value you can just do:
this.currentPlayer = this.playerService.playerBSubject.getValue();
And with a little help of asObservable you can do this:
this.playerService
.asObservable()
.subscribe(player => {
if (player === {}) {
this.route.params.subscribe(params => {
this.playerService.getPlayer(params.id); //voila, your player is updated
})
}
//remember to update the value
this.currentPlayer = player
})
Related
I am working on an angular project that needs to load up pages and then display them one by one / two by two.
As per this article and some other sources, subscribing in services is almost never necessary. So is there a way to rewrite this in pure reactive style using RxJS operators?
Here's what I have (simplified) :
export class NavigationService {
private pages: Page[] = [];
private mode = Mode.SinglePage;
private index = 0;
private currentPages = new BehaviorSubject<Page[]>([]);
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.pageService.pages$.subscribe(pages => {
this.setPages(pages);
});
this.view.mode$.subscribe(mode => {
this.setMode(mode);
});
}
private setPages(pages: Page[]) {
this.pages = pages;
this.updateCurrentPages();
}
private setMode(mode: Mode) {
this.mode = mode;
this.updateCurrentPages();
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.index += 1;
this.updateCurrentPages();
}
public get currentPages$() {
return this.currentPages.asObservable();
}
}
I've tried multiple solutions and didn't manage to get it right. The closest I got was using scan(), but it always reset my accumulated value when the outer observables (pages, mode) got updated.
Any help is appreciated, thanks !
You can use merge to create reducer functions from observables. These functions will update part of a state maintained by the service. They are past along to the scan operator which will update the prior state from the reducer. After the reducer is run, currentPages is set on the new state and that new state is returned.
export class NavigationService {
private readonly relativePageChangeSubject = new Subject<number>();
readonly state$ = merge(
this.pageService.pages$.pipe(map((pages) => (vm) => ({ ...vm, pages }))),
this.relativePageChangeSubject.pipe(map((rel) => (vm) => ({ ...vm, index: vm.index + rel }))),
this.view.mode$.pipe(map((mode) => (vm) => ({ ...vm, mode })))
).pipe(
startWith((s) => s), // if necessary, force an initial value to be emitted from the initial value in scan.
scan((s, reducer) => {
const next = reducer(s);
// update currentPages on the next state here.
return next;
}, { currentPages: [], index: 0, mode: Mode.SinglePage, pages: [] }),
shareReplay(1)
)
readonly currentPages$ = this.state$.pipe(
map(x => x.currentPages),
distinctUntilChanged()
);
constructor(private pageService: PageService, private view: ViewService) { }
goToNextPage() {
this.relativePageChangeSubject.next(1);
}
}
Notes:
Instead of having a nextPage Subject, a more flexible relative change subject is used that will modify the index from the value in the prior state.
The currentPage$ observable isn't necessary, as a consumer could just attach to the main state$ and map as needed. Feel free to make state$ private or remove currentPage$.
Let's first detail what you are doing:
You subscribe to pageService.pages$ and view.mode$
Those subscriptions take the values and put them in a private variabkle
Then fire a function to use those two variables
Finally, trigger a value push.
All this can be done in a simple pipeline. You'd need to include the index as an observable (behaviour subject in our case) to react to that change too.
Use combineLatest, this will subscribe to all observables we want, and trigger the pipe WHEN ALL have fired once, and every time one changes afterwards. You may want to use .pipe(startWith("something")) on observables that should have a default value so your observable pipe triggers asap.
CombineLatest will then provide an object as value, with each value in the object key passed when created. Here pages, mode and index. I've used a switchMap to demo here if updateCurrentPages passes an observable, but you could use a map if there is no async task to be done.
export class NavigationService {
readonly currentPages$:Observable<Pages[]>;
constructor(
private pageService: PageService,
private view: ViewService,
) {
this.paginator = new Paginator(this.pageService.pages$);
this.currentPages$ = combineLatest({
pages:this.pageService.pages$,
mode:this.view.mode$,
index:this.this.paginator.pageChange$
}).pipe(
switchMap(({pages,mode,index})=>{
return this.updateCurrentPages(pages,mode);
}),
);
}
private updateCurrentPages() {
// get an array of current pages depending on pages array, mode & index
this.currentPages.next(...);
}
public goToNextPage() {
this.paginator.next();
}
}
class Paginator{
pageChange$ = combineLatest({
total:this.pages$.pipe(map(pages=>pages.length)),
wanted:this.pageMove$}).pipe(map({total,wanted}=>{
// Make sure it is between 0 and maximum according to pages.
return Math.max(Math.min(total-1,wanted),0);
}),
// Do not emit twice the same page (pressing next when already at last)
dinstinctUntilChanged());
);
pageMove$ = new BehaviorSubject<number>(0);
constructor(pages$: Observable<Pages[]>){
}
next(){
this.pageMove$.next(this.pageMove$.value()+1);
}
previous(){
this.pageMove$.next(this.pageMove$.value()-1);
}
to(i:number){
this.pageMove$.next(i);
}
}
Beware tough about stating that subscribe is never needed. You may want to subscribe to some events for some reason. It is just that combining everything in a pipeline makes things easier to handle... In the example above, the observables will be unsubscribed to when the consumer of your service unsubscribes to currentPages$. So one thing less to handle.
Also, note that if multiple consumers subscribe to this service's currentPages$ the pipeline will be duplicated and unecessary work will be done, once for each subscriber. While this MAY be good, you might want to have everyone subscribe to the same "final" observable. This is easily do-able by adding share() or shareReplay(1) at the end of your pipeline. Share will make sure the same observable pipeline will be used for the new subscriber, and they will receive new values starting from then. Using shareReplay(1), will do the same but also emit the latest value directly on subscribe (just like BehaviourSubject) the 1 as parameter is indeed the number of replays to send out...
Hope this helps! When you master the RxJS you'll see that things will get easier and easier (see the difference in code amount!) but getting the hang of it take a little bit of time. Do not worry, just perseverate you'll get there. (Hint to get better, using outside variables/properties are the evil of handling pipelines)
Basically, as you can see in the code, I only want to run one of these API requests. If origCompId is passed, it runs duplicateInstance, otherwise, it runs addNewInstance. origCompId is a query parameter so it takes a second for it to update. This causes addNewInstance to run because origCompId begins as null. I feel like the logic is simple here, but for some reason, I’m just not seeing the solution.
I tried adding the hasRendered ref, as you can see, but that didn’t prevent it from happening. The problem is, that I need this hook to run when the app first loads. I can’t separate these API calls. If the user is loading the app for the first time, it’ll run addNewInstance and load their initial data. If the user duplicates their existing app, it’ll have origCompId and run duplicateInstance.
Any help would be much appreciated!
import { useEffect, useRef } from "react";
import { addNewInstance, duplicateInstance } from "../util/services";
const useAddInstanceToDB = (instance, compId, origCompId) => {
const hasRendered = useRef<boolean | null>(null);
useEffect(() => {
const controller = new AbortController();
if (hasRendered.current) {
if (instance && compId && origCompId) {
duplicateInstance(instance, controller, compId, origCompId);
} else if (instance && compId) {
addNewInstance(instance, controller, compId);
}
}
return () => {
controller.abort();
hasRendered.current = true;
};
}, [instance, compId, origCompId]);
};
export default useAddInstanceToDB;
I think you can set the initial props to null / undefined so that non of the statement will run before your api completes
useAddInstanceToDB(undefined, undefined, undefined);
const useAddInstanceToDB = (instance, compId, origCompId) => {
useEffect(() => {
const controller = new AbortController();
if (origCompId) {
if (instance && compId && origCompId !== -1) { // will not run when hook is inited
duplicateInstance(instance, controller, compId, origCompId);
} else if (instance && compId && origCompId === -1) { // also will not run when hook is inited, only run when instance and compId is defined
addNewInstance(instance, controller, compId);
}
}
return () => {
controller.abort();
};
}, [instance, compId, origCompId]);
};
setOrigCompId to -1 if new instance is needed, or any value other than -1 if duplicate is needed
Possible to have better solutions and I am open for that
You want to run your side effect exactly when your component gets information about origCompId whether it exists or not. Right now your component just can't know when the data is loaded - so you can't do anything. So you need to tell it to your component - you can set origCompId = -1 if it not exists (as suggested by previous author), but I advise you to create a new variable that will tell your component that all the data has loaded.
I'm listening to the observable that may return true or false value - the only thing that I want to do is to set throttleTime for function call when it's true and don't have it when it's false. So I did some kind of workaround for that but I don't like this solution. I have tried a different approach where I tried to do it in the actions' effect but without success..
So this is the observable:
this.store$
.pipe(
takeUntil(this.componentDestroyed$),
select(selectGlobalsFiltered([
GlobalPreferencesKeys.liveAircraftMovement])),
distinctUntilChanged()
)
.subscribe((globals) => {
if (globals && globals[GlobalPreferencesKeys.liveAircraftMovement] !== undefined) {
this.isLiveMovementEnabled = (globals[GlobalPreferencesKeys.liveAircraftMovement] === 'true');
}
if (!this.isLiveMovementEnabled) {
this.processPlaneData = throttle(this.processPlaneData, 4000);
} else {
this.processPlaneData = this.notThrottledFunction;
}
});
And as you can see I've created excat the same method that is 'pure' - notThrottledFunction and I'm assigning it when it's needed.
processPlaneData(data: Airplane[]): void {
this.store$.dispatch(addAllAirplanes({ airplanes: data }));
}
notThrottledFunction(data: Airplane[]): void {
this.store$.dispatch(addAllAirplanes({ airplanes: data }));
}
So basically this is working solution, but I'm pretty sure there is a better approach for doing such a things.
*throttle(this.processPlaneData, isLiveMovementEnabled ? 0 : 4000) doesn't work
So the second approch where I tried to do this inside of effect, I added a new argument for addAllAirplanes action - isLiveMovementEnabled: this.isLiveMovementEnabled
addAllAirplanes$ = createEffect(() =>
this.actions$.pipe(
ofType(ActionTypes.ADD_ALL_AIRPLANES),
map((data) => {
if (data.isLiveMovementEnabled) {
return addAllAirplanesSuccessWithThrottle(data);
} else {
return addAllAirplanesSuccess(data);
}
}
)
);
And then I added another effect for addAllAirplanesSuccessWithThrottle
addAllAirplanesThrottle$ = createEffect(() =>
this.actions$.pipe(
ofType(ActionTypes.ADD_ALL_AIRPLANES_THROTTLE),
throttleTime(4000),
map((data) => addAllAirplanesSuccess(data))
)
);
But it doesn't work. Can someone help me?
(It's not clear how the data arrives in your example code, but I'll assume an Observable)
throttle and throttleTime are similar to your use case, but I think it makes sense to build your own custom implementation without them. I'd suggest managing the timing yourself, using Date to determine time deltas.
You've already cached the live filtering boolean in the component state, so we can just handle all of the data stream from your position update observable and filter them out manually (which is all throttle does, but it expects you to be able to feed it an interval at subscription time, and yours needs to be dynamic).
Setup component scoped variables to contain previous timestamps, as
private prevTime: number;
private intervalLimit: number = 4000;
Supposing data$ is your input plane position data stream:
data$.pipe(filter(data => {
const now: number = Date.now();
const diff = now - this.prevTime;
if (this.isLiveMovementEnabled) {
// no throttle - pass every update, but prepare for disabling too
// record when we last allowed an update & allow the update
this.prevTime = now;
return true;
} else if (diff > intervalLimit) {
// we are throttling results, but this one gets through!
this.prevTime = now;
return true;
} else {
// we're throttling, and we're in the throttle period. eat the result!
return false;
}
}
Something like that gives you full control over the logic used whenever data comes in. You can add other operations like takeUntil and distinctUntilChanges into the pipe and trust that when you subscribe you'll be getting updated when you want them.
You can even adjust the intervalLimit to dynamically adjust the throttle period on the fly.
i need to change the source of an observable with a swith
.
this.su = this.appService._sub1.subscribe(data => {
this.items.push(data);
});
//appService
setSub(name) {
if (name == 'A') {
console.log('B');
this._sub1 = this.sub2;
} else if (name == 'B') {
console.log('B');
this._sub1 = this.sub3;
}
console.log(this._sub1);
}
however, the source of the first observable keeps sending data, how can I do it?
Stackblitz link
https://stackblitz.com/edit/angular-ivy-evczcx?file=src/app/app.service.ts
Ultimately, I think you will want to use switchMap. You don't really want to switch the subscription, but rather the source.
It's cleaner if you design your service to expose observables, then have your components subscribe to them (ideally with AsyncPipe).
As a sample, something like this should work:
export class AppService {
private source$ = new BehaviorSubject<SourceType>('frutas');
private frutas = ['pera', 'manzana', 'platano', 'fresa'];
private electronicos = ['celular', 'pc', 'cable'];
private frutas$ = interval(2000).pipe(
map(n => n % 4),
map(i => this.frutas[i])
);
private electronicos$ = interval(2000).pipe(
map(n => n % 3),
map(i => this.electronicos[i])
);
data$ = this.source$.pipe(
switchMap(source => source === 'frutas' ? this.frutas$ : this.electronicos$)
);
setSource(name: SourceType) {
this.source$.next(name);
}
}
Here's a working StackBlitz demo.
Swapping a reference to a subscription does nothing to the subscription itself. You need to unsubscribe. Also keep in mind, that interval produces a "hot" observable. Meaning it will push data to the stream wether someone has subscribed or not.
In real life, you would probably use something like httpClient.get() which is not "hot" (runs only when subscribed to, and completes once it's done). So it's not an issue usually, but with interval, you will end up with memory leaks and performance hits unless you manually stop the scheduler from running.
this._sub1.unsubscribe();
I am using ng2-simple-timer in my ionic3 App.
Here is code from repo:
counter: number = 0;
timerId: string;
ngOnInit() {
this.timerId = this.st.subscribe('5sec', () => this.callback());
}
callback() {
this.counter++;
}
simpletimer will create timer name and tick every 'number' of seconds.
callback will return counter value (0,1,2,3,4,5,6, etc..)
what is my problem?
I want define unique uniquecounterName: number = 0; because I have more than one timer.
what will be my results:
return uniquecounterName(0,1,2,3,4,5,6, etc..)
return otheruniquecounterName(0,1,2,3,4,5,6, etc..)
in other words callback() function must return pre defined unique variable names like as this.counter
callback(var) {
var++;
}
this one will not work because I want use var in my view.
....
It doesn't seem to be possible, if you take a look at the "GitHub: ng2-simple-timer-example", directly from the docs, you'll find how the author deals with multiple timers; I won't quote all the code, you can look at it yourself, but just paste here the way callbacks are handled:
timer0callback(): void {
this.counter0++;
}
timer1callback(): void {
this.counter1++;
}
timer2callback(): void {
this.counter2++;
}
As you can see, the whole process (new, subscribe, del, unsubscribe) is done for each timer. So the library doesn't support your use case directly.
What you could do is inline the callback function so you have access to the same variables you had when creating it:
function sub(name) {
this.timerId = this.st.subscribe(name, () => {
// still have access to the name
});
}
Of course this has to be heavily adapted to your purposes, this is as much as I could gather from your question.