How to continuously sample data based on clicks in RxJS? - javascript

I have an observable with interesting data that changes over time (BehaviorSubject):
const obs = ...
Then I have a click action (Subject which gets called .next() upon button press):
const clickAction = ...
I wish to do this:
obs.pipe(sample(clickAction)).subscribe(data => console.log(data))
The problem I have is this: when I click my button multiple times in a row, it only logs the message once to the console. If obs changes, then clicking again will once again log to the console.
Expected behavior: the data (whether it has changed or not) should also be logged every time I click the button.
I thought sample was supposed to do this, but I reckon there must be a better way to do this?

sample isn't working in your case because:
sample looks at the source Observable and emits whichever value it
has most recently emitted since the previous sampling, unless the
source has not emitted anything since the previous sampling.
withLatestFrom
Use withLatestFrom to combine click events with the latest emitted value from your source observable.
clickAction.pipe(
withLatestFrom(obs),
map(([notifier, source]) => source)
).subscribe(console.log)
https://stackblitz.com/edit/typescript-uxizci

I ended up implementing this:
export function upon<T>(notifier: Observable<any>): MonoTypeOperatorFunction<T> {
return (source: Observable<T>) => {
const latest = withLatestFrom(source)(notifier)
const mapper = map(([, source]: [any, T]) => source)
return mapper(latest)
}
}
Which can be used like so: obs.pipe(upon(clickAction))

Related

Submit button not giving required result

My submit button is not displaying output when i am pressing it but when i do a backspace it is giving me the output on the screen.
enter image description here
Post code instead of posting images
According to the image that you have provided, inside saveTask you are pushing the obj to the taskList and then passing it to the state updating function.
Arrays are of reference type and since here you are not passing a new array, react will figure out that the array hasn't changed (although you have appended the obj) and hence it won't trigger a re-render.
So change your code to this,
const saveTask = (taskObj) => {
setTaskList(prevList => {
prevList.push(taskObj)
return [...prevList]
})
}

Filtering Down Component Map using click function react

I am currently working on a fun project and I want to know how to do something that has stumped for a bit.
Basically I am using Axios to get my data and then rendering data out in a .map func then I have a click function to show only the data that is corresponding to the ID for example ID 1 has some values that I want to show in another component. How do I do that?
https://j99t7.csb.app/
If you see my sand box and click on one of the ids and see the console / code - this is where I am stuck at.
Cheers,
Dave :)
In order to filter the data, you can use something like:
const [filteredData, setFilteredData] = useState([]);
//onclick
setFilteredData(data.filter(element => element.id === id));
//jsx return
filteredData.map(filteredElement => {
//loop through elements and display data desired
})
Though I'm not a React Master, onClick doesn't return JSX or TSX.
i.e where would the returned value be rendered, in most cases, it used as a void function with no return value

how to pipe an observable without creating a new instance

Lets say i have a subject that represents pages in a paginateable table:
pageCount$
I then pipe that into a new variable which goes of to an API to get the data for the table:
const tableData$ = pageCount$.pipe(switchMap(pageCount => getTableData(pageCount)));
This works great, every time i emit a new page tableData$ emits the data for that page.
Now here comes the problem and the question i wish to solve.
On my page i also wish to use that table data to display averages of what it currently contains. So i thought i could just pipe tableData$ through a map that performs those averages.
const averageData$ = tableData$.pipe(map(data => performAverages(data)));
This works, but because every-time i subscribe to each variable it creates a new instance my API call happens twice. Once for the tableData$ subscription and once for the averageData$ subscription. I understand this behavior is by design however.
It feels like i want to use some sort of tap/fork operator but i don't think such an operator exists.
Is it even possible to perform these tasks whilst only making the api call once?
Thanks
You can use the share operator to achieve this.
First create the observable that calls the API, and pipe it with share
https://www.learnrxjs.io/learn-rxjs/operators/multicasting/share.
Then the resulting observable can be subscribed twice, both subscription will receive the same results, without the shared observable being called twice ('multicasting').
That should give you something along the lines of :
const tableData$ = pageCount$.pipe(
switchMap(pageCount => getTableData(pageCount)),
tap(_ => console.log('API called')),
share()
);
// subscribe to tabledata$ twice
tableData$.subscribe(_ => console.log('get and use data once'));
tableData$.subscribe(_ => console.log('get and use data a second time'));
(to be tested, feedback appreciated!)
What about something like this
const tableData$ = pageCount$.pipe(
switchMap(pageCount => getTableData(pageCount).pipe(
map(data =>
const avg = performAverages(data);
return {data, avg}
)
))
);
This way you get an object containing both table data and its average, which is what I understand you are looking for.

react register simultaneous setState (hooks)

I have a component that is supposed to listen for multiple key-down events.
const [activeKeys, setActiveKeys] = useState([]);
const onKeyDown = e => {
if (!activeKeys.includes(e.key)) {
console.log("keydown", e.key);
setActiveKeys([...activeKeys, e.key]);
}
};
const onKeyUp = e => {
console.log("keyup", e.key);
setActiveKeys([...activeKeys].filter(i => i !== e.key));
};
useEffect(() => {
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
});
is at the core. the entire code can be found here (codesandbox)
it works fine 90% of the time, however:
when 2 keys are released at the exact same time the state doesn't get updated twice.
(You can try to reproduce it in the codesandbox provided above, press any 2 keys simultaneously and then release them at the exact same time. Might take a few tries to get right, here is a gif of the issue happening and here's what it looks like (the number is the amount of keys pressed down, and the 's' is the key that's supposedly pressed down, however, you can see that that in the console log the keyup of 's' was registered.)
Has anyone encountered something similar before?
From what I can see the issue is not with
the event listener (console.log gets fired)
a rerender (you can try to press some other keys, the key will stay in the array)
that's why I'm assuming that the issue lies with the hook, however I have no way to explain why this is happening.
The problem is that you're using the simple form of setState(newValue) and that replaces your state with your new value. You have to use the functional form of setState( (prevState) => {} ); because your new state depends on the previous state.
Try this:
const onKeyDown = e => {
if (!activeKeys.includes(e.key)) {
console.log("keydown", e.key, activeKeys);
setActiveKeys(prevActiveKeys => [...prevActiveKeys, e.key]);
}
};
const onKeyUp = e => {
console.log("keyup", e.key, activeKeys);
setActiveKeys(prevActiveKeys =>
[...prevActiveKeys].filter(i => i !== e.key)
);
};
Link to Sandbox
Turns out that I just needed to replace useEffect by useLayoutEffect
from the docs:
The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
I've tested your codes and found it is more than just 2 keys, actually you can reproduce it more easily with more keys; try to press asdjkl (6 keys) at the same time and you can see even more mess.
And I think the problem is that, react's rerender cycle is not working with dom event listener, which means onKeyDown and onKeyUp fires whenever it wants, but react does not rerender for every triggering of them. Just log [...activeKeys, e.key] and [...activeKeys].filter(i => i !== e.key) and you will see it.
I think the solution is to use a simple local variable to update the activeKeys, then render method should use this variable instead of the result of the hook, and finally do a forceUpdate in every onKeyDown and onKeyUp to make react rerender.

switch Observable stream and grab subscription each next

I have an Observable listening to the URL and I am switching it to a getRows() which returns an Observable pulling data back from the API with the URL parameters. I want to be able get a Subscription reference for every emit that getRows() does. This is to show a loading indicator on the UI.
The current code:
this.tableSource = this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.map(p => this.getRows(p))
.switch()
.share();
And then explicitly when I have changed the parameters I have been calling:
this.tableLoad = this.tableSource.take(1).subscribe((r) => this.rows = this.parseRows(r));
But I want to enable the component to update when external entities manipulate the URL and so I should be subscribing instead of sharing tableSource, so how can I get a Subscription everytime I call getRows(), is it possible?
I managed to solve it this way:
this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.do(p => {
this.tableLoad = this.getRows(p).subscribe((r) => this.rows = this.parseRows(r));
})
.subscribe();
So I stopped trying to use both Observable sequences as one (even though one depends on the other), and just subscribed to the second as a side effect of the first.

Categories