ClojureScript and re-frame Hello World
Let's get started by creating the project running lein new re-frame reframe-hello-world +10x
:
> lein new re-frame reframe-hello-world +10x
Generating re-frame project.
Now I'll enter the project directory and run yarn
to install all Node.js dependencies. At this time, the shadow-cljs
will download the ClojureScript dependencies and setup the project for us.
Then I will run yarn watch
to start the development server. After build the application, I will be able to access it at http://localhost:8280
.
Nice, fast feedback already setup 💪.
My Hello World is a application that displays a counter and two buttons. One of them increments the counter and the another decrements the same counter.
This application is a re-frame application. So it already have a reactive state management library already setup. I will use that.
The initial state is declared in the db.cljs
file:
(ns reframe-hello-world.db)
(def default-db
{:name "re-frame"})
This database is loaded when the application. You can view the definition in the events.cljs
file:
(ns reframe-hello-world.events
(:require
[re-frame.core :as re-frame]
[reframe-hello-world.db :as db]
[day8.re-frame.tracing :refer-macros [fn-traced
))
(re-frame/reg-event-db
::initialize-db
(fn-traced [_ _]
db/default-db))
The ::initialize-db
is dispatched when the application mounts. This behavior is defined in the core.cljs
file:
;; More code above
(defn init []
(re-frame/dispatch-sync [::events/initialize-db])
(dev-setup)
(mount-root))
When the application init, the first thing that happens is the dispatch of the event ::initialize-db
, then the dev setup is configured and the root application is mounted in the dom. re-frame uses Reagent behind the scenes. Reagent uses React.
Ok, what happens if I change the default state of the application to {:name "Hello World"}
in the db.cljs
file?
(ns reframe-hello-world.db)
(def default-db
{:name "Hello World"})
Yay! ✨✨
The application itself is just a function. You can found it at the views.cljs
file.
(defn main-panel []
(let [name (re-frame/subscribe [::subs/name])]
[:div
[:h1
"Hello from " @name]
]))
In the second line the symbol name
is defined. It is a subscription to the name reactive parameter. The subscription is defined at subs.cljs
file:
(re-frame/reg-sub
::name
(fn [db]
(:name db)))
In the code above, we a subscription with the name ::name
is defined at the "re-frame context". To watch changes in that value we just subscribe to that piece of reactive state using the function subscribe
from the re-frame
namespace.
In other words, every time that the ::name
state change, the main-panel
will re-render with the brand new value. We could test it registering a "database change event" and dispatch it from a button.
Let start describing the new event handler at events.cljs
file. I'll call it as ::set-name
:
(ns reframe-hello-world.events
(:require
[re-frame.core :as re-frame]
[reframe-hello-world.db :as db]
[day8.re-frame.tracing :refer-macros [fn-traced]]))
(re-frame/reg-event-db
::initialize-db
(fn-traced [_ _]
db/default-db))
(re-frame/reg-event-db
::set-name
(fn [db [_ name]]
(assoc db :name name)))
The assoc
function takes a map, and a set of key and values and set the new values to the keys in a new version of the original map.
Now I'll import the application events in the views.cljs
page and render a button that dispatche ::set-name
action:
(ns reframe-hello-world.views
(:require
[re-frame.core :as re-frame]
[reframe-hello-world.subs :as subs]
[reframe-hello-world.events :as events]))
(defn main-panel []
(let [name (re-frame/subscribe [::subs/name])]
[:div
[:h1
"Hello from " @name]
[:button {:on-click
#(re-frame/dispatch
[::events/set-name "Heeeeeeyyy"])}
"Update name"]]))
After click in the "Update name" button, the page text will change to
Cool, right? Looks like super powered version of Redux.
With that in mind, we can proceed and create the "counter" state. I will define a new key in the initial state -- the initial state is a Clojure map:
(ns reframe-hello-world.db)
(def default-db
{:name "Hello World"
:counter 42 })
Now I'll define two new event handlers. One that increases the counter and other that decrease the value. Both events I will define at the events.cljs
file.
(re-frame/reg-event-db
::inc-counter
(fn [db _]
(let [value (:counter db)]
(assoc db :counter (+ value 1)))))
(re-frame/reg-event-db
::dec-counter
(fn [db _]
(let [value (:counter db)]
(assoc db :counter (- value 1)))))
Here I'm using the let
form and defining a symbol called value
. This symbol holds the current value of the counter
in our db. For me, it is a lot easier to read this code than the version that makes the update in-place.
Now I'll make the main-panel
function subscribe to the counter value. First I need to define the registration of a subscription at the file subs.cljs
(ns reframe-hello-world.subs
(:require
[re-frame.core :as re-frame]))
(re-frame/reg-sub
::name
(fn [db]
(:name db)))
(re-frame/reg-sub
::counter
(fn [db]
(:counter db)))
Now is easy to just subscribe to that reactive value:
;; ...
(defn main-panel []
(let [name (re-frame/subscribe [::subs/name])
counter (re-frame/subscribe [::subs/counter])]
[:div
[:h1
"Hello from " @name]
[:h2
(str "Counter " @counter)]
[:button {:on-click
#(re-frame/dispatch
[::events/set-name "Heeeeeeyyy"])}
"Update name"]]))
Nice, let's look at our interface
With everything already done, it is easy to just dispatch the two new events and see the magic happens.
(defn main-panel []
(let [name (re-frame/subscribe [::subs/name])
counter (re-frame/subscribe [::subs/counter])]
[:div
[:h1
"Hello from " @name]
[:h2
(str "Counter " @counter)]
[:button {:on-click
#(re-frame/dispatch
[::events/set-name "Heeeeeeyyy"])}
"Update name"]
[:div
[:button {:on-click
#(re-frame/dispatch
[::events/inc-counter])} "Increment"]
[:button {:on-click
#(re-frame/dispatch
[::events/dec-counter])} "Decrement"]]]))
To clean up the main-panel
function I'll delete all hello world related stuff. This is a more cleaner version
(ns reframe-hello-world.views
(:require
[re-frame.core :as re-frame]
[reframe-hello-world.subs :as subs]
[reframe-hello-world.events :as events]))
(defn main-panel []
(let [counter (re-frame/subscribe [::subs/counter])]
[:div
[:h2
(str "Counter " @counter)]
[:div
[:button {:on-click
#(re-frame/dispatch
[::events/inc-counter])} "Increment"]
[:button {:on-click
#(re-frame/dispatch
[::events/dec-counter])} "Decrement"]]]))
And the browser responds with
I think that it is a pretty nice application, don't you?
##In a nutshell
- re-frame is a front-end framework. It is more like a version of Next.js for ClojureScript.
- re-frame implements a mechanism where we can subscribe, dispatch and manage events that could both update the application state or request any external api. This library is very powerfull, you can read more in the docs.