RXJS observables cancel mouseover stream after mouseout event

Viewed 216

Trying to implement an rxjs observable pipeline in React.

Codesandbox example -> https://codesandbox.io/s/optimistic-fog-xtdi4

Creating a html element (div) in React, and binding mouseover and mouseout events.

React Code to initialize this:

   useEffect(() => {
    const elMouseOut$ = fromEvent(elRef.current, "mouseout").subscribe(() =>
      setState({ hover: "mouseout", eventType: "mouseout" })
    );

    const elMouseOver$ = fromEvent(elRef.current, "mouseover")
      .pipe(debounceTime(2000))
      .subscribe(() =>
        setState({ hover: "mouseover", eventType: "mouseover" })
      );

    return () => {
      elMouseOver$.unsubscribe();
      elMouseOut$.unsubscribe();
    };
  }, []);

Acceptance criteria Steps.

  1. When I mouse over, after a delay of 2 seconds i need to make a state update.
  2. If I mouse out at any time BEFORE the 2 second delay I need to cancel the mouseover stream, and set some state. (THIS STEP IS BROKEN)
  3. If I mouse out any time AFTER the 2 second delay, I need to set some state.
  4. This should be repeatable, in the sense that if i mouse over, mouse out and mouse over again we start from Step 1.

I have tried several different combinations of operators, but cannot get things working as expected.

Note: If the mouse over debounce time is met, an ajax request is made.

Any help/advice would be appreciated, i simply do not have the rxjs chops to sort this one out.

2 Answers

CodeSandbox solution.


I think this could be a way to solve it:

const sub = merge(
  elMouseOver$.pipe(
    // Step 4)
    // Repeating the steps with the help of `switchMap`
    switchMap(() =>
      timer(2000).pipe(
        // Step 2).
        // Cancelling the stream and setting some state(have a look at '3)', below).
        takeUntil(elMouseOut$),

        // Step 1).
        // This will be invoked if 2 seconds have passed after the mouseover event without
        // the mouseout event taking place.
        tap(() => setState({ hover: "mouseover", eventType: "mouseover" })),
      )
    )
  ),
  // Step 3).
  // Setting the state.
  elMouseOut$.pipe(
    tap(() => setState({ hover: "mouseout", eventType: "mouseout" }))
  )
).subscribe();

Also, because we're subscribing to elMouseOut$ multiple times, you might want to use the share() operator so that only one event listener will be added:

const elMouseOut$ = fromEvent(elRef.current, "mouseout").pipe(
  share(),
);

This should do the job:

useEffect(() => {
    const elMouseOver$ = fromEvent(elRef.current, "mouseover");
    const elMouseOut$  = fromEvent(elRef.current, "mouseout");

    const mouseoutSub = elMouseOver$.pipe(
        debounceTime(2000),
        tap(() => setState({ hover: 'mouseover', eventType: 'mouseover' })),
        switchMap(() => elMouseOut$),
        tap(() => setState({ hover: 'mouseout', eventType: 'mouseout' })),
        takeUntil(scheduled(elMouseOut$, asyncScheduler)),
        repeat()
    );

    return () => mouseoutSub.unsubscribe();
}, []);

Flow:

Since switchMap and takeUntil are using the same source, I used scheduled inside the takeUntil so that the switchMap would have a chance to emit before the observable was completed.


Here's a StackBlitz that demonstrates this behavior.

Related