re-frame/docs/Debugging-Event-Handlers.md

4.7 KiB

Debugging Event Handlers

This page describes useful techniques for debugging re-frame's event handlers.

Event handlers are quite central to a re-frame app. Only event handlers can update app-db, to "step" an application "forward" from one state to the next.

The debug Interceptor

You might wonder: is my handler making the right changes to the value in app-db? Does it remove that entry? Does it increment that value?

During development, the built-in debug interceptor can be helpful in this regard. It shows, via console.log:

  1. the event being processed, for example: [:attempt-world-record true]
  2. the changes made to db by the handler in processing the event.

Regarding point 2, debug uses clojure.data/diff to compare the state of db before and after the handler ran. If you look at the docs, you'll notice that diff returns a triple, the first two of which are displayed by debug in console.log (the 3rd says what hasn't changed and isn't interesting).

Interceptors

So, now we have two middlewares to put on every handler: debug and log-ex.

At the top of our handlers.cljs we might define:

(def standard-middlewares  [log-ex debug])

And then include this standard-middlewares in every handler registration below:

(register-handler 
    :some-id
    standard-middlewares      ;;  <----  here!
    some-handler-fn)

No, wait. I don't want this debug middleware hanging about in my production version, just at develop time. And we still need those runtime exceptions going to airbrake.

So now, we make it:

(def standard-middlewares [ (if ^boolean goog.DEBUG log-ex log-ex-to-airbrake) 
                            (when ^boolean goog.DEBUG debug)]) 

Ha! I see a problem, you say. In production, that when is going to leave a nil in the vector. No problem. re-frame filters out nils.

Ha! Ha! I see another problem, you say. Some of my handlers have other middleware. One of them looks like this:

(register-handler 
    :ev-id
    (path :todos)       ;;  <-- already has middleware
    todos-fn)

How can I add this standard-middlewares where there is already middleware?

Like this:

(register-handler 
    :ev-id
    [standard-middlewares (path :todos)]       ;;  <--  put both in a vector
    todos-fn)

But that's a vector in a vector? Surely, that a problem?. Actually, no, re-frame will flatten any level of vector nesting, and remove nils before composing the resulting middleware.

3. Checking DB Integrity

I'd recommend always having a schema for your app-db, specifically a Prismatic Schema. If ever herbert is ported to clojurescript, it might be a good candidate too but, for the moment, a Prismatic Schema.

Schemas serve as invaluable documentation, plus ...

Once you have a schema for your app-db, you can check it is valid at any time. The most obvious time to recheck the integrity of app-db is immediately after a handler has changed it. In effect, we want to recheck after any handler has run.

Let's start with a schema and a way to validate a db against that schema. I would typically put this stuff in db.cljs.

(ns my.namespace.db
  (:require
    [schema.core :as s]))

;; As exactly as possible, describe the correct shape of app-db 
;; Add a lot of helpful comments. This will be an important resource
;; for someone looking at you code for the first time.
(def schema           
  {:a {:b s/Str
       :c s/Int}
   :d [{:e s/Keyword
        :f [s/Num]}]})

(defn valid-schema?
  "validate the given db, writing any problems to console.error"
  [db]
  (let [res (s/check schema db)]
    (if (some? res)
      (.error js/console (str "schema problem: " res)))))

Now, let's organise for our app-db to be validated against the schema after every handler. We'll use the built-in after middleware factory:

(def standard-middlewares [(if ^boolean goog.DEBUG log-ex log-ex-to-airbrake) 
                           (when ^boolean goog.DEBUG debug)
                           (when ^boolean goog.DEBUG (after db/valid-schema?))]) ;; <-- new

BTW, we could have written it without vectors, using comp:

(def standard-middlewares (if ^boolean goog.DEBUG               ;; not a vector
                            (comp log-ex debug (after db/valid-schema?))  ;; comp used
                            log-ex-to-airbrake))

Now, the instant a handler messes up the structure of app-db you'll be alerted. But this overhead won't be there in production.

These 3 steps will go a very long way in helping you to debug your event handlers.