ReactJS with CSP & core.async
ReactJS often gets conflated with the entire stack of tools that people in the React community use. JSX is fine but is off putting to new comers. Webpack is wonderful with HMR and amazing deployment options but has a difficult learning curve. But the flux of the week pattern is the most difficult. Every few months there’s a new library and they are always very restrictive about data access patterns. Ask any developer doing React in the past year and they’ve written and rewritten at least a few apps in different versions of Flux/Redux. We tried RxJS and different Flux libraries but hit many walls after a few months of use. Who in the React community can say that they been with a data layer for 9 months? I fairly certain we’re one of the few groups who are ecstatic with how simple, stable, performance and malleable our data layer is. We use ClojureScript and you should too.
- Do you like RxJS/Flux/Redux-type Observables? Got that.
- How about Go and Es2016 awaits and generators? Got that and better with transducers.
- Like immutable data? Got the best implementation around.
- Like travel traveling, teleportation, telekinesis? That’s trivial in cljs.
- Like having a great community with great libraries? Got that.
- Great composability with just the right amount of encapsulation? Yep.
- Writing code in whatever way is best for your data? ClojureScript is a full language.
- Do you like not ripping out your entire data model when a new Flux comes along? We’ve unsubscribed from the flux of the month club long ago.
Why would you want to be restricted to a single pattern with Flux/Redux/RxJs when you can use an entire functional language instead? Our ClojureScript data layer was completed in the first 20% of our application’s development. We had nightly, fully functional builds, for almost the entire project. ClojureScript has been nearly flawless with only 3 bugs that made it past development and got caught in the unit test/automation/QA process for our v1 release.
Case Study: Capital One’s React & ClojureScript Web App
To build the v1 of our web app on Capital One’s Level Money team the development process only took 12 developer weeks from start to finish. On first day of the project I had never programmed in a LISP before. After just 3 developer weeks, we had a fully functional data layer. (That includes numerous code reviews from one of our amazing back-end Clojure engineers).
Once the data layer was complete, it took one more week to create a fully functional skeleton app (live data/REST with a basic UI). For the rest of the build process we maintained a fully functional app. The rest of the project was just incrementally refining the UI.
What is ClojureScript?
Side-effect free data flows
ClojureScript manages multiple dependencies and caching while allowing React to connect for subscribing to data changes and triggering new flows. It also allows us to create very simple, isolated data flows.
For example take the process of changing the user’s password. Generally a program would first call login(email, pass) then if and when it is successful it will be able to call changePassword(oldPass, newPass).
In order to isolate state and side-effect as much as possible, our ClojureScript code focus on dependencies. As long as we have a user-id and auth-token as static data we should be able to call the change password function like this: `changePassword(user-id , auth-token, new-pass, old-pass)`. This eliminates an enforced requirement to call multiple functions and keeps functions as side-effect free as possible. Internally it’s a little more advance than that but we’ll get to those details in the following post.
The touch points between React and ClojureScript are very light. In ClojureScript we have a very small veneer that transforms internal ClojureScript patterns (core.async, atom-watchers & immutable data) to JS patterns (Promises, callbacks & JSON). Our ClojureScript library is aptly called “Triforce”. For React to access Triforce, it’s passed into a context passed in during bootup. Views can access it with this.context.triforce. The components that have access to Triforce are kept to a minimum. The more abstract and the more domain-agnostic the views the better.
CLJS->JS data layer
In the example above, ClojureScript is doing all the work to retrieve the bank list. It fetches the data from the server and cache it. When the bank_list function is called, it either waits until the network request is complete or returns data from the cache right away.
It starts to get more complicated in the BankItem view. In order to connect a bank there’s multiple REST calls, long polling for async jobs and multiple process flows, error paths, editing account details once connected and keeping local data model throughout the app and server in sync. ClojureScript manages dependancies, isolates data flows extremely well while making the code very simple and easy to understand. Looking at the public interface looks it’s easy to dismiss the complexity ClojureScript is managing.
Let’s start with the simpler case of event propagation when the bank is connected or changed. This is a basic callback with a filter for whenever the bank accounts are modified. It gets added on componentWillMount (and remove on didUnmount).
The second touch point is passing an event handler down to a child component so that accounts can be activated and deactivated.
Bank Linking Lifecycle
More complex then account changes is the authentication process to link to a bank. There a few things that require user interaction, authentication, security question challenge (for added authentication), success and fail. If and when ClojureScript needs to authenticate or handle a challenge we’ll pass in a callback it can trigger. The callback will return a promise that will let us get the information from the user and send it back to ClojureScript. Here’s the complete linking function:
The only thing left to do is wire up the this._link function and the challenge data we might or might not receive from the server. When the bank linking succeeds it will send the account information in the resolve as well as triggering the bank account watcher.
The huge win we have with this very physical separation allows us focus hardening the most critical piece, the data layer. Since the business logic requirements rarely change we can harden the most important part first. On the View layer unit tests we add does 2 things; help prevent known possible failure points and add a barrier (in the form or extra work to redo tests) when we want to change the view. Not even the best UX designer will create the perfect interface on the first pass. We want to balance remaining flexible to change to the view for users to understand better as well as maintain that it functions properly.
Hopefully this help so how simple and flexible a ClojureScript data layer can be. Part deux we’ll attempt to cover the highlights of our ClojureScript interworking.