Ian Jones Logo

re-frame

Clojure framework that leverages React.

Notes on Docs

Homoiconic

You program in data when you are programming in lisp.

data is the ultimate in late binding.

Re-frame is a data orienter design.

Everything is data:

  • events

  • effects

  • DOM

Interceptors (data) are preferred to middleware (higher order functions).

The data loop

Re-frame implements a perpetual loop.

You provide transforming functions to reframe on this loop.

tag line for re-frame: derived values, flowing

Data flows around this loop like water flows around the rain cycle.

Six Dominoes

There are 6 stages in the re-frame loop:

  1. Event dispatch

  2. Event handling

  3. Effect handling

  4. Query

  5. View

  6. DOM

Event Dispatch

event

sent when something happens

User clicks a button, websocket receives a message

Nothing occures without this first event.

reframe- is event driven

Event Handling

We must know what to do when a certain event comes in. Event handler functions will take the event and affect the world some how. This is also called as computing side effects.

Most of the time, the event will cause side effects to "application state".

Effect Handling

The side effects calculated in the previous step are actioned.

Data -> Action -> Mutate the world

Application state

re-frames state is held in a central in memory database.

any changes in state will trigger dominoes 4-5-6.

v = f(s). The view is a function of application state. This is where React comes in.

Query   ATTACH

This is where data is extracted from app state and given to the view functions in domino 5. This produces a signal graph thats passed to the view.

View

aka ViewFunctions - Reagent components.

uses the Hiccup format to represent the DOM.

The ViewFunctions subscribe to the signal graph from domino 4...reactively delivering state.

DOM

This is handled by Reagent/React for us.

Application State

This is called the app-db.

Well-formed Data at rest is as close to perfection in programming as it gets. All the crap that had to happen to put it there however...

— Fogus (@fogus) April 11, 2014

app-db is (reagent/atom {}).

ratom

A Reagent atom

Ratoms contained structured data. The db is the value (clojure map) stored inside the Ratom.

the app-db is created for you by re-frame

You can use any in memory database you'd like. If you want to use a Datascript database, you can use Posh.

When you store your data in one place, theres a single source of truth, rather than having a bunch of stateful components.

You can give your db a schema that validates all the data coming in.

Undo/redo is almost trivial to implement because the we can store a bunch of snapshots of the data. The snapshots use structural sharing so that memory is concerved.

app-db is primarily used as a local caching point for remote databases.

This feels a lot like Redux in flavor. It will be interesting to see how a different language could change my opinion on this pattern.

Code

Event Dispatch

You use re-frame.core/dispatch to dispatch an event. You pass a vector to the dispatch function [:delete-item item-id].

Event Handling

You register event handlers using re-frame.core/reg-event-fx.

(re-frame.core/reg-event-fx
 :delete-item
 h) ;; the actual event handler function.

h will take 2 arguments:

  1. a coeffects map, the data describing the current state of the world.

  2. the event, which contains the event name and other info in a vector.

h could be implemented like this:

(defn h                          ;; maybe choose a better name like `delete-item`
 [coeffects event]               ;; `coeffects` holds the current state of the world
 (let [item-id  (second event)   ;; extract id from event vector
       db       (:db coeffects)  ;; extract the current application state
       new-db   (dissoc-in db [:items item-id])]   ;; new app state
   {:db new-db}))                ;; a map of the necessary effects

You take the coeffects, and event and return a new coeffects, updated with your changes.

  1. obtain the state in the coeffects

  2. compute modified application state

  3. return modified application state in an effects map

Destructure the args for h:

(defn h
  [{:keys [db]} [_ item-id]]    ;; <--- new: obtain db and item-id directly
  {:db  (dissoc-in db [:items item-id])})    ;;

Effect Handling

Register effects with re-frame.core/reg-fx.

The event handler from above returned a map with only 1 key: :db. so it is specifying just one effect. If there are 2 keys returned, then you will have to register 2 effect handlers to handle those changes.

This is how you register the effect handlers for the :db:

(re-frame.core/reg-fx       ;; part of the re-frame API
  :db                       ;; the effects key
  (fn [val]                 ;; the handler function for the effect
    (reset! app-db val)))   ;; put the new value into the ratom app-db

This is mutating app-db. This is what effect handlers do. re-frame does not supply standard effect handlers, so you have to write them yourself.

Query

updating the db in the effect handler will trigger 4, 5, and 6 dominoes.

In this case, theres not a lot of data to extract. It is just plucking :items out of the db.

We use re-frame.core/reg-sub to register the query function.

The query function will take the database and the query vector as arguments:

(defn query-fn
  [db v]
  (:items db))

(re-frame.core/reg-sub
 :query-items
 query-fn)

View

This is where we subscribe to these query functions:

(defn items-view
  []
  (let [items (subscribe [:query-items])]
    [:div (map item-render @items)]))

This is the Hiccup format. subscrib queries can be parameterized:

(subscribe [:items "blue"]) You would have to write a more sophisticated query function to allow for querying by color.

DOM

DOM is generated from hiccup into Reagent/React.

Building my first Re-frame project

lein new re-frame <app-name> +cider

We use lein deps to install dependencies and lein watch to start the app.

Its not apprent how to create an on-click handler for hiccup.

It was some sort of formatting issue.