Using the Expand Operator to Fetch Recursively
The last key feature we need to implement is to actually make use of the
gifsRequired value (i.e. how many gifs we should display per request), but
there is some hidden complexity here. Let’s consider how we would actually
implement this.
At the moment, we fetch GIFs from this URL:
`https://www.reddit.com/r/${subreddit}/hot/.json?limit=100` + (after ? `&after=${after}` : '')Notice that we supply a limit=100 to tell the Reddit API how many posts we
want to return. It then might seem sensible to implement a gifsRequired limit
like this:
`https://www.reddit.com/r/${subreddit}/hot/.json?limit=${gifsRequired}` + (after ? `&after=${after}` : '')However, that won’t quite work for us. At the moment this is what we do:
- Fetch
100posts from Reddit - Filter those to only include valid GIFs
- Display those GIFs
The actual number of GIFs we end up displaying in our application won’t
necessarily be 100 or whatever limit we supply, it could be anywhere from 0
to 100. This is where things get tricky.
We will continue to discuss this as we implement the solution, but the general
idea that we want to implement is (assuming an initial gifsRequired value of
20):
- Fetch
100posts from Reddit - Filter those for valid GIFs
- If there are less than
20valid GIFs, keep hitting the API until we do have20valid GIFs
The tricky part is that last step, and it is where the expand operator will
become extremely useful.
Implement the expand operator
The expand operator might look intimidating, but really it is just one more
operator that we are adding onto our stream. Let’s just add it, and talk about
it after.
this.fetchFromReddit(subreddit, lastKnownGif, 20).pipe( expand((response, index) => { const { gifs, gifsRequired, lastKnownGif } = response; const remainingGifsToFetch = gifsRequired - gifs.length; const maxAttempts = 15;
const shouldKeepTrying = remainingGifsToFetch > 0 && index < maxAttempts && lastKnownGif !== null;
return shouldKeepTrying ? this.fetchFromReddit( subreddit, lastKnownGif, remainingGifsToFetch ) : EMPTY; }) )Although there is quite a bit of logic going on here, the basic idea can be broken down to this:
- Do we need to fetch more gifs? Return the
fetchFromRedditstream - Do we have enough gifs or do we want to give up? Return the
EMPTYstream
If we return an observable stream in the expand operator, it will subscribe to
that stream, and then the expand operator will run again. This allows us to
keep recursively called the fetchFromReddit stream — with a different
lastKnownGif and remainingGifsToFetch each time — until we are satisfied.
The logic we have set up will keep retrying until any of these are true:
- We have enough gifs
- We have tried
15times - The
lastKnownGifisnullmeaning that there are no gifs left to fetch
Once any of these conditions are met, we return the EMPTY stream which
immediately completes and will cause the recursion to stop.
Now if we load up the application we should see that 20 gifs are displayed. If
we scroll down to the bottom, 20 more should display. This generally is easy
for the gifs subreddit because there are lots of gifs available in every
request. To put our functionality to the test a bit more you might try
subreddits with less gifs — pixelart and woodworking are two good examples
that have at least some valid gifs, but will likely require some retries with
the expand operator to get a full page of 20 (I checked the Network tab
whilst trying to load the woodworking subreddit and saw that it took
4 requests to get enough gifs).
What we have built is an extremely complex stream — which is a combination of multiple different streams — and it is more advanced than the types of streams you will usually have to deal with. Again, I want to reiterate that you should view what we have done as more of a brain teaser to challenge your knowledge of observables and RxJS. A bunch of the concepts we have used here will come up from time to time in everyday usage of RxJS, just not usually all at once like this.
Although what we have built is complex, and the operators may be hard to understand in the beginning, RxJS does actually greatly simplify what we are building. The requirements we are trying to satisfy here are just plain hard.
With a few declarative streams and some operators, we have implemented a strategy to recursively retry hitting an API based on some condition, paginate, and react to form changes. This would likely be much more complex and error prone to do without using RxJS operators like we have.