When accepting user input and converting it into a request that talks to the outside world, we can often end up in a situation where responses are no longer relevant. This is a common case in type-ahead style input fields - we would only want to call an external search API after the user has stopped typing for a short amount of time. In this lesson we’ll see how we can begin to combine RxJS operators to solve such problems.
In this application, you can type a search term into this box, it will make an API request, and show the results here. It uses this Punk API, which is a public API that lets you search for beers.
In particular, to implement the search, we are using this, along with the beer name parameter. If we copy this, and run it in the terminal, it'll make things clearer.
We call this API, and I'm going to pipe it through jq to highlight it. You can see the results we get back is an array, and because we didn't provide any parameters, it just includes all the beers it has.
Now, because we are implementing search, we can use beer name here, and pass it along as a parameter. We can take that previous request, and we can add a query parameter, beer name equals skull, run it again. You can see we get an array, but this time, it only contains a single item.
This was the command we ran. This is the result. You can see it has a name, tagline, and image URL. Those are the three fields that we're using to display this.
Over in the code now, we have this main app component, and we render out the search field. This component accepts a default value and an unchanged function, and it returns import, that will call this unchanged whenever the value inside the import has changed, for every keystroke.
When it calls the unchanged function, we call this, the handle beer search, and we get the query, which was the value from the input field, and we call search beers on the props. This is available on the props, because we have connected this app component to the Redux store here, and we've passed through this object literal that has search beers.
If we look at this, it's an action creator that takes in a query, produces an object like this, that has a type of search beers, and a payload of query. Every time we type in the input field, it's going to fire this action into the Redux store.
Next, we have the beers component, which accepts beers and loading as props. It has a heading showing how many results we currently have. If we are in a loading state, it will show that spinner. If there are any beers, meaning the search was successful, then we create a list. For each beer, we create a list item, and we show the beer image, the name, and the tagline.
In this component, search beers comes from here, and the beers unloading properties come from the only reducer we have. In this reducer, we have an initial state, where beers is an empty array, and we have loading set to false.
In the reducer function, every time we see an action that has a type of searched beers, we set the loading state to true, and we don't modify anything else on the state. When we receive some beers back from the API, it's an array that comes back. We set that as the beers, and we set loading to false.
That covers how to render the search component, the list of beers, and how the state will be updated inside the reducer. Now, we're ready to implement the AJAX requests.
Here, we have the API that we saw earlier. Then, we've got a search function that takes in a term, and returns a full URL -- this is used in the URL above -- appending the query parameter, and encoding the term, so that it's safe for a URL.
Next, we have this AJAX function, which will take in the term from the payload of the action, create a URL via this function, and pass that to observable ajax.getJSON. Those are the component pieces, but what in the store do we want to react to?
We can filter the action stream down to searched beers, and we'll call switchMap. As always, we have access to the action inside here. We can destructure off the payload. This will be the search term that the user typed in. We'll return AJAX, pass along the search term, and should this return successfully, we'll map that into an action that can save the results into the store.
If we get back to our actions, we have this receivedBeers function. We want to import that. Then, we add the searchBeers epic into this call to combine epics. Now, we're ready to view this in the browser.
We can try it out by typing Skull, and we get that one result. Everything looks to be working correctly, but there's a hidden problem. If we reload this page, and open up dev tools, go to the network panel, watch what happens when I type Skull in here.
We can see that there are actually four requests that got cancelled mid-flight. That's actually a feature we get for free by using the RxJS implementation of AJAX. Go back in the code. This is what switchMap does for us. Every time we see that search beer action come through, this function is executed. In our case, we return this observable here, and it begins executing.
If another action comes through before this is completed, switchMap will unsubscribe to the previous one, and will resubscribe to the current one. That's why, in the browser, you see these marked as red. These are being canceled before they can complete.
That seems really good, but the problem here is that, as you are typing, we want to avoid this altogether. What we really want to do is, wait for a small amount of time after the user stopped typing, and make the AJAX request.
To implement this, let's look at the code we have so far. On this line, we are filtering the action stream to only include those that have the type search beers. This line will stay. Then we have switchMap, which has given us this cancellation feature for free. We'll be keeping that too.
Now, we just need a way to stop this AJAX request happening for every single keystroke. We need a way to slow this down. This will fire for every character typed by the user. We don't care about every character. We only care about the entire string in the input field after a given amount of time has gone past.
With Rx, we can do this with debounceTime, and we can provide something like 500, which is the amount of milliseconds to wait. This will subscribe to the stream before it, and it has an internal timer that it keeps resetting every time it sees an element come through the stream.
Only when this timer successfully completes, will it let the element through, so to speak, and it will come through to the switchMap, and the AJAX request will continue as normal. You can think of this like a barrier, or a gate. It's not buffering the events. It's not aggregating them in any way.
It's actually just dropping the ones that happen within 500 millisecond intervals of each other, and finally, when 500 milliseconds successfully completes, without seeing an event, that's when it will let it through the stream.
When the user is typing characters for the word, the internal timer for debounceTime keeps getting reset, and reset, and reset until, finally, he stops typing, and the element is let through. That's how debounce works. Let's have a look at it in the browser.
Previously, when we typed Skull here, we got four cancelled requests, and the fifth one made it through successfully. Now, if we do the same thing, you can see that only one GET request happens, and we get the result we expect.
We can do the same thing with deleting and leaving a character in place. You can see, if we filter this down, there was the first one, and there was the second.