Tips for resetting an RXJS scan operator depending on a different Observable

I created a component that triggers an onScrollEnd event once the last item in a virtual list is displayed. This event initiates a new API request to fetch the next page and combine it with the previous results using the scan operator.

In addition, this component features a search field that triggers an onSearch event.

Now, I am facing an issue of clearing the previously accumulated results from the scan operator when a search event occurs. Do you have any suggestions on how to handle this situation effectively, or should I consider refactoring my current approach?

const loading$ = new BehaviorSubject(false);
const offset$ = new BehaviorSubject(0);
const search$ = new BehaviorSubject(null);

const options$: Observable<any[]> = merge(offset$, search$).pipe(
  // 1. Start the loading indicator.
  tap(() => loading$.next(true)),
  // 2. Fetch new items based on the offset.
  switchMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
  // 3. Stop the loading indicator.
  tap(() => loading$.next(false)),
  // 4. Complete the Observable when there is no 'next' link.
  takeWhile((response) => response.links.next),
  // 5. Map the response.
  map(({ data }) =>
    data.map((user) => ({
      label: user.name,
      value: user.id
    }))
  ),
  // 6. Accumulate the new options with the previous options.
  scan((acc, curr) => {
    // TODO: Don't merge when search$.next is triggered
    return [...acc, ...curr]);
  }
);

// Fetch next page
onScrollEnd: (offset: number) => offset$.next(offset);
// Fetch search results
onSearch: (term) => {
  search$.next(term);
};

Answer №1

To alter the state of a scan, you can create higher order functions that take the old state and the new update as arguments. Utilize these functions with the merge operator to maintain a clean stream-based solution devoid of side effects.

const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;

add$ = new Subject();
clear$ = new Subject();

add = (value) => (state) => [...state, value];
clear = () => (state) => [];

const result$ = merge(
  add$.pipe(map(add)),
  clear$.pipe(map(clear))
).pipe(
  scan((state, innerFn) => innerFn(state), [])
)

result$.subscribe(result => console.log(...result))

add$.next(1)
add$.next(2)
clear$.next()
add$.next(3)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>

This approach is easily customizable and expandable for various other state scenarios in RxJS.

Example (remove last item)

removeLast$ = new Subject()

removeLast = () => (state) => state.slice(0, -1);

merge(
  ..
  removeLast$.pipe(map(removeLast)),
  ..
)

Answer №2

By adjusting the structure of your chain, you can accomplish your desired outcome without the need for tap calls that trigger loading:

search$.pipe(
  switchMap(searchterm =>
    concat(
      userService.getUsers(0, searchterm),
      offset$.pipe(concatMap(offset => userService.getUsers(offset, searchterm)))),
    ).pipe(
      map(({ data }) => data.map((user) => ({
        label: user.name,
        value: user.id
      }))),
      scan((acc, curr) => [...acc, ...curr], []),
    ),
  ),
);

Each emission from search$ will generate a new inner Observable with its own scan function that initializes with an empty accumulator.

Answer №3

Successfully discovered a solution: I validate the current position by employing withLatestFrom prior to the implementation of the scan operator, adjusting the accumulator as required based on this evaluation.

Live demonstration on Example.com

Answer №4

The concept of this stream is quite intriguing. Upon reflection, it appears that offset$ and search$ operate as distinct streams with differing logics, suggesting that they should be merged at the conclusion rather than the inception.

Furthermore, I believe that initiating a search should reset the offset to zero, a feature which seems absent in the current implementation.

Here is a proposed solution:

const offsettedOptions$ = offset$.pipe(
    tap(() => loading$.next(true)),    
    withLatestFrom(search$),
    concatMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
    tap(() => loading$.next(false)),
    map(({ data }) =>
    data.map((user) => ({
      label: user.name,
      value: user.id
    })),
    scan((acc, curr) => [...acc, ...curr])
);

const searchedOptions$ = search$.pipe(
    tap(() => loading$.next(true)),
    concatMap(searchTerm => userService.getUsers(0, searchterm)),
    tap(() => loading$.next(false)),
    map(({ data }) =>
    data.map((user) => ({
      label: user.name,
      value: user.id
    })),
);

const options$ = merge(offsettedOptions, searchedOptions);

Please assess whether this approach proves effective or coherent. It is possible that crucial details have been overlooked.

Answer №5

Although it may be considered outdated, I found an alternative solution that I think adds value to the discussion.

Essentially, there are only two user-triggered actions:

const search$ = new Subject<string>();
const offset$ = new Subject<number>();

The pivotal point is when search$ emits, so we want offset$ to reset to 0 at that moment. Here's how I would approach it:

const results$ = search$.pipe( 
  switchMap((searchTerm) => {
    return offset$.pipe(
      startWith(0),  
      switchMap((offset) => {
        return userService.getUsers(offset, searchTerm))
      })
    )
  }))

By resetting the offset upon search$ emission, we ensure a fresh API call for the desired resources whenever offset$ changes. For resetting the collection on search$ emissions, wrapping the offset$ pipe inside switchMap seems appropriate.

const results$ = search$.pipe( 
  switchMap((searchTerm) => {
    return offset$.pipe(
      startWith(0),  
      switchMap((offset) => {
        return userService.getUsers(offset, searchTerm))
      }),
      takeWhile((response) => response.links.next),
      map(({ data }) => 
        data.map((user) => ({
          label: user.name,
          value: user.id
        }))
      ),
      scan((list, response) => {
        return [  
          ...list,
          ...response
        ]
      }, [])
    )
  }))

An interesting aspect is that the scan operation resets with each new search$ emission.

In the final adjustment, I suggest moving loading$ outside of tap and declaring it separately. The revised code should resemble this:

const search$ = new Subject<string>();
const offset$ = new Subject<number>();
let results$: Observable<{label: string; value: string}[]>;

results$ = search$.pipe( 
  switchMap((searchTerm) => {
    return offset$.pipe(
      startWith(0),  
      switchMap((offset) => {
        return userService.getUsers(offset, searchTerm))
      }),
      takeWhile((response) => response.links.next),
      map(({ data }) => 
        data.map((user) => ({
          label: user.name,
          value: user.id
        }))
      ),
      scan((list, response) => {
        return [  
          ...list,
          ...response
        ]
      }, [])
    )
  }));

const loading$ = merge(
  search$.pipe(mapTo(true)), 
  offset$.pipe(mapTo(true)),  
  results$.pipe(mapTo(false)),
);

results$.subscribe((results) => {
  console.log(results);
})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieve information from a variety of selected checkboxes

Is there a way to retrieve the values of check boxes that are generated dynamically? @ $db = mysql_connect("abc", "abc", ""); mysql_select_db("abc"); $strSQL = "SELECT * FROM student"; ...

Are there any other ways to write code in a Functional style within Angular?

From what I understand, there are several types of Angular v17 code that can be implemented in a Functional way: Functional HTTP Interceptors Functional Route Guards Functional Validator (Reactive forms) I'm interested to know if there are any addit ...

Step-by-step guide to creating a custom wrapper in React that modifies the props for a component

Exploring React components for the first time and seeking assistance. I am interested in dynamically wrapping one component inside another and modifying its props. For instance, considering the following component: If we want to pass the key3 from a wrapp ...

Material-UI is having trouble resolving the module '@material-ui/core/styles/createMuiTheme'

Although I have searched for similar issues on StackOverflow, none of the solutions seem to work for me. The errors I am encountering are unique and so are the fixes required, hence I decided to create a new post. The company conducting my test provided m ...

When using ngFor in HTML, the ID of an object within an array of objects can become undefined during execution

Currently, I am in the process of developing a mobile application with the Ionic framework. One of the key functionalities of this app involves making an API call to retrieve transaction data, which then needs to be displayed in the HTML template: The dat ...

Deciphering the hidden power of anonymous functions within Express.js

Recently, I started learning about express and am grappling with understanding callbacks in RESTful actions. In the following PUT request code snippet, I am puzzled by the specific line that is highlighted below. Why is response.pageInfo.book being assigne ...

Performance issues with Datatables server side processing

Utilizing Datatables server-side processing with PHP, JQuery, Ajax, and SQL Server database, I encountered slow performance in features such as pagination and search. Despite working with moderate data, there is a delay of over 40 seconds when using the se ...

Is it necessary to 'type assert' the retrieved data in Axios if I have already specified the return type in the function declaration?

Consider the code snippet below: import axios from 'axios' async function fetchAPI<T>(path: string, data: any): Promise<T> { return (await axios.get(path, data)).data as T } async function getSomething(): Promise<SomeType> { ...

Having trouble with the functionality of the React Infinite Scroll component

Here is how I am implementing the react-infinite scroll component: const [hasMore, setHasMore] = useState(true) const [pageNumber, setPageNumber] = useState(1) const fetchDataOnScroll = async () => { try { const res = await axios.get( ...

Troubles with Custom Control Component: ControlValueAccessor and Validator Out of Sync with Form Group

Background: My custom email control component, EmailControlComponent, is designed to implement both ControlValueAccessor and Validator. The validate() method of this component takes a single parameter of type AbstractControl. As specified in Angular' ...

What is the method to obtain the keycode for a key combination in JavaScript?

$(document).on('keydown', function(event) { performAction(event); event.preventDefault(); }); By using the code above, I am successful in capturing the keycode for a single key press. However, when attempting to use a combin ...

Search through a group of distinct objects to find arrays nested within each object, then create a new object

I am currently working with objects that contain arrays that I need to filter. My goal is to filter an array of classes based on category and division, then return the new object(s) with the filtered arrays. Below is a representation of the JSON structure ...

The date of posting will always be '0000-00-00 00:00:00'

I'm experiencing an issue with my JavaScript code for writing reviews. Previously, it worked fine, but now the 'datePosted' column consistently outputs the default '0000-00-00 00:00:00'. writeReview(request, respond) { va ...

Steps to verify the current time and execute a task if it matches the designated time

After successfully implementing a time function which changes the value of an input text based on a specific time, I encountered an issue. Although the time function is designed to change the input text value to 1 when the time reaches 2:30:35 PM... if(b ...

Dynamically Exporting Node Modules

// custom-exports.js exports.createCustomExports = (exportObject) => { module.exports = exportObject for (const exportItem in exportObject) { exports[exportItem] = exportObject[exportItem] } } // calculations.js const { createCustomExports } = ...

A practical guide to effectively mocking named exports in Jest using Typescript

Attempting to Jest mock the UserService. Here is a snippet of the service: // UserService.ts export const create = async body => { ... save data to database ... } export const getById = async id => { ... retrieve user from database ... } The ...

Encountering issue with building/deploying a node application on Red Hat OpenShift due to the error message "Unable to resolve output image"

After linking my GitHub repo to an OpenShift app and adding the necessary properties to package.json, I encountered an issue with the Builds section. The status message reads "Output image could not be resolved" and it shows that the build has not starte ...

What is the best way to incorporate callback/await into my code?

After spending a considerable amount of time on my code, I've been struggling to incorporate callbacks, awaits, or any necessary elements with no success. The main query is, how can I delay the response until I receive callbacks from both functions? ...

convert an array of hexadecimal colors into an array of RGB colors

I am trying to convert an array of hex colors to RGB values using a custom function. The array looks like this: var hexColors = [ "#ffffff", "#ffffff", "#ffffff", "#f3f3f3", "#f3f3f3", "#f3f3f3"]; The desired output is: var rgbCo ...

Tips on effectively organizing information retrieved from an XML file?

I would like to organize the data in ascending order based on the DATE after making this ajax call function parseXML(xml){ var weatherArray = []; var weatherElement = xml.getElementsByTagName("forecast")[0]; weatherArray.queryTime = weatherEl ...