Adventures in client-side routing with re‑frame and OAuth

It’s been a while since my last post; one of the few disadvantages of working at a company you like a lot* is that as your work projects get more interesting, the urge to hack on external stuff in the interim diminishes. But having had some downtime and considerable anxiety to burn off in the last week or two, I revived one of my old open-source ideas: Haunting Refrain, a little single-page app that gets your Foursquare check-in history and then builds a random Spotify playlist out of random data from the places you’ve been to.

This is still very much a work in progress, but I’m pretty pleased with its direction and it uses a lot of intriguing tech. It’s all written in ClojureScript, and it uses re-frame for the basic single-page app control flow and UI bits. re-frame was written by Mike Thompson, probably Clojure’s greatest living essayist, and is an excellent package somewhat in the elm / redux / FRP-if-you-squint vein. I’m also now using datascript to retain more domain focused data and posh to wire it up to the UI elements, about which more later.

In a previous post, well over a year ago now, I talked about some difficulties I was having with dealing with external OAuth-style authentication in single-page apps. Having obtained a great deal more experience with client-side routing since then, I managed to solve this fairly quickly in Haunting Refrain (if you don’t count the time I spent bashing my head against similar problems while at work as trying to figure it out, I guess).

My current set-up uses pushy to handle the HTML5 history setup and sibiro as a routing library, largely because it’s the only client-side routing library whose README I can read without the risk of developing a migraine (honestly, client-side routing should not be very complex, what’s the deal?). I define a big route table which uses keywords for every route; each entry has a URL-matching pattern and a reference to a reagent component which will be used to render the given page.

On the re-frame side I keep a value in the app-db called :route/current-page which keeps track of the keyword matching the current URL. Whenever the URL has been changed, whether by the user landing on the page for the first time or from a link being clicked or the app itself redirecting the user, pushy will dispatch a new re-frame event of the form [:route/changed route-keyword route-params-if-any]. The handler for that route just persists those two values in the app-db, and then in the view code there’s a subscription which pulls out the two values, checks the big route table for the component matching the current route keyword, and renders the component, passing it the page parameters in case it needs them.

So that bit is pretty straightforward, and I’m pleased with how declarative the routing table winds up. For the spotify and foursquare OAuth callback pages, I’m cheating a little bit. When the user needs to authenticate, he or she will be redirected to Foursquare, will hit the “allow access” button, and will then be redirected back to a specific callback URL on Haunting Refrain. Back on the site, pushy parses the callback URL as :foursquare/hello, and the component which is associated with that route renders a blank page and dispatches an event when it is mounted into the DOM. The handler for this event parses the OAuth access token out of the URL, saves it in the app-db, persists it into HTML5 LocalStorage, and then redirects the user back to the home page at :main/index.

The LocalStorage bit is an interesting side track. Since the user winds up seeing a full page refresh whenever he or she is redirected for authorization, the application essentially has three entry points where it needs to construct the user’s state from scratch: on the home page, and then on the authorization callback pages for both Spotify and Foursquare. Since, in theory, we need to store access token for both services, the volatile re-frame app-db is not going to cut it. In an earlier version of the application, you could authenticate to Foursquare, get an access token, then authenticate to Spotify, at which point a full page refresh wiped out the Foursquare token.

Persisting stuff to LocalStorage wound up being pretty easy, and relies on the new re-frame effect and coeffect system. I have a re-frame effect :persist! which, when returned from an event handler, will write a value to the browser via the hodgepodge library. A matching :local-storage coeffect pulls it out of the browser, and I’m using this during app initialization to seed the database with any previously-retrieved access tokens. This works great, and should make it quite easy to persist arbitrary data across application invocations.

Overall I’m quite happy with re-frame. I’ve found that it can be a little difficult to track the control flow once a system gets reasonably complex, since you’re essentially encoding most of it by means of constructing a large bespoke FSM, but it does a fantastic job of keeping the control flow and display logic separate, and it is fairly easy to tinker with.

I’ll have more to say about Haunting Refrain in the days to come—the routing stuff isn’t actually one of the more interesting parts of it, the datascript and posh bits are. The thing is still buggy as heck and not deployed anywhere, but it’s runnable locally. Check it out!

 

* Workframe – we’re hiring!