Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent execution based on previously emitted value

Tags:

rxjs

If my typeahead gets an empty search result, any subsequent query with a norrowed down search query should be prevented. E.g. if the search for 'red' returns empty, a search for 'redcar' makes no sense.

I tried using pairwise() and scan() operator. Code snippet:

import { tap, switchMap, filter, pairwise, scan, map } from 'rxjs/operators';

this.searchForm.get('search').valueChanges
  .pipe(
    switchMap( queryString => this.backend.search(queryString))
  )
  .subscribe()

Update Given a simplified scenario: There is only the term 'apple' in the backend. The user is typing the search string (the request is not aborted by the switchMap()):

  1. 'a' -------> backend call returns 'apple'
  2. 'ap' ------> backend call returns 'apple'
  3. 'app' -----> backend call returns 'apple'
  4. 'appl' ----> backend call returns 'apple'
  5. 'apple' ---> backend call returns 'apple'
  6. 'apple p' -----> backend call returns EMPTY
  7. 'apple pi' ----> backend call returns EMPTY
  8. 'apple pie' ---> backend call returns EMPTY

The backend calls for 7. and 8. are unnecessary, because 6. already returns EMPTY. Therfore any subsequent call could be omitted. In my opinion some memoization is needed.

I would like to prevent unnecessary backend calls (http). Is there any way to achieve this in rxjs?

like image 534
Dennis Cieplik Avatar asked Apr 29 '26 13:04

Dennis Cieplik


1 Answers

This is an interesting use-case and one of a very few situations where mergeScan is useful.

Basically, you want to remember the previous search term and the previous remote call result and based on their combination you'll decide whether you should make another remote call or just return EMPTY.

import { of, EMPTY, Subject, forkJoin } from 'rxjs'; 
import { mergeScan, tap, filter, map } from 'rxjs/operators';

const source$ = new Subject();
// Returns ['apple'] only when the entire search string is contained inside the word "apple".
// 'apple'.indexOf('app') returns 0
// 'apple'.indexOf('apple ap') returns -1
const makeRemoteCall = (str: string) =>
  of('apple'.indexOf(str) === 0 ? ['apple'] : []).pipe(
    tap(results => console.log(`remote returns`, results)),
  );

source$
  .pipe(
    tap(value => console.log(`searching "${value}""`)),
    mergeScan(([acc, previousValue], value: string) => {
      // console.log(acc, previousValue, value);
      return (acc === null || acc.length > 0 || previousValue.length > value.length)
        ? forkJoin([makeRemoteCall(value), of(value)]) // Make remote call and remember the previous search term
        : EMPTY;
    }, [null, '']),
    map(acc => acc[0]), // Get only the array of responses without the previous search term
    filter(results => results.length > 0), // Ignore responses that didn't find any results
  )
  .subscribe(results => console.log('results', results));

source$.next('a');
source$.next('ap');
source$.next('app');
source$.next('appl');
source$.next('apple');
source$.next('apple ');
source$.next('apple p');
source$.next('apple pi');
source$.next('apple pie');

setTimeout(() => source$.next('app'), 3000);
setTimeout(() => source$.next('appl'), 4000);

Live demo: https://stackblitz.com/edit/rxjs-do457

Notice that after searching for "apple " there are no more remote calls. Also, after 3s when you try searching a different term "'app'" it does make a remote call again.

like image 134
martin Avatar answered May 01 '26 03:05

martin