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

163 lines
5.7 KiB
Markdown
Raw Normal View History

2016-09-05 05:04:14 +00:00
## Debugging Event Handlers
2016-09-05 12:08:25 +00:00
This page describes techniques for debugging re-frame's event handlers.
2016-09-05 05:04:14 +00:00
Event handlers are quite central to a re-frame app. Only event handlers
2016-09-05 12:08:25 +00:00
can update `app-db`, to "step" an application "forward" from one state
to the next.
2016-09-05 05:04:14 +00:00
## 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
2016-09-05 12:08:25 +00:00
state of `db` before and after the handler ran, showing exactly what
mutation has happened.
If you [look at the docs for diff](https://clojuredocs.org/clojure.data/diff),
you'll notice it returns a triple, the first two of which
`debug` will display in `console.log` (the 3rd says what hasn't changed
2016-09-05 05:04:14 +00:00
and isn't interesting).
2016-09-05 12:08:25 +00:00
The output produced by `clojure.data/diff` can take some getting used to,
but you should stick with it -- your effort will be rewarded.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
### Using `debug`
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
So, you will add this interceptor to your event handlers like this:
2016-09-05 05:04:14 +00:00
```clj
2016-09-05 12:08:25 +00:00
(re-frame.core/reg-event-db
:some-id
[debug] ;; <---- here!
some-handler-fn)
2016-09-05 05:04:14 +00:00
```
2016-09-05 12:08:25 +00:00
Except, of course, we need a bit more subtly than that because
we only want `debug` to be present in development builds.
So it should be like this:
2016-09-05 05:04:14 +00:00
```clj
2016-09-05 12:08:25 +00:00
(re-frame.core/reg-event-db
:some-id
[(when ^boolean goog.DEBUG debug)] ;; <---- conditional!
some-handler-fn)
2016-09-05 05:04:14 +00:00
```
2016-09-05 12:08:25 +00:00
`goog.DEBUG` is a compile time constant provided by the `Google Closure Compiler`.
It will be `true` when the build within `project.clj` is `:optimization :none` and `false`
otherwise.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
Ha! I see a problem, you say. In production, that `when` is going to
leave a `nil` in the interceptor vector. No problem. re-frame filters out nils.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
### Too Much Repetition - Part 1
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
Remember that each event handler has its own interceptor stack.
All very flexible, but does that mean we have to repeat this `debug`
business on every single handler? Yes, it does. But there are
a couple of ways to make this pretty easy.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
Normally, standard interceptors are defined up the top of the `event.cljs` namespace:
2016-09-05 05:04:14 +00:00
```clj
2016-09-05 12:08:25 +00:00
(def standard-interceptors [(when ^boolean goog.DEBUG debug) other-interceptor])
2016-09-05 05:04:14 +00:00
```
2016-09-05 12:08:25 +00:00
And then, any one event handler, would look like:
2016-09-05 05:04:14 +00:00
```clj
2016-09-05 12:08:25 +00:00
(re-frame.core/reg-event-db
:some-id
[standard-interceptors specific-interceptor]
some-handler-fn)
2016-09-05 05:04:14 +00:00
```
2016-09-05 12:08:25 +00:00
Wait on! I see a problem, you say. `standard-interceptors` is a `vector`, and it
is within another `vector` allongside `specific-interceptor` - so that's
nested vectors of interceptors!
No problem, re-frame uses `flatten` to take out all the nesting - the
result is a simple chain of interceptors. Also, of course, nils are removed.
2016-09-05 05:04:14 +00:00
## 3. Checking DB Integrity
2016-09-05 12:08:25 +00:00
Always have a detailed schema for the data in `app-db`.
**First**, schemas serve as invaluable documentation. When I come to
a new app, the first thing I want to look at is the underlying
information model - the schema of the data. I hope it is well
commented and I expect it to be rigorous and complete, using
[Clojure spec](http://clojure.org/about/spec)
or, perhaps, [a Prismatic Schema](https://github.com/Prismatic/schema).
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
**Second** a good spec allows you to assert the integrity and correctness of
the data in app-db.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
When? Well, only event handlers can change what's in `app-db`, so only an event handler
could corrupt it. So, we'd like to recheck the integrity of `app-db` immediately
after **every** event handler has run.
2016-09-05 05:04:14 +00:00
2016-09-05 12:08:25 +00:00
This allows us to catch any errors very early, and easily assign blame (to an event handler).
Schemas are typically put into `db.cljs`. Here's an example using Prismatic Schema
(although a more modern choice would be to use [Clojure spec](http://clojure.org/about/spec)):
2016-09-05 05:04:14 +00:00
```clj
(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)))))
```
2016-09-05 12:08:25 +00:00
Now, let's organise for `valid-schema?` to be run after every handler. We'll use the built-in `after` interceptor factory:
2016-09-05 05:04:14 +00:00
```clj
2016-09-05 12:08:25 +00:00
(def standard-interceptors [(when ^boolean goog.DEBUG debug)
2016-09-05 05:04:14 +00:00
(when ^boolean goog.DEBUG (after db/valid-schema?))]) ;; <-- new
```
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.
2016-09-05 12:08:25 +00:00
### Too Much Repetition - Part 2
Above we discussed a way of "factoring out" common interceptors into `standard-interceptors`.
But there's a 2nd way to ensure that all event handlers get certain Interceptors: you write a custom registration function, like this:
```clj
(defn my-reg-event-db ;; alternative to reg-event-db
([id handler-fn]
(my-reg-event-db id nil handler-fn))
([id interceptors handler-fn]
(re-frame.core/reg-event-db
id
[(when ^boolean goog.DEBUG debug)
(when ^boolean goog.DEBUG (after db/valid-schema?))
interceptors]
handler-fn)))
```
From now on, you can register your event handlers like this:
```clj
(my-reg-event-db ;; <-- adds std interceptors automatically
:some-id
some-handler-fn)
```