Although ‘epics’ in RxJS are often described as ‘actions in, actions out’, there may be times when you need to ‘bail out’ of an action, or to just ignore the current one given some condition. By learning about RxJS’s filter
Operator, we’ll see how easy it is to do ‘nothing’ when you need to. We’ll also see how the composition techniques available in RXJS allow us to control the UI in a declarative manner.
In this application, anything I type into this box will be appended to a URL that makes up an API call. In this case, it gave us three results. Watch what happens, if I delete all three characters. We get a 400 error.
If we open up the network panel and repeat that, we can see the first request sent beer name equals SKU. If I delete all the characters, you can see it's making another API call with an empty string. You can see in the response we get invalid query parameters. Let's head over to the code, see if we can figure out why this is happening.
We can see that every time the searched beer action goes through the Redux store, we're destructuring the payload which will be the search term the user typed. We're calling this AJAX helper, but as it stands, we're just sending it through exactly as it comes out of this text input field.
In this case, an empty string is absolutely fine, but we know the API doesn't support that. We need to put a filter in place to ensure that actions that come through this type, but that have an empty payload, never make it as far as this.
We can apply a filter just before the switchMap. The filter operator from RX works just like the filter method does on arrays. We get access to each element, and then we return either true or false based on some condition.
In our case, we just care that the payload is not equal to an empty string. Of course, if you have more complicated logic for the filter, this could be extracted to a separate file as it is just a pure function. I'll keep it in-line here for the demonstration purposes. Now, having the filter in place here means that switchMap will never receive an element that has an empty payload.
Let's see how that looks in the browser. If we repeat the steps from before and type SKU, you can see that we get an API call with beer name equals SKU. If we delete all three characters, you can see we don't get any further requests, which is what we wanted. We do however have this problem where the loading spinner is still in place, so that must mean the loading property on the state is still set to true.
Let's address that. If we look at the file that contains our reducer, you can see that every time an action with search beers as its type comes into this reducer, we're setting loading to true. If we look at our Epic, we've applied a timer and a filter to the stream. There are many cases where this action will fire, but it won't actually result in an AJAX request. In our case, this is no longer the correct place to set loading true. Let's remove it from there.
We'll create another action that is specifically designed just to set the loading state. We'll create another constant. We'll have a function that can generate the action. This will just receive a parameter called loading which will just be true or false. We'll send that through on the payload.
Now back in the reducer, we can import that constant and simply set loading equal to whatever comes through in the payload which will be either true or false. Now, we have this action that we can fire explicitly to set the loading state. We want to do this back in Epic.
When you look at this implementation here, it's very clear that the only time we want to set the UI into a loading state is when we get all the way into here. Really what we want to be able to do is, return multiple actions from this.
We want to first set the UI into a loading state, and let the Ajax play out as it normally would. If it's successful, it will fire the success action. If there's an error, it will fire the error action. Crucially, we want to be able to join actions together and return multiple actions where needed.
This is where we really start to see the power of RX. RX is AJAX implementation is lazy in that, until someone subscribes to it, it's not actually going to do anything. We can make use of this by saying that request is equal to the result of calling this.
We can construct the loading action that we want to fire before the AJAX. We can just say loading is equal to observable of search beers loading. We'll just bring that in. Send through true here. Now, we have request and loading that are both observable.
We can return the result of stringing those two things together. We can use observable.concat to do this. This is a higher order observable that takes in any number of arguments, and it will subscribe to them in sequence, each waiting for the previous to complete successfully.
In our case, we want to set loading to true first. We'll send through the loading observable. This only produces a single element which will be this action. Then, it will complete. Immediately after it, we can send through the request.
If it makes it simpler to understand, we could reorder these, loading state in UI and this one is the external API call. Here we're just constructing two separate observables. They both eventually produce actions. This one has one action. This one has either a success or an error.
Then to switchMap, we return the result of queuing them up, so if we ever make it past the debounce, past the filter, we will always fire loading to set that true. Then, we'll follow up with the AJAX request.
Now, let's go over to the browser and check, if this has solved our problem. Now just as before, if I type SKU and I just noticed here that the loading spinner did not show, that's because back in the actions I actually got the name wrong here. Inside this search beers loading action creator, this should be loading here. We'll save that and go back to the browser. Reload.
This time if I type SKU, you can see we go into that loading state, and we make a single API request. If we delete all three characters, nothing happens here, and we have no loading spinner. Now, it's working exactly how we want it to.
Nice trick