Modify Streams with User Input
Before we get into the next addition to our stream, let’s briefly recap what our
gifsLoaded$
stream does so far. This is the general flow:
- Gets a stream of values from the
pagination$
stream - It will start that stream with one single emission of
null
automatically - It will take those emissions and then switch to the
fetchFromReddit
stream, but we useconcatMap
because we want to wait for each request to Reddit to complete before addressing the next emission
The key thing we are going to address this lesson is that, at the moment, the
subreddit we are using to pull in GIFs is hardcoded to the gifs
subreddit. We
want our user to be able to supply whatever subreddit they like to pull in GIFs
from.
This means that we want to achieve the following:
- The user should be able to type a subreddit into a form control
- The
gifsLoaded$
stream will now emit GIFs from that particular subreddit (and it will get rid of any GIFs from the previous subreddit)
The question is, how do we modify gifsLoaded$
to support this behavior?
Adding a subredditChanged$ Source
Now we want to react to the user changing the subreddit, so we are dealing with a new source of data now and we are going to have to add that to our service.
It can be hard to figure out what that source of data should be exactly. Your
first instinct, and one that would technically work, is to just create a new
source that is a Subject
:
subredditChanged$ = new Subject<string>();
You could just next
this with whatever subreddit you want to change to, and
react to that by incorporating it into the gifsLoaded$
source.
This faces the same problem as our imperative approach to the
videoLoadComplete$
source. It will work, but it requires an imperative call to
make it work and we have no idea what subredditChanged$
is just by looking at
it. Again, sometimes it might make sense to do that, but it is worth first
considering what exactly our data source is and if we can just access that
directly.
We are going to be reacting to a user entering data into a form. That form will be associated with a Form Control. What we really want to do is react to the value of that control changing.
The good news is that we can just define that form control directly in our service. This will give the service direct access to the values as they change, and we can also bind the form in our template to the form control in the service.
Add a
FormControl
to theRedditService
:
export class RedditService {
private http = inject(HttpClient);
subredditFormControl = new FormControl();
A FormControl
has a valueChanges
property which is an observable stream that
will emit every time its value changes — just what we need!
Add the following source to the
RedditService
:
private subredditChanged$ = this.subredditFormControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
startWith('gifs'),
map((subreddit) => (subreddit.length ? subreddit : 'gifs'))
);
We’re using valueChanges
from the form control to create our source here…
but what is all this other stuff?
Consider that we want to launch a request to the Reddit API every time the value
in that form control changes. If I type chemicalreactiongifs
that means the
form control will change 20 times (one for each letter) and 20
requests to
Reddit will be launched — really, we only want to launch one request when the
user is done typing.
So, we use debounceTime
which will cause the stream not to emit any values
until it has received no new value for 300ms
. This might still cause
unnecessary requests if the user types slowly or if they pause in the middle,
but it should remove a lot of unnecessary requests. We also use
distinctUntilChanged
which will cause the stream not to emit a value if the new
value is the same as the old value (no need to re-fetch the same data). We also
make use of startWith
because, just like before, we still want to launch an
initial request for the gifs
subreddit without waiting for the user to
actually type something.
Finally, we also want to handle the situation where the user clears the search
bar such that there is no search term — in this case we want to default back to
using the gifs
value so we map to that if the search term is empty.
Reacting to the subreddit changing
Now that we have our source, we can incorporate it into our gifsLoaded$
stream
so that the gifsLoaded$
source reacts to the subreddit changing by emitting
gifs for that subreddit.
Update the
gifsLoaded$
source to reflect the following:
//sources
pagination$ = new Subject<string | null>();
private subredditChanged$ = this.subredditFormControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
startWith('gifs'),
map((subreddit) => (subreddit.length ? subreddit : 'gifs'))
);
private gifsLoaded$ = this.subredditChanged$.pipe(
switchMap((subreddit) =>
this.pagination$.pipe(
startWith(null),
concatMap((lastKnownGif) =>
this.fetchFromReddit(subreddit, lastKnownGif, 20)
)
)
)
);