Combine keyboard events with regular actions

Shane Osbourne
InstructorShane Osbourne

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 3 years ago

The core benefit of RxJS is the ability to compose side-effecting code in creative ways. This lesson shows an example where we can cancel a pending networking request by either a regular action, or by a keyboard event such as pressing the ESC key.

Instructor: [00:00] This application allows users to cancel a search request by clicking the cancel button. If we want to allow them to cancel it using the escape key as well, we can do that within the epic.

[00:11] This is where the Ajax request is created. We have this line "takeUntil(action$.ofType(CANCEL))." Since takeUntil just accepts any observable, our first approach might be to merge this with an observable of key presses. Let's try that.

[00:31] Let's remove that. Inside here, we'll call it "Blockers." That's going to be a merge of the stream of actions, the hover type CANCEL, and fromEvent, which we can import from RxJS.

[00:53] We'll give the current document. We're going to listen for any keyup event, but we need to filter that to only be escape keys. We can do that like so.

[01:04] Now blockers here, this is going to be an observable that produces elements from both of these streams. If we give that to takeUntil, this would be the first attempt. Let's go to the browser to see why it's not quite right.

[01:19] If we perform a search...After a second now, I'm going to press the escape key on my keyboard. I've pressed the escape key. Now we're in this weird state.

[01:29] We're not getting the data back. The network request has been canceled. The last action to be dispatched into the store was this setState as pending. Let's go back to our epic and see what's wrong.

[01:47] The problem is that this is occurring. Then takeUntil is canceling this part. Because there is no action associated with the keyboard event, there's no way of putting the UI back into an idle state.

[02:06] Really, what we want to say is takeUntil this observable produces a value. If it does produce a value, then dispatch this other action instead. We can do that with Rx. First, let's model that in the reducer.

[02:23] Right now, when the cancel action is seen, we set ourselves into an idle state. Let's change that to be a reset instead. We want our epic to be in charge of dispatching the actions that will change the state rather than just listening to that cancel method which is being fired from the user interface.

[02:44] We'll change that to reset. We'll add it here. Now we have this, that we can use inside the epic to reset the UI when we see fit. Back in the epic, any values that are produced from here, we don't want the actual value. Rather, we want to map it into the action reset.

[03:13] We can pipe anything from here into mapTo. Then we can just call reset. Import mapTo and import the action creator. It's still not going to work as we expect because takeUntil will not pass on the elements produced by this observable. It's only listening for them using it as a way to cancel previous observables.

[03:39] We need a totally different approach. Let's remove that. Let's extract this part out. We'll call this the Ajax. Now we have these two observables. This one will produce actions of type either FETCH_FULFILLED or FETCH_FAILED.

[04:00] This one is going to produce actions of type RESET. When you think about it, what we really want them to do is have a race. We want to subscribe to both of these. The one who emits a value first, we want that to cause the other one to be unsubscribed to.

[04:18] For example, if we subscribe to both of these at the same time, neither are going to produce any values immediately. If the escape key is pressed before we get here, we want to cause this to be unsubscribed to.

[04:32] Likewise, if the Ajax request completes successfully and we get a fetch fulfilled or if we get a fetch failure, we want to unsubscribe to this because we no longer care about keyup events or cancellations.

[04:48] The crucial part here is that when we get values from one, it causes the other to be unsubscribed. It's the reason why we don't have to manually bind or unbind event handlers for this keyup.

[05:01] This will be unsubscribed to if the Ajax request produces a value. It means that this will execute its unsubscribe logic. In the case of a keyup, it will just be removing an event listener.

[05:15] We can achieve this in Rx using race. We need to import that from Rx, not Rx operators. Then we can pass along Ajax and blockers. Race takes any number of observables and does exactly what we just said. It will subscribe to them all. The moment one of them starts producing values, it will unsubscribe to all the others.

[05:40] In the browser, what we expect to see is that we can still click the cancel button, which will produce this action, or we can press the escape key. Either one will result in another action called reset. That's the one we're now using in the reducer to put the UI back into an idle state.

[06:05] If we begin a search and press the escape key, you can see that we just get the reset. We did begin the Ajax request. Then we followed up with this reset. If I press the cancel button, again, we started the Ajax request. Then we sent a cancel event through the UI, which ended up resulting in a reset.