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

196 lines
6.9 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
2016-09-07 05:40:52 +00:00
You might wonder: is my event handler making the right changes to the
value in `app-db`? Did it correctly remove that entry? Did it change that
2016-09-05 05:04:14 +00:00
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]`
2016-09-07 05:40:52 +00:00
2. the changes made to `db` by the handler in processing the event
2016-09-05 05:04:14 +00:00
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
2016-09-07 05:40:52 +00:00
`debug` will display in `console.log` because they show all changes
(the 3rd says what hasn't changed and isn't interesting).
2016-09-05 05:04:14 +00:00
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-07 05:40:52 +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
2016-09-07 05:40:52 +00:00
[debug] ;; <---- added here!
2016-09-05 12:08:25 +00:00
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
2016-09-07 05:40:52 +00:00
we only want `debug` to be present in development builds. We don't
want the overhead of those `clojure.data/diff` calculations in production.
So our code should be amended to look 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
2016-09-07 05:40:52 +00:00
leave a `nil` in the interceptor vector. So the Interceptor vector will be `[nil]`.
Surely that's a problem?
Well, actually, no it isn't. re-frame filters out `nil`.
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-07 05:40:52 +00:00
Each event handler has its own interceptor stack.
That might be all very flexible, but does that mean we have to put this `debug`
business on every single handler? That would be very repetitive.
Yes, you will have to put it on each handler. And, yes, that could be repetitive, unless
you take some steps.
2016-09-05 05:04:14 +00:00
2016-09-07 05:40:52 +00:00
One thing you an do is to define standard interceptors the top of the `event.cljs` namespace:
2016-09-05 05:04:14 +00:00
```clj
2016-09-07 05:40:52 +00:00
(def standard-interceptors [(when ^boolean goog.DEBUG debug) another-interceptor])
```
And then, for any one event handler, the code would look like:
```clj
(re-frame.core/reg-event-db
:some-id
standard-interceptors ;; <--- use the common definition
some-handler-fn)
2016-09-05 05:04:14 +00:00
```
2016-09-07 05:40:52 +00:00
or perhaps:
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
2016-09-07 05:40:52 +00:00
[standard-interceptors specific-interceptor] ;; mix with something specific
2016-09-05 12:08:25 +00:00
some-handler-fn)
2016-09-05 05:04:14 +00:00
```
2016-09-07 05:40:52 +00:00
So that `specific-interceptor` could be something required for just this one
event handler, and it can be combined the standard ones.
Wait on! "I see a problem", you say. `standard-interceptors` is a `vector`, and it
is within another `vector` allong side `specific-interceptor` - so that's
2016-09-05 12:08:25 +00:00
nested vectors of interceptors!
No problem, re-frame uses `flatten` to take out all the nesting - the
2016-09-07 05:40:52 +00:00
result is a simple chain of interceptors. And also, as we have discussed,
nils are removed.
2016-09-05 05:04:14 +00:00
## 3. Checking DB Integrity
2016-09-07 05:40:52 +00:00
Always have a detailed schema for the data in `app-db`!
Why?
2016-09-05 12:08:25 +00:00
**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
2016-09-07 05:40:52 +00:00
the data in `app-db`. Because all the data is in one place, that means you
are asserting the integrity of ALL the data in your app, at one time.
2016-09-05 05:04:14 +00:00
2016-09-07 05:40:52 +00:00
When should we do this? Ideally every time a change is made!
2016-09-05 05:04:14 +00:00
2016-09-07 05:40:52 +00:00
Well, it turns out that only event handlers can change the value 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 12:08:25 +00:00
2016-09-07 05:40:52 +00:00
This allows us to catch any errors very early, easily assigning blame (to the rouge event handler).
Schemas are typically put into `db.cljs` (see the todomvc example in the re-frame repo). Here's
an example using Prismatic Schema
2016-09-05 12:08:25 +00:00
(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]}]})
2016-09-07 05:40:52 +00:00
```
2016-09-05 05:04:14 +00:00
2016-09-07 05:40:52 +00:00
And a function which will check a db value against that schema:
```clj
2016-09-05 05:04:14 +00:00
(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-07 05:40:52 +00:00
Now, let's organise for `valid-schema?` to be run **after** every handler.
We'll use the built-in `after` Interceptor factory function:
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`.
2016-09-07 05:40:52 +00:00
But there's a 2nd way to ensure that all event handlers get certain Interceptors:
you write a custom registration function -- a replacement for `reg-event-db` -- like this:
2016-09-05 12:08:25 +00:00
```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)))
```
2016-09-07 05:40:52 +00:00
Notice that it inserts our two standard Interceptors.
From now on, you can register your event handlers like this and know that the two standard Interceptors have been inserted:
2016-09-05 12:08:25 +00:00
```clj
(my-reg-event-db ;; <-- adds std interceptors automatically
:some-id
some-handler-fn)
```