I have infinite stream of events that can emit some consecutive event portions and I want to take one event per 1000 each milliseconds.
I tried debounceTime / auditTime / throttleTime but they doesn't include all events I want - to demonstrate the behavior I created playground on stackblitz which fires events once per 300ms within portion of 10 events:
debounceTime(1000) will give only event #10
throttleTime(1000) will give events 1,5,9 but it will omit #10
which is essential
auditTime(1000) will give events 4,8
What I want here is to get events 1,5,9,10 (one event per 1000ms interval). How do I achieve this?
const events$ = interval(300).pipe(
map(num => `Firing event ${num + 1}`)
);
const source = events$.pipe(
tap(console.log),
take(10),
// make some debouncing??
map(x => `Received ${x}!`)
);
source.subscribe(x =>
console.log(
"%c" + x,
"background: purple; color: white; padding: 3px 5px; border-radius: 3px;"
)
);
I also tried to play with zip / combineLatest and emitting values via interval but no luck with that
UPDATED
based on the discussion in comments
const events$ = timer(0, 6000).pipe(
take(3),
switchMap(x =>
timer(0, 300).pipe(
map(num => `event #${num + 1}`),
take(x > 1 ? 9 : 10)
)
)
);
const source = merge(
events$.pipe(
tap(e => console.log(`%cStream: ${e}`, "color: blue;")),
debounceTime(1000),
tap(x => console.log(`%cdebounceTime captured: ${x}`, "color: red;"))
),
events$.pipe(
throttleTime(1000),
tap(x => console.log(`%cthrottleTime captured: ${x}`, "color: green;"))
),
).pipe(
// we need to avoid duplicates (like case with 9).
// if all events aren't unique you need to use the original solution below.
distinctUntilChanged(), // <-- if all events are unique.
map(x => `Received ${x}!`)
);
source.subscribe(x =>
console.log(
"%c" + x,
"background: purple; color: white; padding: 3px 5px; border-radius: 3px;"
)
);
ORIGINAL
I hope that's what you wanted: https://take.ms/VP7tA
const events$ = interval(300).pipe(
map(num => `Firing event ${num + 1}`)
);
const source = concat(events$.pipe(
tap(console.log),
take(10),
), timer(1000).pipe(switchMapTo(EMPTY)), events$.pipe(
tap(console.log),
take(10),
));
let lastTimer = 0;
const last$ = new Subject<number>();
merge(
source.pipe(
scan((state, event) => {
state[1] = null;
const stamp = new Date().getTime();
clearTimeout(lastTimer);
if (stamp - state[0] < 1000) {
lastTimer = setTimeout(() => last$.next(event), (stamp - state[0]) + 50);
return state;
}
state[0] = stamp;
state[1] = event;
return state;
}, [0, null]),
filter(([, event]) => event !== null),
map(([, event]) => event || 0),
),
last$,
).pipe(
map(x => `Received ${JSON.stringify(x)}!`)
).subscribe(x =>
console.log(
"%c" + x,
"background: purple; color: white; padding: 3px 5px; border-radius: 3px;"
)
);
Here's one approach to this:
const events$ = interval(300).pipe(
map(num => `Firing event ${num + 1}`),
share(),
);
const source$ = events$.pipe(
take(10),
);
const lastValue$ = source$.pipe(last());
merge(source$.pipe(throttleTime(1000)), lastValue$).subscribe(console.log);
share() will make sure the source(the observable produced interval by) is subscribed only once
last() will return the latest nexted value when its source(source$ in this case) completes due to take(10)
EDIT
Based on my understanding of the problem, this would be another alternative:
const events$ = merge(
timer(300), // 1
timer(600), // 2
timer(900), // 3
timer(1301), // 4
timer(1500), // 5
timer(1800), // 6
timer(2302), // 7
).pipe(
map((_, num) => `Firing event ${num + 1}`),
share(),
);
const lastValue$ = events$.pipe(debounceTime(1000));
const source$ = events$.pipe(
throttleTime(1000),
buffer(lastValue$)
)
.subscribe(console.log)
debounceTime(1000) - if 1s passed without any notifications
buffer(lastValue$) - when lastValue$ emits, send the collected values
StackBlitz
Related
I added a start, stop, pause button. Start will start a count down timer which will start from a value, keep decrementing until value reaches 0. We can pause the timer on clicking the pause button. On click of Stop also timer observable completes.
However, once the timer is completed ( either when value reaches 0 or
when clicked on stop button ), I am not able to start properly. I
tried adding repeatWhen operator. It starts on clicking twice. Not at
the first time.
Also, at stop, value is not resetting back to the initial value.
const subscription = merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
tap(val => {
console.log(val);
}),
switchMap(val => (val ? interval(10).pipe(takeUntil(stopClick$)) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
repeatWhen(() => startClick$),
startWith(startValue)
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Stackblitz Code link is available here
This is a pretty complicated usecase. There are two issues I think:
You have two subscriptions to startClick$ and the order of subscriptions matters in this case. When the chain completes repeatWhen is waiting for startClick$ to emit. However, when you click the button the emission is first propagated into the first subscription inside merge(...) and does nothing because the chain has already completed. Only after that it resubscribes thanks to repeatWhen but you have to press the button again to trigger the switchMap() operator.
When you use repeatWhen() it'll resubscribe every time the inner Observable emits so you want it to emit on startClick$ but only once. At the same time you don't want it to complete so you need to use something like this:
repeatWhen(notifier$ => notifier$.pipe(
switchMap(() => startClick$.pipe(take(1))),
)),
So to avoid all that I think you can just complete the chain using takeUntil(stopClick$) and then immediatelly resubscribe with repeat() to start over.
merge(
startClick$.pipe(mapTo(true)),
pauseBtn$.pipe(mapTo(false))
)
.pipe(
switchMap(val => (val ? interval(10) : EMPTY)),
mapTo(-1),
scan((acc: number, curr: number) => acc + curr, startValue),
takeWhile(val => val >= 0),
startWith(startValue),
takeUntil(stopClick$),
repeat(),
)
.subscribe(val => {
counterDisplayHeader.innerHTML = val.toString();
});
Your updated demo: https://stackblitz.com/edit/rxjs-tum4xq?file=index.ts
Here's an example stopwatch that counts up instead of down. Perhaps you can re-tool it.
type StopwatchAction = "START" | "STOP" | "RESET" | "END";
function createStopwatch(
control$: Observable<StopwatchAction>,
interval = 1000
): Observable<number>{
return defer(() => {
let toggle: boolean = false;
let count: number = 0;
const ticker = timer(0, interval).pipe(
map(x => count++)
);
const end$ = of("END");
return concat(
control$,
end$
).pipe(
catchError(_ => end$),
switchMap(control => {
if(control === "START" && !toggle){
toggle = true;
return ticker;
}else if(control === "STOP" && toggle){
toggle = false;
return EMPTY;
}else if(control === "RESET"){
count = 0;
if(toggle){
return ticker;
}
}
return EMPTY;
})
);
});
}
Here's an example of this in use:
const start$: Observable<StopwatchAction> = fromEvent(startBtn, 'click').pipe(mapTo("START"));
const reset$: Observable<StopwatchAction> = fromEvent(resetBtn, 'click').pipe(mapTo("RESET"));
createStopwatch(merge(start$,reset$)).subscribe(seconds => {
secondsField.innerHTML = seconds % 60;
minuitesField.innerHTML = Math.floor(seconds / 60) % 60;
hoursField.innerHTML = Math.floor(seconds / 3600);
});
You can achieve that in another way without completing the main observable or resubscribing to it using takeUntil, repeatWhen, or other operators, like the following:
create a simple state to handle the counter changes (count, isTicking)
merge all the observables that affecting the counter within one observable.
create intermediate observable to interact with the main merge observable (start/stop counting).
interface CounterStateModel {
count: number;
isTicking: boolean;
}
// Setup counter state
const initialCounterState: CounterStateModel = {
count: startValue,
isTicking: false
};
const patchCounterState = new Subject<Partial<CounterStateModel>>();
const counterCommands$ = merge(
startClick$.pipe(mapTo({ isTicking: true })),
pauseBtn$.pipe(mapTo({ isTicking: false })),
stopClick$.pipe(mapTo({ ...initialCounterState })),
patchCounterState.asObservable()
);
const counterState$: Observable<CounterStateModel> = counterCommands$.pipe(
startWith(initialCounterState),
scan(
(counterState: CounterStateModel, command): CounterStateModel => ({
...counterState,
...command
})
),
shareReplay(1)
);
const isTicking$ = counterState$.pipe(
map(state => state.isTicking),
distinctUntilChanged()
);
const commandFromTick$ = isTicking$.pipe(
switchMap(isTicking => (isTicking ? timer(0, 10) : NEVER)),
withLatestFrom(counterState$, (_, counterState) => ({
count: counterState.count
})),
tap(({ count }) => {
if (count) {
patchCounterState.next({ count: count - 1 });
} else {
patchCounterState.next({ ...initialCounterState });
}
})
);
const commandFromReset$ = stopClick$.pipe(mapTo({ ...initialCounterState }));
merge(commandFromTick$, commandFromReset$)
.pipe(startWith(initialCounterState))
.subscribe(
state => (counterDisplayHeader.innerHTML = state.count.toString())
);
Also here is the working version:
https://stackblitz.com/edit/rxjs-o86zg5
Suppose that you have a function that returns an rxjs observable that contains a list of objects.
const getItems = () =>
of([
{
id: 1,
value: 10
},
{
id: 2,
value: 20
},
{
id: 3,
value: 30
}
]);
and a second function that returns an observable with a single object
const getItem = id =>
of({
id,
value: Math.floor(Math.random() * 30) + 1
});
Now we want to create an observable that will get the first list and at a regular interval will randomly update any list item.
const source = getItems().pipe(
switchMap(items =>
interval(5000).pipe(
switchMap(x => {
// pick up a random id
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
)
);
})
)
)
);
source.subscribe(x => console.log(JSON.stringify(x)));
The problem with the above code is that each time the interval is triggered the items from the previous iteration reset to their initial form. e.g,
[{"id":1,"value":10},{"id":2,"value":13},{"id":3,"value":30}]
[{"id":1,"value":10},{"id":2,"value":20},{"id":3,"value":18}]
[{"id":1,"value":10},{"id":2,"value":16},{"id":3,"value":30}]
[{"id":1,"value":21},{"id":2,"value":20},{"id":3,"value":30}]
As you see, on each interval our code is resetting the list and updates a new item (eg value 13 is lost in the second iteration and reverts to 20).
The behaviour seems reasonable since the items argument in the first switchMap acts like a closure.
I managed to somehow solve the issue by using BehaviorSubject but i think that my solution is somehow dirty.
const items$ = new BehaviorSubject([]);
const source = getItems().pipe(
tap(items => items$.next(items)),
switchMap(() =>
interval(5000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId).pipe(
map(item =>
items$
.getValue()
.reduce(
(acc, cur) =>
cur.id === item.id ? [...acc, item] : [...acc, cur],
[]
)
),
tap(items => items$.next(items)),
switchMap(() => items$)
);
})
)
)
);
Is there a better approach ?
Example code can be found here
I believe this should be doing what you want:
const source = getItems().pipe(
switchMap(items =>
interval(1000).pipe(
switchMap(() => {
const rId = Math.floor(Math.random() * 3) + 1;
return getItem(rId);
}),
scan((acc, item) => {
acc[acc.findIndex(i => i.id === item.id)] = item;
return acc;
}, items),
)
)
);
It's basically what you're doing but I'm using scan (that is initialized with the original items) to keep the output array in acc so I can update it later again.
Live demo: https://stackblitz.com/edit/rxjs-kvygy1?file=index.ts
Here is my useEffect Call:
const ref = useRef(null);
useEffect(() => {
const clickListener = (e: MouseEvent) => {
if (ref.current.contains(e.target as Node)) return;
closePopout();
}
document.addEventListener('click', clickListener);
return () => {
document.removeEventListener('click', clickListener);
closePopout();
}
}, [ref, closePopout]);
I'm using this to control a popout menu. When you click on the menu icon to bring up the menu it will open it up. When you click anywhere that isn't the popout it closes the popout. Or when the component gets cleaned up it closes the popout as well.
I'm using #testing-library/react-hooks to render the hooks:
https://github.com/testing-library/react-hooks-testing-library
We are also using TypeScript so if there is any TS specific stuff that would be very helpful as well.
Hopefully this is enough info. If not let me know.
EDIT:
I am using two companion hooks. I'm doing quite a bit in it and I was hoping to simplify the question but here is the full code for the hooks. The top hook (useWithPopoutMenu) is called when the PopoutMenu component is rendered. The bottom one is called inside the body of the PopoutMenu component.
// for use when importing the component
export const useWithPopoutMenu = () => {
const [isOpen, setIsOpenTo] = useState(false);
const [h, setHorizontal] = useState(0);
const [v, setVertical] = useState(0);
const close = useCallback(() => setIsOpenTo(false), []);
return {
isOpen,
menuEvent: {h, v, isOpen, close} as PopoutMenuEvent,
open: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
setIsOpenTo(true);
setHorizontal(e.clientX);
setVertical(e.clientY);
},
close
};
}
type UsePopoutMenuArgs = {
menuEvent: PopoutMenuEvent
padding: number
tickPosition: number
horizontalFix: number | null
verticalFix: number | null
hPosition: number
vPosition: number
borderColor: string
}
// for use inside the component its self
export const usePopoutMenu = ({
menuEvent,
padding,
tickPosition,
horizontalFix,
verticalFix,
hPosition,
vPosition,
borderColor
}: UsePopoutMenuArgs) => {
const ref = useRef() as MutableRefObject<HTMLDivElement>;
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (ref.current.contains(e.target as Node)) return;
menuEvent.close();
}
document.addEventListener('click', handleClick);
return () => {
document.removeEventListener('click', handleClick);
menuEvent.close();
}
}, [menuEvent.close, ref]);
const menuContainerStyle = useMemo(() => {
const left = horizontalFix || menuEvent.h;
const top = verticalFix || menuEvent.v;
return {
padding,
left,
top,
marginLeft: hPosition,
marginTop: vPosition,
border: `1px solid ${borderColor}`
}
}, [
padding,
horizontalFix,
verticalFix,
menuEvent,
hPosition,
vPosition,
borderColor
]);
const backgroundArrowStyle = useMemo(() => {
return {
marginLeft: `-${padding + 6}px`,
marginTop: 4 - padding + tickPosition,
}
},[padding, tickPosition]);
const foregroundArrowStyle = useMemo(() => {
return {
marginLeft: `-${padding + 5}px`,
marginTop: 4 - padding + tickPosition,
}
},[padding, tickPosition]);
return {
ref,
menuContainerStyle,
backgroundArrowStyle,
foregroundArrowStyle
}
}
Here is the component:
type PopoutMenuProps = {
children: React.ReactChild | React.ReactChild[] // normal props.children
menuEvent: PopoutMenuEvent
padding?: number // padding that goes around the
tickPosition?: number // how far down the tick is from the top
borderColor?: string // border color
bgColor?: string // background color
horizontalFix?: number | null
verticalFix?: number | null
vPosition?: number
hPosition?: number
}
const Container = styled.div`
position: fixed;
display: block;
padding: 0;
border-radius: 4px;
background-color: white;
z-index: 10;
`;
const Arrow = styled.div`
position: absolute;
`;
const PopoutMenu = ({
children,
menuEvent,
padding = 16,
tickPosition = 10,
borderColor = Style.color.gray.medium,
bgColor = Style.color.white,
vPosition = -20,
hPosition = 10,
horizontalFix = null,
verticalFix = null
}: PopoutMenuProps) => {
const binding = usePopoutMenu({
menuEvent,
padding,
tickPosition,
vPosition,
hPosition,
horizontalFix,
verticalFix,
borderColor
});
return (
<Container ref={binding.ref} style={binding.menuContainerStyle}>
<Arrow style={binding.backgroundArrowStyle}>
<Left color={borderColor} />
</Arrow>
<Arrow style={binding.foregroundArrowStyle}>
<Left color={bgColor} />
</Arrow>
{children}
</Container>
);
}
export default PopoutMenu;
Usage is something like this:
const Parent () => {
const popoutMenu = useWithPopoutMenu();
return (
...
<ComponentThatOpensThePopout onClick={popoutMenu.open}>...
...
{popoutMenu.isOpen && <PopoutMenu menuEvent={menuEvent}>PopoutMenu Content</PopoutMenu>}
);
}
Do you need to test the hook in isolation?
Testing the component that consumes the hook would be much easier and it would also be a more realistic test, pseudo code below:
render(<PopoverConsumer />);
userEvent.click(screen.getByRole('button', { name: 'Menu' });
expect(screen.getByRole('dialog')).toBeInTheDocument();
userEvent.click(screen.getByText('somewhere outside');
expect(screen.getByRole('dialog')).not.toBeInTheDocument();
How can I share the startWith(false) state between the 3 streams? I tried using withLatestFrom() but got some weird errors for the value.
const Home = componentFromStream(prop$ => {
const { handler: toggleHandler, stream: toggle$ } = createEventHandler();
const { handler: showHandler, stream: show$ } = createEventHandler();
const { handler: hideHandler, stream: hide$ } = createEventHandler();
const modal$ = merge(
toggle$.pipe(
startWith(false),
map(() => prev => !prev),
scan((state, changeState: any) => changeState(state))
),
show$.pipe(
startWith(false),
map(() => prev => true),
scan((state, changeState: any) => changeState(state))
),
hide$.pipe(
startWith(false),
map(() => prev => false),
scan((state, changeState: any) => changeState(state))
)
);
return combineLatest(prop$, modal$).pipe(
map(([props, modal]) => {
console.log(modal);
return (
<div>
<button onClick={toggleHandler}>Toggle</button>
<button onClick={showHandler}>Show</button>
<button onClick={hideHandler}>Hide</button>
<h1>{modal ? 'Visible' : 'Hidden'}</h1>
</div>
);
})
);
});
In the example, the toggle doesn't respect the current value of show or hide, but only of its own latest value.
https://stackblitz.com/edit/react-rxjs6-recompose
In order to do this, you will need to manage state a bit differently, something similar to what people do in redux. Take a look at example:
const { of, merge, fromEvent } = rxjs; // = require("rxjs")
const { map, scan } = rxjs.operators; // = require("rxjs/operators")
const toggle$ = fromEvent(document.getElementById('toggle'), 'click');
const show$ = fromEvent(document.getElementById('show'), 'click');
const hide$ = fromEvent(document.getElementById('hide'), 'click');
const reduce = (state, change) => change(state);
const initialState = false;
const state$ = merge(
of(e => e),
toggle$.pipe(map(e => state => !state)),
show$.pipe(map(e => state => true)),
hide$.pipe(map(e => state => false)),
).pipe(
scan(reduce, initialState),
);
state$.subscribe(e => console.log('state: ', e));
<script src="https://unpkg.com/rxjs#6.2.2/bundles/rxjs.umd.min.js"></script>
<button id="toggle">Toggle</button>
<button id="show">Show</button>
<button id="hide">Hide</button>
To better understand how it works, take a look at Creating applications article from rxjs documentation
First having startWith on all those observables means they will all emit at the beginning false. Which is like you pushed all those buttons at once.
I think you should try to achieve different behaviour. Using startWith you want to set the initial state of the modal property, right? Therefore it should come after merging those stream together.
To toggle the value you need two things:
place where to store the state (usually accumulator of scan)
differentiate those button presses. You have three buttons therefore you need three values.
This is my approach:
const modal$ = merge(
show$.pipe(mapTo(true)),
hide$.pipe(mapTo(false)),
toggle$.pipe(mapTo(null))
).pipe(
startWith(false),
scan((acc, curr) => {
if (curr === null) {
return !acc;
} else {
return curr;
}
})
);
Show button always emits true, hide button emits false and toggle button emits null. I merge them together and we want to start with false. Next there is scan which holds the state in accumulator.
When null comes it returns negated state. When true or false comes it returns it - that way it sets the new state regardless the previous value.
I took everyone's approach & came up with this. It keeps the toggle() logic inside the map function, & uses startWith() to set the value.
const modal$ = merge(
toggle$.pipe(
map(() => prev => !prev),
),
show$.pipe(
map(() => prev => true),
),
hide$.pipe(
map(() => prev => false),
)
).pipe(
startWith(false),
scan((state, change) => change(state)),
);
How can I achive something like the following using RxJS?
a-b-c-d-e-f-g-h
ab---cd---ef---gh
or
a-b-c-d-e-f-g-h
abc----def----gh
I have an array which I need to split by specified partitions and emit values with specified interval.
Note you can't get 'ab--' from 'a-b-' because you must wait for 'b', so strictly the marble diagrams will be
a-b-c-d-e-f-g-h-|
-(ab)---(cd)---(ef)---(gh|)
or
a-b-c-d-e-f-g-h-|
--(abc)----(def)----(gh|)
console.clear()
const Observable = Rx.Observable
const timedEmitter = (observable, interval) =>
Observable.zip(observable, Observable.timer(0, interval))
.map(x => x[0])
const source = Observable.from(['a','b','c','d','e','f','g','h'])
const timedSource = timedEmitter(source, 1000)
const interval = 5000
const size = 3
const output = timedEmitter(timedSource.bufferCount(size), interval)
//Display
const start = new Date()
output.timestamp()
.map(x => { return {value: x.value, elapsed: x.timestamp - start} })
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>
Footnote, changing Observable.interval(interval) to Observable.timer(0, interval) in timedEmitter to get first emit asap.
Footnote#2, this is not quite right, because the complete() of source shorts the last interval.
This was just an artifact of the chosen intervals.
Here's a custom operator version using pipe() from RxJs 5.5, ref Build your own operators easily
console.clear()
const Observable = Rx.Observable
const timedEmitter = (observable, interval) =>
Observable.zip(observable, Observable.timer(0, interval))
.map(x => x[0])
const source = Observable.from(['a','b','c','d','e','f','g','h'])
const timedSource = timedEmitter(source, 1000)
// Custom operator
const timedBufferedEmitter = (interval, bufferSize) => (observable) =>
Observable.zip(observable.bufferCount(bufferSize), Observable.timer(0, interval))
.map(x => x[0])
const interval = 5000
const size = 3
const output = timedSource.pipe(timedBufferedEmitter(interval, size))
//Display
const start = new Date()
output.timestamp()
.map(x => { return {value: x.value, elapsed: x.timestamp - start} })
.subscribe(console.log)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.2/Rx.js"></script>