React in Figwheel from Scratch

Jul 26, 2017 15:58 · 898 words · 5 minutes read

Update 2: 2017-08-10

This tutorial is out of date! Check out the updated version!

Update: 2017-07-30

Since publishing the original post, the Clojurescript team implemented CLJS-2280, which handles the work of shimming process.env for us.

As if that wasn’t enough, the team also released Clojurescript 1.9.854, meaning that we no longer need to build the latest version of Clojurescript from source!

The other thing that changed is that :npm-deps are no longer installed automatically, and the newly-added :install-deps build property is not yet supported in lein-figwheel, so I’m holding off on updating the tutorial until Figwheel catches up for us 👍

Original Post:

The Clojurescript team has been doing some amazing work recently with regard to interop with the Javascript ecosystem. I was surprised to discover that it’s easier to set up a Clojurescript project now than a modern Javascript web app.

Our Web UI stack at BlackSky is React paired with Redux, ImmutableJS, and a workflow powered by Webpack (transpilation with Babel, Hot Module reloading, code splitting, ESLint, Prettier – the works).

The total developer time we invested in setting everything up as it evolved over the months was probably on the order of weeks – an expensive endeavor. But the cost of setting up our Javascript development environment was cheap when you consider the benefits we derive from:

  • Tight feedback loops (enabled by HMR and React/Redux)
  • Elimination of a whole class of bugs (enabled by Redux’s uni-directional data flow and Immutable objects)
  • The rich ecosystem of React components published as open source

Clojurescript has given its developers the first two points for “free” since 2014 thanks to Figwheel and Clojure’s built-in immutable objects, but last bullet point has always eluded us.

Until now!

Here’s how you can set up a brand new Clojurescript web app to consume the popular React framework (and, presumably, almost any other project published on NPM), and all in around 5 minutes!

Step 1: Install Clojurescript

At the time of writing, the version of Clojurescript supporting these features has not been officially released yet, but it’s on the master branch of the Clojurescript repository.

$ cd /tmp
$ git clone git@github.com:clojure/clojurescript.git
$ cd clojurescript
$ ./script/build

This will compile the latest version of Clojurescript, and drop the assets into your local Maven repository. If you don’t have Maven, consider running brew install maven.

Step 2: Create a new project

$ cd /tmp
$ lein new figwheel hhnnngg
$ cd hhnnngg

Step 3: Upgrade Dependencies

At the time of writing, the lein-figwheel template uses older libraries that do not support our desired functionality. We’re going to upgrade them!

Inside project.clj:

ClojureScript

Update :dependencies to use the version of Clojurescript you just installed in Step 1 — check the output if you’re not sure. For me, it was:

[org.clojure/clojurescript "1.9.828"]

Plugins

Update :plugins to use:

:plugins [[lein-figwheel "0.5.11" :exclusions [org.clojure/clojurescript]]
          [lein-cljsbuild "1.1.6"]]

Near the bottom of the file, you will need to update the Figwheel sidecar to match:

[figwheel-sidecar "0.5.11"]

Step 4: Add NPM Dependencies

In your :cljsbuild, you should have two :compiler sections: one for dev, and one for prod. We’re going to modify them by adding the following:

:closure-defines {process.env/NODE_ENV "development"}
:npm-deps {:react "15.6.1" :react-dom "15.6.1"}

We use :clojure-defines as a way to distinguish between dev and min builds; in the min build defined in :cljsbuild’s :builds, you’ll want to set:

:closure-defines {process.env/NODE_ENV "production"}

Step 5: Add process.env namespace

When I first tried getting React to play nicely with Figwheel, I kept getting errors about how process was undefined. Turns out, React relies on the NODE_ENV environment variable (in Node, process.env.NODE_ENV) to know whether or not it’s in development mode, and without this environment variable… well, there were errors.

To solve this issue, we take the following steps:

  1. Create a process directory:
     $ mkdir src/process 
  2. Create env.cljs inside of src/process with the following contents:
    
    (ns process.env
      "This file exists to inform the Google Closure Compiler that we expect variables
      to be defined in the process.env namespace; libraries like React require the
      process.env.NODE_ENV variable to exist, so we define them here")
    
    (goog-define NODE_ENV "production")
        

Step 6: Preload process.env

This is the secret sauce that makes it all work. Before we try and run any code containing React, we need to make sure that the environment variables that Javascript expects are present!

In the :compiler sections of your :cljsbuild builds, make the following changes:

dev build:

Change :preloads [devtools.preload]
to :preloads [devtools.preload process.env]

min build:

Add: :preloads [process.env]

Step 7: Use React in your Clojurescript

Now, all the roadblocks are out of the way! Change src/hhnnngg/core.cljs to something that uses React, like:

(ns hhnnngg.core
  (:require [react :refer [createElement]]
            [react-dom :refer [render]]))

(js/console.log "Node enviornment is" process.env/NODE_ENV)

(def appDiv (.getElementById js/document "app"))

(render
  (createElement "h1" nil "Hello World!")
  appDiv)

Step 8: Behold!

$ lein figwheel

Conclusion

You’re a wizard! But you’re standing on the shoulders of giants.

The Clojurescript team has done absolutely fabulous work to bring this level of Javascript interop to the masses in a way that leverages the Google Closure Compiler to create smaller, better JS assets than weeks worth of Webpack tuning has yielded for our team.

The only hurdle left to clear for Clojurescript adoption at BlackSky is the problem of “who is going to support this when you get hit by a bus?” Not enough believers in the magic of Lisp.

But that’s why we show and tell. ✌️