For the past few years, I’ve kept a Notes.app list of restaurants, hikes, bars, and other things I’ve been meaning to visit. It was messy, not geographically-aware, and difficult to quickly add to. Not finding any satisfactory solutions elsewhere, I decided to scratch my own itch and build an app worth a spot on my home screen. Outboard is the result of the last five months of side project hacking, built in ClojureScript on React Native.
I’ve built a couple apps before using Objective-C and enjoy Swift as a language, so why this stack? Having become familiar with the functional reactive model as interpreted by Re-frame, it’s hard for me to approach UI implementation any other way now. Throw in hot reloading and the Clojure REPL, and ClojureScript on React Native was the obvious choice. What follows are the pros and cons I ran into along the way.
Functional Reactive Programming
At the risk of overselling this from the start, I simply do not run into bugs at the intersection of state and UI anymore. An entire class of defects I’ve struggled with over the last decade-plus of professional software development has virtually disappeared. No more slightly out-of-sync screens, no more links to dead data; all gone. In its place, a lightweight, low-ceremony, low-boilerplate framework in Re-frame that makes data manipulation straightforward and easy to understand when you return to old code in the future. In my opinion, Re-frame is the killer application of the Clojure philosophy on the front end.
The ability to experiment with changes to the app’s layout or the running state in the iOS Simulator is transformative for speed of development. If I want to know what a particular set of data might look like laid out on a device, I can eval a single line in the REPL and the results are on-screen instantaneously. There is no comparison to re-compiling and manually updating data.
This is probably more of a personal preference borne of experience, but doing app layout with Flexbox and Hiccup is much easier for me to grapple with than XCode’s Interface Builder. I’ve never found Apple’s constraints system for managing different device sizes and resolutions intuitive, but Flexbox lets me leverage the (meager) web design experience I have to make reasonable responsive app designs.
The React Native community at this point in time spans a wide range of heavily-invested companies, resulting in a good stable of well-maintained libraries for additional functionality. A few I used:
- react-native-maps, originally by Airbnb.
- Branch for deep links
- Bugsnag for bug catching
Some things I needed to do did not have any React Native library already built, but building functionality for things like data storage in App Groups and geocoding addresses with MapKit in Swift and exposing it to the ClojureScript layer was straightforward and will be the subject of a further post.
It was not all sunshine and rainbows, though.
React Native’s Sharp Edges
React Native in particular is prone to frequent breaking changes. For the most part, I was able to sit on a locked version of React Native, but occasionally adding a library would require an upgrade to React Native itself. The upgrade process is exceptionally brittle, and requires manually git merging large Info.plist files.
One acute pain point resulting from the tool mismatch issue is the React Native packager, Metro. ClojureScript uses an entirely different compiler and packager in Google Closure, and using the two together is inefficient; see for example this issue documenting out of memory and time issues when Metro slowly tries to package an already-packaged ClojureScript file. Even without the extra complications of Closure, Metro can be infuriating to work with — I once “solved” an issue with its cache after an hour of following dozens of posited solutions from a Github issue thread by simpling renaming the directory the project lived under.
I’m happy with my 1.0 release and using it daily, while making the occasional UI tweak or bugfix. I do have plans for big future features though.
The obvious next step is adding sync functionality, for users to share with others or with their own devices. Having the app’s data entirely represented in EDN with specs for all of it will hopefully make that sync service easier to reliably build.
I’d also like to add clients for the iPad and Desktop; again, being able to repurpose the data model with an appropriate UI layer on top should be helpful.
There are tradeoffs, but fully considered, I’d choose ClojureScript on React Native again in a heartbeat for this class of app. The speed and reliability of the heart of the development process are hard to beat, and outweigh the misaligned tools and occasional ecosystem immaturity issues.