Skip to content
Nils Larsgård edited this page Jan 28, 2022 · 167 revisions

This is a quick introduction to Figwheel. It is intended to provide the reader with enough information to use Figwheel productively.

The aim of this document is empowerment, not bewilderment.

This Quick Start fills the same role as the ClojureScript Quick Start. Please see that Quick Start for a solid introduction to how to use the ClojureScript compiler.

If you are brand new to ClojureScript it is highly recommended that you do the ClojureScript Quick Start first. If you skip this you will probably suffer.

Install Leiningen

Leiningen is a terrific build tool for Clojure. To use this tutorial you need to have leiningen installed on your machine. Please visit the leiningen home page to learn how to get lein installed and running.

The minimal setup

Create a directory to work in called hello_seymore. Change into that directory and create a ClojureScript source file, a project file and an index.html.

mkdir hello_seymore
cd hello_seymore
touch project.clj
touch index.html
mkdir -p src/hello_seymore
touch src/hello_seymore/core.cljs

Edit src/hello_seymore/core.cljs to look like:

(ns hello-seymore.core)

(.log js/console "Hey Seymore sup?!")

Now you will need edit the project.clj as follows:

(defproject hello-seymore "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.clojure/clojurescript "1.10.914"]]
  :plugins [[lein-figwheel "0.5.20"]]
  :clean-targets [:target-path "out"]
  :cljsbuild {
    :builds [{:id "dev"
              :source-paths ["src"]
              :figwheel true
              :compiler {:main "hello-seymore.core"}
             }]
   })

At this point make sure you are in the project root directory hello_seymore and run Figwheel as follows:

lein figwheel

You should see a bunch of clojure libraries get downloaded and installed into your local maven repository (which happens to be in ~/.m2 on my Mac).

After that Figwheel will start up, compile your hello-seymore.core library and try to start a repl, BUT THE REPL WILL NOT START. Sorry for the caps, but this behavior is expected.

Type Ctrl-C to quit the Figwheel process.

If you list your project directory you should see this:

$ ls
figwheel_server.log
index.html
main.js
out
project.clj
src
target

Some new files have been created. The main.js file and the out directory contain your compiled ClojureScript code.

If you look at the contents of main.js you will see:

if(typeof goog == "undefined") document.write('<script src="out/goog/base.js"></script>');
document.write('<script src="out/cljs_deps.js"></script>');
document.write('<script>if (typeof goog != "undefined") { goog.require("hello_seymore.core"); } else { console.warn("ClojureScript could not load :main, did you forget to specify :asset-path?"); };</script>');

document.write("<script>if (typeof goog != \"undefined\") { goog.require(\"figwheel.connect\"); }</script>");

The last line of main.js loads the code that will connect to the Figwheel server. This is what enables the Figwheel server to communicate with the application, which is running in the browser.

In order to run the hello Seymore program, we will need an HTML file to load the compiled program into in the browser.

Edit the index.html file to look like this:

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <script src="main.js" type="text/javascript"></script>
  </body>
</html>

Now run Figwheel again:

$ lein figwheel

and load the index.html in the browser from the filesystem. The location bar in your browser should have a file://<...>/hello_seymore/index.html url in it.

Change back to the terminal where Figwheel is starting and when it finishes you should see a REPL prompt.

Go ahead and type some ClojureScript at the REPL prompt:

=> (+ 1 2 3)
6

If you get 6 as a response then you have successfully set up Figwheel!!!

You can also see that the REPL is connected to the browser:

=> (js/alert "Am I connected to Figwheel?")
nil

You should see the alert in the browser window. Only after you click on "Ok" there, will the REPL return nil.

Also, go ahead and open up the browser's dev tools so you can see the messages from Figwheel. You should see something like this (in Chrome):

Figwheel messages in the console

Now go back to your src/hello_seymore/core.cljs file and change the line that looks like:

(.log js/console "Hey Seymore sup?!")

to

(.log js/console "Hey Seymore! wts goin' on?")

and save the file. You should now see Hey Seymore! wts goin' on? printed in the dev console of your browser.

Congratulations!! You have set up Figwheel and your code is getting loaded into the browser as you save it.

As you can see, side-effects from print functions happen on every reload. This might not be desirable for all side-effects. Code that only triggers desired side-effects is called "reloadable code". We will discuss how to write such code later. Please remember that you are responsible for writing reloadable code! :)

Pro-tip: tail the figwheel_server.log

When working with Figwheel you should really have a separate terminal open to watch the figwheel_server.log. This can be immensely helpful when things aren't working correctly.

So open another terminal and type: $ tail -f figwheel_server.log

Creating the amazing counter

Let's create a simple program for demonstration purposes. This is going to be a bare bones program that uses the sablono ClojureScript interface to React.js.

First add

[cljsjs/react "15.2.1-1"]
[cljsjs/react-dom "15.2.1-1"]
[sablono "0.7.4"]

to the :dependencies list in your project.clj.

You will need to restart Figwheel to pick up the new dependency.

Pro-tip: use lein clean

Whenever you add new dependencies and restart Figwheel, also run lein clean. It will reduce the chances of working with old code

Add a place for us to mount our app in your index.html.

<!DOCTYPE html>
<html>
  <head></head>
  <body>
    <div id="app"></div> <!-- add this line -->
    <script src="main.js" type="text/javascript"></script>
  </body>
</html>

Now, edit your src/hello_seymore/core.cljs file:

(ns hello-seymore.core
  (:require [sablono.core :as sab]))

(def app-state (atom { :likes 0 }))

(defn like-seymore [data]
  (sab/html [:div
             [:h1 "Seymore's quantified popularity: " (:likes @data)]
             [:div [:a {:href "#"
                        :onClick #(swap! data update-in [:likes] inc)}
                    "Thumbs up"]]]))

(defn render! []
  (.render js/ReactDOM
           (like-seymore app-state)
           (.getElementById js/document "app")))

(add-watch app-state :on-change (fn [_ _ _ _] (render!)))

(render!)

Since Figwheel is running, once you save this file you should see the application running in the browser.

Let's talk about the reloadability of this tiny React app.

First, try the program out by clicking the Thumbs up link several times. You will see Seymore's popularity increase. Popularity should really be measured by how many times people are willing to click a thumbs up eh?

Now, if you change the source files by simply adding a blank line and saving it (something I actually do pretty often), the code will reload and you will see the popularity count go to zero. This is because we are redefining the app-state atom on every code load. This is not what we want. You can fix this by using defonce instead of def on app-state as follows:

(defonce app-state (atom {:likes 0}))

After you make this change you will see that the state of the program will persist through reloads.

Something else to notice is that the call to add-watch is getting called over and over again on reload. But this is OK because it is just replacing the current :on-change listener, making this top level side-effect reloadable.

Also, you will notice that we have a top level call to render! at the end of the file. This forces a re-render every time the file is loaded. This is helpful so that we will render app changes as we edit the file.

Remove the like-seymore function from the core.cljs file and add it to its own file src/hello_seymore/components.cljs.

Updated core.cljs:

(ns hello-seymore.core
  (:require [sablono.core :as sab]
            [hello-seymore.components :refer [like-seymore]]))

(defonce app-state (atom { :likes 0 }))

(defn render! []
  (.render js/ReactDOM
           (like-seymore app-state)
           (.getElementById js/document "app")))

(add-watch app-state :on-change (fn [_ _ _ _] (render!)))

(render!)

New src/hello_seymore/components.cljs:

(ns hello-seymore.components
  (:require [sablono.core :as sab]))

(defn like-seymore [data]
  (sab/html [:div
             [:h1 "Seymore's quantified popularity: " (:likes @data)]
             [:div [:a {:href "#"
                        :onClick #(swap! data update-in [:likes] inc)}
                    "Thumbs up"]]]))

As long as Figwheel was running this whole time your refactor should have been live loaded into the browser. After reflecting on the coolness of this, go ahead and edit the components.cljs file and remove the word "quantified" and save it. You will see your changes show up as expected.

Auto-reloading CSS

Figwheel will autoreload css as well. You will need to add some server level configuration to get this feature.

First create a CSS file css/style.css.

body {
  background-color: yellow;
}

Edit the project.clj

(defproject hello-seymore "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.clojure/clojurescript "1.10.914"]
                 [cljsjs/react "15.2.1-1"]
                 [cljsjs/react-dom "15.2.1-1"]
                 [sablono "0.7.4"]]
  :plugins [[lein-figwheel "0.5.20"]]
  :clean-targets [:target-path "out"]
  :cljsbuild {
    :builds [{:id "dev"
              :source-paths ["src"]
              :figwheel true
              :compiler {:main hello-seymore.core } 
             }]
   }
   :figwheel { ;; <-- add server level config here
     :css-dirs ["css"]
   }
)

And of course don't forget to add a link to your css in the index.html:

<!DOCTYPE html>
<html>
  <head>
    <!-- add link here -->
    <link href="css/style.css" rel="stylesheet" type="text/css"> 
  </head>
  <body>
    <div id="app"></div>
    <script src="main.js" type="text/javascript"></script>
  </body>
</html>

Now restart Figwheel, reload your index.html into the browser, and edit the style.css:

body {
  background-color: green;
}

Your page in the browser should now be green and not yellow. Without reloading the page!

Opting out of autoreloading

You may not want to have your code auto-reloaded but still want to benefit from Figwheel's auto-building and its REPL. All you have to do is add :autoload false to the :figwheel entry in your build config.

In this case your build config would look like this:

(defproject hello-seymore "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.clojure/clojurescript "1.10.914"]
                 [cljsjs/react "15.2.1-1"]
                 [cljsjs/react-dom "15.2.1-1"]
                 [sablono "0.7.4"]]
  :plugins [[lein-figwheel "0.5.20"]]
  :clean-targets [:target-path "out"]
  :cljsbuild {
    :builds [{:id "dev"
              :source-paths ["src"]
              :figwheel { :autoload false } ;; <<- add this
              :compiler {:main hello-seymore.core } 
             }]
   })

Serving assets with Figwheel's built-in webserver.

It is recommended that if you start needing a server to serve your ClojureScript assets that you just bite the bullet and write your own. But for convenience Figwheel has a built-in webserver. In order to use it you will have to place your assets in resources/public.

Following the example we are using you would need to make these changes:

Move your assets into resources/public:

mkdir -p resources/public
$ mv index.html css resources/public

Now you need to change your build config to output your compiled ClojureScript to resources/public as well. For good measure we will place them in a cljs directory.

(defproject hello-seymore "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.10.3"]
                 [org.clojure/clojurescript "1.10.914"]
                 [cljsjs/react "15.2.1-1"]
                 [cljsjs/react-dom "15.2.1-1"]
                 [sablono "0.7.4"]]
  :plugins [[lein-figwheel "0.5.20"]]
  :clean-targets ^{:protect false} [:target-path "out" "resources/public/cljs"] ;; Add "resources/public/cljs"
  :cljsbuild {
    :builds [{:id "dev"
              :source-paths ["src"]
              :figwheel true
              :compiler {:main hello-seymore.core 
                         ;; add the following 
                         :asset-path "cljs/out"
                         :output-to  "resources/public/cljs/main.js"
                         :output-dir "resources/public/cljs/out"} 
             }]
   }
   :figwheel { 
      :css-dirs ["resources/public/css"] ;; <-- Add "resources/public/"
   }
)

So now we have changed where the compiler is placing the compiled assets. It's very important to get :asset-path correct otherwise main.js will have code that doesn't point to your compiled assets and nothing will work. It is also important to add our new target path to the :clean-targets, otherwise lein clean won't work.

Since we changed the location of main.js, we need to reflect that in index.html:

<!DOCTYPE html>
<html>
  <head>
    <link href="css/style.css" rel="stylesheet" type="text/css"> 
  </head>
  <body>
    <div id="app"></div>
    <script src="cljs/main.js" type="text/javascript"></script> ;; <-- changed to "cljs/main.js"
  </body>
</html>

Now you can restart Figwheel.

You can now find your application at http://localhost:3449/index.html

Using the REPL

Since all code evaluated in the REPL actually runs on the browser, we have some powerful tools at our disposal. For example, we can take our app-state to places our UI interface can't easily reach to test for edge cases. Imagine that there might be a problem when the counter displays a 5 digit number after a number of things happened. Would you do those things and then click 10000 times on "Thumbs up"?

Pro-tip: use rlwrap

You'll get a much better REPL experience if you install rlwrap and run rlwrap lein figwheel

you can install rlwrap on OSX with brew install rlwrap

Start the REPL and go to the hello-seymore.core namespace

> (in-ns 'hello-seymore.core)
nil

Then change app-state to whatever number you want to test and check it on the browser:

> (reset! app-state {:likes 10000})
{:likes 10000}