rxjs has a steep learning curve, but can do some really cool things. Let’s say you want an input form to do “as you type” async validation. Perhaps it’s checking if the username is taken or not. Another use case could be checking if some url is valid. I implemented this with ngrx-effects (after failing a lot!) and thought I would share.
@Effect() asyncServerUrlCheck$ = this.store.select(fromAccount.getLoginForm).pipe( filter(form => form.value.showUrl), distinctUntilChanged( (first, second) => first.value.url === second.value.url ), switchMap(form => concat( timer(300).pipe( map( () => new StartAsyncValidationAction(form.controls.url.id, "exists") ) ), this.userService.checkUrl(form.value.url).pipe( map(() => new ClearAsyncErrorAction(form.controls.url.id, "exists")), catchError(() => [ new SetAsyncErrorAction( form.controls.url.id, "exists", form.value.url ) ]) ) ) )
Lots going on here and it sums up both my love and hate of rxjs. It’s unreadable garbage code until you understand it – then it’s fantastic. Let’s try to break this mess down.
First off – notice the asyncServerUrlCheck observable (All ngrx effects are just observables) is watching state instead of actions$. This is done because I’m watching the form field’s state instead of waiting for an action. Then I filter out changes that are not to the form field in question and I make sure it’s a real change.
Now the magic starts – next in my pipe is switchMap which is important because I want to cancel any previous observable. If the user types google.co and then google.com I probably don’t want to check if google.co exists. switchMap throws out any previous work and start over. Of note – if I didn’t use switchMap I would see a LOT more network requests.
Next up is concat. concat is what is going to allow me to return multiple actions. Without it, I would just get the first start async validation and nothing else. concat is a static function and not a operator (Oh but it was an operator in rxjs 5 – which actually makes me hate rxjs a little because it’s so much mental overhead!). We’ll pass observables as parameters to concat. Our first concat observable is a timer. Timer is what implements the logic to wait until the user stops typing. Because we use it with switchMap earlier – it will get canceled if the user types something else! We could stop right now and have a start async validation action dispatch when the user stops typing. Cool. I do suggest trying this out piece by piece if you want to understand it rather than coping the entire snippet.
Now I need to add success and failure actions after the async call is made. I’ll add a second parameter to concat which is my service function call. The function will return an Observable (Once again remember that concat accepts a list of observables). I pipe this into a map and catchError. That logic should look familiar if you used effects before so I won’t go into detail.
This is how it looks in redux devtools. I get lots of set value’s from each user character input. But I don’t get a start async validation for each one (meaning I don’t excessively check the server!). Then I get either set async error or clear async error (success) actions depending on if the server url is valid.
I’ve found this pattern hard to grok initially – but now is easy to apply elsewhere. Making async validation easy means I’ll be more likely to use it and give users a more interactive experience. Try it yourself at https://passit.io and download the chrome or firefox extension (The web version ask for server url). And if you aren’t a regular visitor to my blog – Passit is an open source, online password manager that my company built so please give it a try.