How to handle only one observer and dont call others? - javascript

I have the following usage of rxjs streams:
ngOnInit() {
combineLatest(
this.eventsService.subjectSearchDistribution.pipe(
tap((querySearch) => {
this.paginationService.setQuery(querySearch);
this.paginationService.reset();
}),
),
this.eventsService.subjectSortingDistribution.pipe(
tap((sortedList: ListItem[]) => {
this.paginationService.setSortBy(getSortingString(sortedList));
}),
),
this.eventsService.subjectFilterDistribution.pipe(
tap((filterUrl) => {
const page = 1;
this.paginationService.setFilterBy(filterUrl);
this.paginationService.setCurrentPage(page);
this.paginationService.calculateOffsetLimit(page);
}),
),
this.eventsService.subjectFilterDistributionReset.pipe(tap(() => this.paginationService.reset())),
).subscribe(() => {
this.loadPage();
});
}
Problem is I need to handle only one case, onle one stream and dont call others, as result call this.loadPage();.
Now when I send message to this.eventsService.subjectSearchDistribution, this.eventsService.subjectSortingDistribution, this.eventsService.subjectFilterDistribution.
I see that calling of this.loadPage(); increases from fist time +1 each event.
SO, ONLY one observer can be active, not all torgether.
How to fix it?

It seems the reason your loadPage method is called twice due to your event listeners, but without sharing the code for those methods I cannot confirm that issue. The simplest way to fix your double call of the loadPage method would be this:
class A {
constructor() {
this.pageLoadCalled = false;
this.loadPage();
this.events.filter.listen().subscribe((res) => this.loadPage());
this.events.search.listen().subscribe((res) => this.loadPage());
}
loadPage() {
if (this.pageLoadCalled) {
// Exit early (will not call anything below the return)
return;
}
// Mark this method as being called
this.pageLoadCalled = true;
return new Promise((resolve) => {
// do stuff
resolve();
});
}
}

If you want to call loadPage only once, don't execute it when events.filter and events.search trigger:
class A {
constructor() {
// Call pageLoad in the constructor only once
this.loadPage();
// Remove call to pageLoad when events fire.
// this.events.filter.listen().subscribe((res) => this.loadPage());
// this.events.search.listen().subscribe((res) => this.loadPage());
}
}

I solved this using rxjs:
ngOnInit() {
combineLatest(
this.eventsService.subjectSearchDistribution.pipe(
tap((querySearch) => {
this.paginationService.setQuery(querySearch);
this.paginationService.reset();
}),
),
this.eventsService.subjectSortingDistribution.pipe(
tap((sortedList: ListItem[]) => {
this.paginationService.setSortBy(getSortingString(sortedList));
}),
),
this.eventsService.subjectFilterDistribution.pipe(
tap((filterUrl) => {
const page = 1;
this.paginationService.setFilterBy(filterUrl);
this.paginationService.setCurrentPage(page);
this.paginationService.calculateOffsetLimit(page);
}),
),
this.eventsService.subjectFilterDistributionReset.pipe(tap(() => this.paginationService.reset())),
)
.pipe(takeUntil(this._onDestroy))
.subscribe(() => {
this.loadPage();
});
}

Related

Wait x seconds for new emitted value before triggering a function

I have a child component that emits a value, and in the parent I perform an axios call with this value each time it is emitted. My problem is that I want to trigger the axios call only if in x ms (or seconds) the child has not emmited another value in order to reduce the amount of calls I do.
Code here :
<script>
import axios from "axios";
import DataTable from './DataTable.vue';
export default {
name: 'Test',
data() {
return {
gamertags: [],
// Utils
timeout: 500,
delay: 500
}
},
methods: {
// API calls
async getGamerTags(string='') {
const path = `http://localhost:5000/gamertags?string=${string}`
await axios.get(path)
.then((res) => {
this.gamertags = res.data;
})
.catch((error) => {
console.error(error);
});
},
// DataTable
handleFilters(filters) {
clearTimeout(this.timeout);
this.timeout = setTimeout(this.getGamerTags(filters.find(o => o.field == "playerGamerTag").filter), this.delay);
}
}
components: {
DataTable
}
};
</script>
<template>
<DataTable
#filters="handleFilters"
/>
</template>
Thanks in advance.
What you need is debouncing. Here is an example:
var timeout, delay = 3000;
function func1() {
clearTimeout(timeout);
timeout = setTimeout(function(){
alert("3000 ms inactivity");
}, delay);
}
<input type="text" oninput="func1()">
When emitted, simply call func1(), and if there are no new emissions after 3000 ms, the function in timeout will be executed.
It would be better to understand the problem and use case if you add the code also.
but As I could understand the problem these is two way
if you using inside input and triggering based #changed event you can add #change.lazy this not trigger on each change.
second solution is to use setTimeout(function,delayInMs) inside parent
vuejs Docs link
By simply changing the handleFilters function to :
handleFilters(filters) {
clearTimeout(this.timeout);
this.timeout = setTimeout(
this.getGamerTags,
this.delay,
filters.find(o => o.field == "playerGamerTag").filter
);
},
the problem is solved.

setState causing infinite loop in custom hook

I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay

How can I pause and resume an observable on document click?

I have an observable that emits certain values to the console on every second. I show an example below and a StackBlitz as well.
My dilemma is I need to able to pause it on the next document click and resume it (not restart it) it on the next one again. I was looking into switchMap and I couldn't figure out how to implement it.
export class AppComponent implements OnInit {
title = "my-app";
pageClickObservable: Observable<Event> = fromEvent(document, "click");
ngOnInit(): void {
new Observable();
const watchForClick = () => {
setTimeout(() => {
console.log("A");
}, 1000);
setTimeout(() => {
console.log("BB");
console.log("10");
}, 2000);
setTimeout(() => {
console.log("CCC");
}, 3000);
setTimeout(() => {
console.log("DDDD");
console.log("20");
}, 4000);
setTimeout(() => {
console.log("EEEEE");
}, 5000);
setTimeout(() => {
console.log("FFFFFF");
console.log("30");
}, 6000);
};
this.pageClickObservable.pipe().subscribe(() => {
watchForClick();
});
}
}
I think you want something like this:
const $myContinuousObs = ...... // This is your observable that keeps emitting
const $isOn = new BehaviourSubject(true); // BehaviorSubject to store the current on/off state
combineLatest([$myContinuousObs, $isOn]).pipe( //combine the two
filter(res => res[1]), // only continue when $isOn is true
map(res => res[0]) // return only the myContinuousObs and not the $isOn
).subscribe(console.log) // log the value
To change $onOff:
$isOn.next(true)
$isOn.next(false)
To toggle $onOff
$isOn.pipe(take(1)).subscribe(val => $onOff.next(!val))
you need to turn your page click into some stream that can track the state of the switch using scan like so:
const clickStream$ = fromEvent(document, 'click').pipe(
scan((isOn, click) => !isOn, true), // on every click, toggle the switch
startWith(true) // initially emit true
)
then you can just switchMap:
clickStream$.pipe(
switchMap(isOn => isOn ? obs$ : EMPTY)
).subscribe(v => console.log(v, 'got value'))
where obs$ is whatever you want to start and stop listening to. now this method with switchMap will resubscribe on each clickStream$ emission, which depending on the nature of the observable of interest, may not be what you're after, but the concept of the clickStream$ observable may still be useful to you combined with the other answer here using filter
Check this out:
import { Component, OnInit } from "#angular/core";
import { fromEvent, Observable } from "rxjs";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
title = "my-app";
pageClickObservable: Observable<Event> = fromEvent(document, "click");
pause = false; // creating a variable to track the pause
ngOnInit(): void {
new Observable();
this.pageClickObservable.pipe().subscribe(() => {
this.pause = !this.pause; // change the pause status - true / false at alternative clicks
});
// mimicking observable sending data every second
setInterval(() => {
if (!this.pause) {
// if not pause then execute the complete functions
console.log("Click");
} else {
console.log("Do nothing");
}
}, 1000);
}
}
This will simply console "Click" and "Do nothing" based on clicks.
For example, you have an observable sending data at every second, let's say, setInterval
If they share a common variable, such as this.pause in this case, then you can pause and resume easily.
What I've done is to create an operator that delays the value based on a numerical property value.
delayByProperty.ts
import {of, NEVER} from 'rxjs';
import {concatMap, delay} from 'rxjsoperators';
export function delayByProperty(propertyNameOfSecondsDelay:string) {
return concatMap(obj => {
const seconds:any = obj[propertyNameOfSecondsDelay];
if (!seconds) {
return of(obj);
} else if (!Number.isFinite(seconds)) {
throw new TypeError(`object property "${propertyNameOfSecondsDelay}" expected to be falsy or a number, was ${typeof(secondsDelay)} with value of ${secondsDelay}.`);
} else if (seconds === -1) {
return NEVER;
} else if (seconds > 0) {
return of(obj).pipe(
delay(seconds * 1000)
)
}
});
}
Now you can do
from([1,2,3,2,5,2,1]).pipe(
map(n => {value:n, seconds:Number.parseFloat(n)}),
delayByProperty('seconds')
)
When dealing with Observables, I've found it useful to think less like a programmer and more like a plumber. :-) Consider this:
function getObservableOfMsgs(click$) {
//Array of objects that have whatever values you want in them, plus a property of "seconds" that contains the number of seconds you want to delay.
const objs = [{msg:'1', seconds:1},{msg:'2', seconds:2},{msg:'TWO', seconds:2}];
// observable we want per click.
const perClick$ = from(objs).pipe(
// map each object into an observable that delays, then emits the msg... and combine all of those observables to happen simultaneously.
mergeMap(({msg, seconds}) => of(msg).pipe(delay(seconds * 1000)))
);
return click$.pipe(
mapTo(perClick$)
);
}
and now you'll have an observable that emits a pattern of messages per time you click.
So if you have a function called doSomethingWithMessage that takes a msg as a single argument and returned either null or a message you wanted to log as a debug message, you could do...
const unsubscribeFunctionInCaseINeedIt = getObservableOfMsgs(click$).pipe(
tap(doSomethingWithMessage)
).subscribe(
(d) => if (d !== null) console.debug(d),
(e) => console.error(e),
() => console.log('Complete!')
)
and then, until you call the returned function, it will all just "happen" as events stream through.
from and of are part of rxjs, so you should be able to import {from, of} from 'rxjs' Hopefully this automatically resolves because rxjs is part of Angular. Likewise, you might also have to import {mergeMap, delay, mapTo, tap} from 'rxjs/operators' or something, but I'm unable to access StackBlitz to confirm for you.

Reload HTML elements with javascript not working

I get a list of items and use it to dynamically create an HTML list
_loadList(){
HttpUtils.get('http://myserver/list/users/')
.then((res) => {
const self = this;
res.forEach((item) => {
userListContainer.append('<li> item.name </li>')
});
});
}
I call this function in the constructor, everything is working fine
constructor() {
this._loadList();
}
I am trying to recall this function every 5 seconds to update the list with the new result:
constructor() {
const that = this;
this._loadList();
window.setInterval(function(){
that._loadList();
}, 5000);
}
The function is called, the received result contains the new content, but the HTML is not updated. Do you have an idea about the problem?
You can try below code that will work for in your case. You can checkout https://es6console.com/jgyxgm1f/ example which will alert random number (in your case it's equivalent of adding new data coming from API response).
_loadList = () => {
HttpUtils.get('http://myserver/list/users/')
.then((res) => {
userListContainer.empty();
res.forEach((item) => {
userListContainer.append('<li> item.name </li>')
});
});
}
constructor = () => {
this._loadList();
window.setInterval(() => {
this._loadList();
}, 5000);
}

Unsubscribe from Redux store when condition is true?

I'm employing the suggestion from #gaearon to setup a listener on my redux store. I'm using this format:
function observeStore(store, select, onChange) {
let currentState;
if (!Function.prototype.isPrototypeOf(select)) {
select = (state) => state;
}
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState);
}
}
let unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
I'm using this in an onEnter handler for a react-router route:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
const disposeRouteHandler = observeStore(store, null, (state) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
// I'm done: how do I dispose the store subscription???
}
});
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
Basically this helps gate the progression of the router while actions are finishing dispatching (async).
My problem is that I can't figure out where to call disposeRouteHandler(). If I call it right after the definition, my onChange function never gets a chance to do it's thing, and I can't put it inside the onChange function because it's not defined yet.
Appears to me to be a chicken-egg problem. Would really appreciate any help/guidance/insight.
How about:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
let shouldDispose = false;
const disposeRouteHandler = observeStore(store, null, (state) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
if (disposeRouteHandler) {
disposeRouteHandler();
} else {
shouldDispose = true;
}
}
});
if (shouldDispose) {
disposeRouteHandler();
}
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
Even though using the observable pattern leads to some buy-in, you can work around any difficulties with normal js code. Alternatively you can modify your observable to suit your needs better.
For instance:
function observeStore(store, select, onChange) {
let currentState, unsubscribe;
if (!Function.prototype.isPrototypeOf(select)) {
select = (state) => state;
}
function handleChange() {
let nextState = select(store.getState());
if (nextState !== currentState) {
currentState = nextState;
onChange(currentState, unsubscribe);
}
}
unsubscribe = store.subscribe(handleChange);
handleChange();
return unsubscribe;
}
and
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
const disposeRouteHandler = observeStore(store, null, (state, disposeRouteHandler) => {
const conditions = [
isLoaded(state.thing1),
isLoaded(state.thing2),
isLoaded(state.thing3),
];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
disposeRouteHandler();
}
}
store.dispatch(
entities.getOrCreate({
entitiesState: store.getState().entities,
nextState,
})
);
};
};
It does add a strange argument to onChange but it's just one of many ways to do it.
The core problem is that handleChange gets called synchronously immediately when nothing has changed yet and asynchronously later. It's known as Zalgo.
Inspired by the suggestion from #DDS, I came up with the following alteration to the other pattern mentioned in #gaearon's comment:
export function toObservable(store) {
return {
subscribe({ onNext }) {
let dispose = this.dispose = store.subscribe(() => {
onNext.bind(this)(store.getState())
});
onNext.bind(this)(store.getState());
return { dispose };
},
dispose: function() {},
}
}
This allows me to invoke like:
Entity.onEnter = function makeFetchEntity(store) {
return function fetchEntity(nextState, replace, callback) {
toObservable(store).subscribe({
onNext: function onNext(state) {
const conditions = [/* many conditions */];
if (conditions.every((test) => !!test) {
callback(); // allow react-router to complete routing
this.dispose(); // remove the store subscription
}
},
});
store.dispatch(/* action */);
};
};
The key difference is that I'm passing a regular function in for onNext so as not to interfere with my bind(this) in toObservable; I couldn't figure out how to force the binding to use the context I wanted.
This solution avoids
add[ing] a strange argument to onChange
... and in my opinion also conveys a bit more intent: this.dispose() is called from within onNext, so it kinda reads like onNext.dispose(), which is exactly what I want to do.

Categories