Fetching data with the Infinite Scroll Timeline component
Imagine going through your favorite cooking web application and getting the latest updates on delicious new recipes. To show this latest news, one of the common UX patterns is to show this recipe news in a timeline component (such as Facebook’s news feed). While you scroll, if there are new recipes, you’ll be updated that there are fresh new recipes so that you can scroll back to the top and start over.
How to do it…
In this recipe, we’re going to build a timeline component that shows the list of your favorite latest cooking recipes. Since there are a lot of delicious recipes out there, this would be a huge list to fetch initially. To increase the performance of the application and to improve the general UX, we can implement an infinite scroll list so that once the user scrolls to the end of a list of 5 initial recipes, we can get a set of 5 new recipes. After some time, we can send a new request to check whether there are new recipes and refresh our timeline of recipe news.
Step 1 – Detecting the end of a list
In our RecipesList
component, we’ll create a stream of scroll events. On each emission, we’ll check whether we’re near the end of the list in the UI based on a certain threshold:
private isNearBottom(): boolean {
const threshold = 100; // Pixels from bottom
const position = window.innerHeight + window.scrollY;
const height = document.documentElement.scrollHeight;
return position > height - threshold;
}
const isNearBottom$ = fromEvent(window, 'scroll').pipe(
startWith(null),
auditTime(10), // Prevent excessive event triggering
observeOn(animationFrameScheduler),
map(() => this.isNearBottom()),
distinctUntilChanged(), // Emit only when near-bottom
//state changes
)
As you can imagine, with the scroll event emissions, there’s the potential for performance bottlenecks. We can limit the number of scroll events that are processed by the stream using the auditTime
operator. This is especially useful since we want to ensure that we are always processing the latest scroll event, and auditTime
will always emit the most recent value within the specified time frame. Also, with observeOn(animationFrameScheduler)
, we can schedule tasks to be executed just before the browser’s next repaint. This can be beneficial for animations or any updates that cause a repaint as it can help to prevent jank and make the application feel smoother.
auditTime versus throttleTime
You might be wondering why we used auditTime
in our scroll stream and not throttleTime
. The key difference between these two operators is that auditTime
emits the last value in a time window, whereas throttleTime
emits the first value in a time window. Common use cases for throttleTime
might include rate-limiting API calls, handling button clicks to prevent accidental double clicks, and controlling the frequency of animations.
Once we know we’re getting near the end of a list, we can trigger a loading state and the next request with a new set of data.
Step 2 – Controlling the next page and loading the state of the list
At the top of our RecipesList
component, we’ll define the necessary states to control the whole flow and know when we require the next page, when to show the loader, and when we’ve reached the end of the list:
private page = 0;
public loading$ = new BehaviorSubject<boolean>(false);
public noMoreData$ = new Subject<void>();
private destroy$ = new Subject<void>();
Now, we can continue our isNearBottom$
stream, react to the next page, and specify when to show the loader:
isNearBottom$.pipe(
filter((isNearBottom) =>
isNearBottom && !this.loading$.value),
tap(() => this.loading$.next(true)),
switchMap(() =>
this.recipesService.getRecipes(++this.page)
.pipe(
tap((recipes) => {
if (recipes.length === 0)
this.noMoreData$.next();
}),
finalize(() => this.loading$.next(false))
)
),
takeUntil(merge(this.destroy$, this.noMoreData$))
)
.subscribe((recipes) => (
this.recipes = [...this.recipes, ...recipes])
);
}
Here’s a breakdown of what we’ve done:
- First, we check whether we’re near the bottom of the page or whether there’s already an ongoing request.
- We start a new request by showing a loading spinner.
- We send a new request with the next page as a parameter.
- When we get a successful response, we can check whether there’s no more data or we can continue scrolling down the timeline.
- Once the stream has finished, we remove the loading spinner:

Figure 2.12: Reactive infinite scroll
Step 3 – Checking for new recipes
In our recipes.service.ts
file, we’ve implemented a method that will check whether there are new recipes periodically and whether we should scroll to the top of the timeline and refresh it with new data:
checkNumberOfNewRecipes(): Observable<number> {
return interval(10000).pipe(
switchMap(() =>
this.httpClient.get<number>(
'/api/new-recipes'))
);
}
Once we receive several new recipes, we can subscribe to that information inside NewRecipesComponent
and display it in the UI:
Figure 2.13: Reactive timeline updates
Now, once we click the 2 new recipes
button, we can scroll to the top of the timeline and get the newest data.
See also
- The
fromEvent
function: https://rxjs.dev/api/index/function/fromEvent - The
auditTime
operator: https://rxjs.dev/api/operators/auditTime - The
animationFrameScheduler
operator: https://rxjs.dev/api/index/const/animationFrameScheduler - The
observeOn
operator: https://rxjs.dev/api/operators/observeOn - The
distinctUntilChanged
operator: https://rxjs.dev/api/operators/distinctUntilChanged - The
switchMap
operator: https://rxjs.dev/api/operators/switchMap - The
takeUntil
operator: https://rxjs.dev/api/operators/takeUntil