Curious Programmer
wikipediawikipedia

How can I create a ClojureScript web app from scratch with Reagent and npm?

#1 | November 17, 2021 | Estimated 8 minute read

The goal of this guide is to create a basic Reagent ClojureScript web app from scratch using the Clojure CLI tools. We are going to bundle our JavaScript using Webpack, have HMR (Hot Module Replacement - reload components while coding) using Figwheel-Main and play around with a few npm packages like axios and moment.

In a nutshell:

  • ClojureScript with Clojure CLI
  • Reagent
  • Figwheel-Main
  • Webpack
  • npm packages

You can download the repository from GitHub.

This guide is a combination of tutorials I have read and videos I have watched. The biggest influencer's have been Between Two Parens, PurelyFunctional.tv and the official Figwheel documentation.

Version of things used in this guide

Java

I have the following version of Java on macOS Monterey, Apple M1 Pro.

openjdk 11.0.13 2021-10-19
OpenJDK Runtime Environment Temurin-11.0.13+8 (build 11.0.13+8)
OpenJDK 64-Bit Server VM Temurin-11.0.13+8 (build 11.0.13+8, mixed mode)

ClojureScript

Dependency Version
Clojure CLI 1.10.3.1020
ClojureScript 1.10.879
Figwheel-Main 0.2.15
Reagent 1.1.0

Node.js

Dependency Version
Node 16.13.0
npm 8.1.3
Webpack 5.64.1
Webpack-cli 4.9.1
React 17.0.2
React-DOM 17.0.2

Start a new project

  1. Create a new directory. In this guide we will use the very original project name example-app:

    mkdir example-app && cd example-app
  2. Create the file structure.

    • resources/public/index.html <- the main HTML file
    • src/example_app/core.cljs <- the main ClojureScript file
    • dev.cljs.edn <- Figwheel's configuration file
    • deps.edn <- ClojureScript's configuration and dependencies file
    mkdir -p src/example_app/ && touch src/example_app/core.cljs
    mkdir -p resources/public/ && touch resources/public/index.html
    touch dev.cljs.edn
    touch deps.edn

    Example of the file structure:

    ❯ tree
    .
    ├── deps.edn
    ├── dev.cljs.edn
    ├── resources
    │   └── public
    │       └── index.html
    └── src
        └── example_app
            └── core.cljs
    
    4 directories, 4 files

Note how spaces are separated by an underscore in the file name. The namespace will be an exact replicate of the file path but the names will contain hyphens instead of underscores. Eg. example_app will become example-app.

Initialize your project

  1. Initialize an npm project.

    npm init -y

    You could also create your own package.json file with an empty object. I've done this for brevity so you can see exactly what I have done through this guide. Feel free to choose either approach.

    echo '{}' > package.json
  2. Initialize a Git repository and add your .gitignore file.

    git init

    There are fancy ignore files in GitHub repositories that you can download and use in your repositories. In this guide, we'll exclude only what we don't need based on what we get exposed to. For not, it's just node_modules as we will be doing npm installs.

    # npm dependencies
    node_modules

Install Webpack

  1. Install Webpack.

    npm install --save-dev webpack@5.64.1 webpack-cli@4.9.1

    package-lock.json will be installed if you used npm. yarn.lock will be installed if you used yarn. You need to commit whichever one was created!

  2. Add the npm packages to play around with

    # A promise based HTTP client for the browser and node.js.
    # https://axios-http.com/
    npm install axios@0.24.0
    
    # A JavaScript date library for parsing, validating,
    # manipulating, and formatting dates.
    # https://momentjs.com/docs/
    npm install moment@2.29.1

Install Reagent

Install React.

npm install react@17.0.2 react-dom@17.0.2

Update your files

resources/public/index.html

A host page is the HTML page that includes your ClojureScript script. Figwheel provides a default host page which will be replaced with the one below.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Example Application</title>
    <!--
      The stylesheet will need to be available on the classpath.
      A good place for this file would be at resources/public/path/to/style.css.
    -->
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <!-- This will be swapped out by the content of our app -->
    <div id="app"></div>
    <!--
      Hardcoded Development Webpack Bundled JavaScript.
      Place the ClojureScript script tag as the last tag in the body.
      This is the convention for Google Closure compiled projects.
      https://figwheel.org/docs/your_own_page.html
    -->
    <script type="text/javascript" src="cljs-out/dev/main_bundle.js"></script>
  </body>
</html>

resources/public/style.css

An optional asset that can be added to your resources and referenced on your host page above.

body {
  color: #cc0000; /* because why not */
}

src/example_app/core.cljs

Entry point to the ClojureScript app.

(ns example-app.core
  (:require [reagent.dom :as r.dom]
            [axios]
            [moment]))

;; Use mock REST API to get data to output to the browser
(-> (.. axios (get "https://jsonplaceholder.typicode.com/todos/1"))
    (.then #(js/console.log %)))


;; Display the day of the week
;; https://momentjs.com/docs/
(js/console.log (.format (moment) "dddd"))


(defn app []
  [:div
   [:h1 "Example application"]])


(r.dom/render [app] (js/document.getElementById "app"))

dev.cljs.edn

Build file for Figwheel.main.

^{:auto-bundle :webpack ;; https://figwheel.org/docs/npm.html
  :watch-dirs  ["src"]
  :css-dirs    ["resources/public"]}

{;; root namespace for the compiled artifact.
 :main example-app.core

 ;; :none does not produce a single self-contained compiled artifact,
 ;; like with :whitespace, :simple or :advanced,
 ;; but rather creates an artifact that loads all of the separately
 ;; compiled namespaces.
 :optimizations :none

 ;; https://figwheel.org/docs/npm.html
 ;; instruct compiler to produce the bundled JavaScript file.
 :target :bundle

 ;; bundles up main.js and pulls in npm dependencies.

 ;;   :output-to is replaced with ./target/public/cljs-out/dev/main.js
 ;;   it is the path to the JavaScript file that will be output
 ;;   needs to point to a path that is basically the classpath + public.

 ;;   :final-output-dir is replaced with ./target/public/cljs-out/dev
 ;;   :final-output-filename is replaced with main_bundle.js
 :bundle-cmd {:none ["npx" "webpack" "--mode=development"
                     "--entry" :output-to
                     "--output-path" :final-output-dir
                     "--output-filename" :final-output-filename]

              :default ["npx" "webpack" "--mode=production"
                        "--entry" :output-to
                        "--output-path" :final-output-dir
                        "--output-filename" :final-output-filename]}}

deps.edn

User level aliases, dependency management and Clojure CLI configuration for deps.edn based projects.

{:paths
 [:src-paths :resources-paths :output-paths]

 :deps {org.clojure/clojurescript {:mvn/version "1.10.879"}
        com.bhauman/figwheel-main {:mvn/version "0.2.15"}
        reagent/reagent           {:mvn/version "1.1.0"}}

 :aliases
 {:src-paths ["src"]
  :resources-paths ["resources"]
  :output-paths ["target"]
  :dev
  {:main-opts ["--main"  "figwheel.main"
               "--build" "dev"
               "--repl"]}}}

Ignore more files

Add the following to your .gitignore file.

# npm dependencies
node_modules

# Cached classpath and the runtime basis files
.cpcache

# Output of the ClojureScript compiler
target

# https://clojure-lsp.io/settings/
.lsp

# https://github.com/clj-kondo/clj-kondo
.clj-kondo

Run the web app

Now that everything is set up, you can run the web app. Enter clj -M:dev in the terminal to open the app on http://localhost:9500

Final project layout

Read more about Figwheel classpaths.

├── node_modules
├── resources
│   └── public
│       # web assets HTML, CSS, images, etc
├── src
│   ├── example_app
│   │   └── core.cljs
│   │       # other source files
├── target
|   └── public
|       # compiled ClojureScript files
├── .gitignore
├── deps.edn
├── dev.cljs.edn
├── package.json
├── package-lock.json

Resources

  • Official Clojure Documentation
  • Official Figwheel Documentation
  • Official Documentation for Clojure/Script libraries
  • Official ClojureScript with Webpack Documentation
  • Get started with CLJS + Figwheel-Main YouTube Video from Between Two Parens
  • Start a ClojureScript App from Scratch Article by Between Two Params
  • ClojureScript [Tutorial][purely-functional-clojure-tutorial] by PurelyFunctional.tv
  • Learn ClojureScript eBook by Andrew Meredith
  • ClojureScript + Reagent Tutorial at PurelyFunctional.tv
  • Clojure Projects from Scratch Guide by Oliver Caldwell
  • Clojure deps.edn Guide on GitHub by Practicalli