Merge branch 'develop'

This commit is contained in:
Daniel Compton 2016-12-15 15:54:39 +13:00
commit cee6d27373
74 changed files with 3764 additions and 1535 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
CHANGES.md merge=union

View File

@ -4,7 +4,11 @@
<option name="PER_PROJECT_SETTINGS">
<value>
<ClojureCodeStyleSettings>{
:cljs.core/with-redefs 1
:cursive.formatting/align-binding-forms true
:cursive.formatting/comment-align-column 0
:re-frame.trace/register-trace-cb :only-indent
:re-frame.trace/with-trace 1
}</ClojureCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />

View File

@ -1,9 +1,32 @@
## 0.8.1 (2016.08.XX) Unreleased
## 0.9.0 (2016.12.15)
Welcome Guests. Dr Ford has created a new [6-part narrative](README.md),
and Bernard [some infographics](/docs/EventHandlingInfographic.md). Anyone seen Dolores?
#### Headline
- The [README](README.md) and [/docs](/docs/README.md) have been substantially reworked.
- [#218](https://github.com/Day8/re-frame/issues/218) Make it okay to use `subscribe` in Form-1 components. This is a big deal.
#### Breaking
- Due to the new tracing features using `goog-define` (described below), re-frame now requires ClojureScript 1.7.48 or above. See [Parameterizing ClojureScript Builds](https://www.martinklepsch.org/posts/parameterizing-clojurescript-builds.html) for more information.
#### Improvements
- [#200](https://github.com/Day8/re-frame/pull/200) Remove trailing spaces from console logging
- [#200](https://github.com/Day8/re-frame/pull/200) Remove trailing spaces from console logging
- Add `re-frame.loggers/get-loggers` function to well, you know.
- Added `clear-subscription-cache!` function. This should be used when hot reloading code to ensure that any bad subscriptions that cause rendering exceptions are removed. See [reagent-project/reagent#272](https://github.com/reagent-project/reagent/issues/272) for more details.
- Added experimental tracing features. These are subject to change and remain undocumented at the moment. By default they are disabled, and will be completely compiled out by advanced optimisations. To enable them, set a [`:closure-defines`](https://www.martinklepsch.org/posts/parameterizing-clojurescript-builds.html) key to `{"re_frame.trace.trace_enabled_QMARK_" true}`
- [#223](https://github.com/Day8/re-frame/issues/223) When using `make-restore-fn`, dispose of any subscriptions that were created after the restore function was created.
- [#283](https://github.com/Day8/re-frame/pull/283) Make trim-v interceptor symmetrical, so it adds the missing event id back on to the `:event` coeffect in the `:after` function.
#### Fixes
- [#259](https://github.com/Day8/re-frame/pull/259) Fix a bug where registering a subscription would create and close over dependent subscriptions, meaning that they would never be garbage collected, and doing more work than necessary.
- Fix a bug where subscribing to a subscription that didn't exist would throw an exception, instead of returning nil.
- [#248](https://github.com/Day8/re-frame/pull/248) Provide after interceptor with `db` coeffect, if no `db` effect was produced.
- [#278](https://github.com/Day8/re-frame/issues/278) Provide enrich interceptor with `db` coeffect, if no `db` effect was produced.
## 0.8.0 (2016.08.19)
@ -67,7 +90,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
Reagent available). But you can debug your event handler tests using full JVM tooling goodness.
@samroberton and @escherize have provided the thought leadership and drive here. They converted
re-frame to `.cljc`, supplying plugable interop for both the `js` and `jvm` platforms.
re-frame to `.cljc`, supplying pluggable interop for both the `js` and `jvm` platforms.
Further, they have worked with @danielcompton to create a library of testing utilities which
will hopefully evolve into a nice step forward on both platforms: <br>
@ -93,7 +116,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
successful part of the framework. We thought we were happy.
But recently @steveb8n gave a cljsyd talk on
Pedistal's Interceptor pattern which suddenly transformed them from
Pedestal's Interceptor pattern which suddenly transformed them from
arcane to delightfully simple in 20 mins. Interceptors are
really "middleware via data" rather than "middleware via higher order functions".
So it is another way of doing the same thing, but thanks to @steveb8n
@ -176,7 +199,7 @@ Joking aside, this is a substantial release which will change how you use re-fra
Breaking:
- removed middleware `log-ex`. It is no longer needed because browsers now correctly report the
throw site of re-thown exceptions. In the unlikely event that you absolutely still need it,
throw site of re-thrown exceptions. In the unlikely event that you absolutely still need it,
the source for `log-ex` is still in `middleware.cljs`, commented out. Just transfer it to your project.
- `debug` middleware now produces slightly different output (to console). So no code will need to change,
@ -197,8 +220,8 @@ Fixed:
New API:
- [#118](https://github.com/Day8/re-frame/pull/118) - Add `add-post-event-callback` to the API.
@pupeno is developing [preprender](https://carouselapps.com/prerenderer) which looks pretty neat.
Support this effort by adding a way for preprender to hook event processing.
@pupeno is developing [prerenderer](https://carouselapps.com/prerenderer) which looks pretty neat.
Support this effort by adding a way for prerenderer to hook event processing.
- `on-changes` middleware now official. No longer experimental.
@ -250,7 +273,7 @@ Headline:
- mean apps, in production, stand a chance of reporting UHE
to the user, and can perhaps even recover to a sane state.
- #53 Fix Logging And Error Reporting
You can now provide your own logging fucntions.
You can now provide your own logging functions.
Further explanation [here](https://github.com/Day8/re-frame/wiki/FAQ#3-can-re-frame-use-my-logging-functions).
Deprecated:

View File

@ -12,7 +12,6 @@ the [ClojureScript mailing list](https://groups.google.com/forum/#!forum/clojure
**Create pull requests to the develop branch**, work will be merged onto master when it is ready to be released.
## Running tests
#### Via Browser/HTML
@ -60,3 +59,9 @@ If possible provide:
* Docstrings for functions
* Documentation examples
* Add the change to the Unreleased section of [CHANGES.md](CHANGES.md)
## Pull requests for docs
* Make your documentation changes
* (Optional) Install doctoc with `npm install -g doctoc`
* (Optional) Regenerate the docs TOC with `bin/doctoc.sh` or `bin/doctoc.bat` depending on your OS

1417
README.md

File diff suppressed because it is too large Load Diff

6
bin/doctoc.bat Executable file
View File

@ -0,0 +1,6 @@
:: Table of contents are generated by doctoc.
:: Install doctoc with `npm install -g doctoc`
:: Then run this script to regenerate the TOC after
:: editing the docs.
doctoc ./docs/ --github --title '## Table Of Contents'

7
bin/doctoc.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Table of contents are generated by doctoc.
# Install doctoc with `npm install -g doctoc`
# Then run this script to regenerate the TOC after
# editing the docs.
doctoc $(dirname $0)/../docs --github --title '## Table Of Contents'

128
docs/ApplicationState.md Normal file
View File

@ -0,0 +1,128 @@
- [On Data](#on-data)
- [The Big Ratom](#the-big-ratom)
- [The Benefits Of Data-In-The-One-Place](#the-benefits-of-data-in-the-one-place)
#### Table Of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [On Data](#on-data)
- [The Big Ratom](#the-big-ratom)
- [The Benefits Of Data-In-The-One-Place](#the-benefits-of-data-in-the-one-place)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### On Data
<blockquote class="twitter-tweet" lang="en"><p>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...</p>&mdash; Fogus (@fogus) <a href="https://twitter.com/fogus/status/454582953067438080">April 11, 2014</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
### The Big Ratom
re-frame puts all your application state into one place, which is
called `app-db`.
Ideally, you will provide a spec for this data-in-the-one-place,
[using a powerful and leverageable schema](http://clojure.org/about/spec).
Now, this advice is not the slightest bit controversial for 'real' databases, right?
You'd happily put all your well-formed data into PostgreSQL.
But within a running application (in memory), there can be hesitation. If you have
a background in OO, this data-in-one-place
business is a really, really hard one to swallow. You've
spent your life breaking systems into pieces, organised around behaviour and trying
to hide state. I still wake up in a sweat some nights thinking about all
that Clojure data lying around exposed and passive.
But, as Fogus reminds us, data at rest is quite perfect.
In re-frame, `app-db` is one of these:
```clj
(def app-db (reagent/atom {})) ;; a Reagent atom, containing a map
```
Although it is a `Reagent atom` (hereafter `ratom`), I'd encourage
you to think of it as an in-memory database. It will contain structured data.
You will need to query that data. You will perform CRUD
and other transformations on it. You'll often want to transact on this
database atomically, etc. So "in-memory database"
seems a more useful paradigm than plain old map-in-atom.
Further Notes:
1. `app-state` would probably be a more accurate name, but I choose `app-db` instead because
I wanted to convey the database notion as strongly as possible.
2. In the documentation and code, I make a distinction between `app-db` (the `ratom`) and
`db` which is the (map) `value` currently stored **inside** this `ratom`. Be aware of that naming.
3. re-frame creates and manages an `app-db` for you, so
you don't need to declare one yourself (see the 1st FAQ if you want to inspect the value it holds).
4. `app-db` doesn't actually have to be a `ratom` containing a map. It could, for example,
be a [datascript](https://github.com/tonsky/datascript database). In fact, any database which
can signal you when it changes would do. We'd love! to be using [datascript](https://github.com/tonsky/datascript database) - so damn cool -
but we had too much data in our apps. If you were to use it, you'd have to tweak re-frame a bit and use [Posh](https://github.com/mpdairy/posh).
### The Benefits Of Data-In-The-One-Place
1. Here's the big one: because there is a single source of truth, we write no
code to synchronize state between many different stateful components. I
cannot stress enough how significant this is. You end up writing less code
and an entire class of bugs is eliminated.
(This mindset is very different to OO which involves
distributing state across objects, and then ensuring that state is synchronized, all the while
trying to hide it, which is, when you think about it, quite crazy ... and I did it for years).
2. Because all app state is coalesced into one atom, it can be updated
with a single `reset!`, which acts like a transactional commit. There is
an instant in which the app goes from one state to the next, never a series
of incremental steps which can leave the app in a temporarily inconsistent, intermediate state.
Again, this simplicity causes a certain class of bugs or design problems to evaporate.
3. The data in `app-db` can be given a strong schema
so that, at any moment, we can validate all the data in the application. **All of it!**
We do this check after every single "event handler" runs (event handlers compute new state).
And this enables us to catch errors early (and accurately). It increases confidence in the way
that Types can increase confidence, only [a good schema can provide more
**leverage** than types](https://www.youtube.com/watch?v=nqY4nUMfus8).
4. Undo/Redo [becomes straight forward to implement](https://github.com/Day8/re-frame-undo).
It is easy to snapshot and restore one central value. Immutable data structures have a
feature called `structural sharing` which means it doesn't cost much RAM to keep the last, say, 200
snapshots. All very efficient.
For certain categories of applications (eg: drawing applications) this feature is borderline magic.
Instead of undo/redo being hard, disruptive and error prone, it becomes trivial.
**But,** many web applications are not self contained
data-wise and, instead, are dominated by data sourced from an authoritative remote database.
For these applications, re-frame's `app-db` is mostly a local caching
point, and being able to do undo/redo its state is meaningless because the authoritative
source of data is elsewhere.
5. The ability to genuinely model control via FSMs (discussed later).
6. The ability to do time travel debugging, even in a production setting. More soon.
### Get You A Leveragable Schema
You really need a schema for `app-db`.
Of course, that means you'll have to learn [spec](http://clojure.org/about/spec) and there's
some overhead in that, so maybe, just maybe, in your initial experiments, you can
get away without one. But not for long. Promise me you'll write a `spec`. Good.
The [todomvc example](https://github.com/Day8/re-frame/tree/master/examples/todomvc)
shows how to use a spec. Look in `src/db.cljs` for the spec itself, and then in `src/events.cljs` for
how to write code which checks `app-db` against this spec after every single event has been
processed.
Specs are more leveragable than types. Watch how: <br>
https://www.youtube.com/watch?v=VNTQ-M_uSo8
***
Previous: [This Repo's README](../README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [First Code Walk-Through](CodeWalkThrough.md)

View File

@ -1,3 +1,13 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Simpler Apps](#simpler-apps)
- [There's A Small Gotcha](#theres-a-small-gotcha)
- [Larger Apps](#larger-apps)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Simpler Apps
To build a re-frame app, you:
@ -58,7 +68,8 @@ src
Continue to [Navigation](Navigation.md) to learn how to switch between panels of a larger app.
---
***
Previous: [CoEffects](coeffects.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Navigation](Navigation.md)

514
docs/CodeWalkthrough.md Normal file
View File

@ -0,0 +1,514 @@
## Initial Code Walk-through
At this point, you are about 50% of the way to understanding re-frame. You are armed with:
- a high level understanding of the 6 domino process (from re-frame's README)
- an understanding of application state (from the previous tutorial)
By the end of this tutorial, you'll be at 70%, which is good
enough to start coding by yourself.
In this tutorial, **we'll look at re-frame code**.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [What Code?](#what-code)
- [What Does It Do?](#what-does-it-do)
- [Namespace](#namespace)
- [Data Schema](#data-schema)
- [Events (domino 1)](#events-domino-1)
- [dispatch](#dispatch)
- [After dispatch](#after-dispatch)
- [Event Handlers (domino 2)](#event-handlers-domino-2)
- [reg-event-db](#reg-event-db)
- [:initialize](#initialize)
- [:timer](#timer)
- [:time-color-change](#time-color-change)
- [Effect Handlers (domino 3)](#effect-handlers-domino-3)
- [Subscription Handlers (domino 4)](#subscription-handlers-domino-4)
- [reg-sub](#reg-sub)
- [View Functions (domino 5)](#view-functions-domino-5)
- [Hiccup](#hiccup)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## What Code?
This repo contains an example application called ["simple"](https://github.com/Day8/re-frame/tree/develop/examples/simple),
which has around 70 lines of code. We'll look at every line of [the file](https://github.com/Day8/re-frame/blob/develop/examples/simple/src/simple/core.cljs).
This app:
- displays the current time in a nice big, colourful font
- provides a single text input field, into which you can type a hex colour code,
like "#CCC", used for the time display
Here's what it looks like:
![Example App image](../images/example_app.png)
To run the code:
* Install Java 8
* Install leiningen (http://leiningen.org/#install)
Then:
1. `git clone https://github.com/Day8/re-frame.git`
2. `cd re-frame/examples/simple`
3. `lein do clean, figwheel`
4. open http://localhost:3449/example.html
## Namespace
Because this example is tiny, the code is in a single namespace which you can find here:
https://github.com/Day8/re-frame/blob/master/examples/simple/src/simpleexample/core.cljs
Within this namespace, we'll need access to both `reagent` and `re-frame`.
So, at the top, we start like this:
```clj
(ns simple.core
(:require [reagent.core :as reagent]
[re-frame.core :as rf]))
```
## Data Schema
Now, normally, I'd strongly recommended you write a quality schema
for your application state (the data stored in `app-db`). But,
here, to minimise cognitive load, we'll cut that corner.
But ... we can't cut it completely. You'll still need an
informal description, and here it is ... for this app `app-db` will contain
a two-key map like this:
```cljs
{:time (js/Date.) ;; current time for display
:time-color "#f88"} ;; the colour in which the time should be be shown
```
re-frame itself owns/manages `app-db` (see FAQ #1), and it will
supply the value within it (a two-key map in this case)
to your various handlers as required.
## Events (domino 1)
Events are data. You choose the format.
re-frame uses a vector
format for events. For example:
```clj
[:delete-item 42]
```
The first element in the vector identifies the `kind` of `event`. The
further elements are optional, and can provide additional data
associated with the event. The additional value above, `42`, is
presumably the id of the item to delete.
Here are some other example events:
```clj
[:admit-to-being-satoshi false]
[:set-spam-wanted false :continue-harassment-nevertheless-flag]
[:some-ns/on-GET-success response]
```
The `kind` of event is always a keyword, and for non-trivial
applications it tends to be namespaced.
**Rule**: events are pure data. No sneaky tricks like putting
callback functions on the wire. You know who you are.
### dispatch
To send an event, call `dispatch` with the event vector as argument:
```clj
(dispatch [:event-id value1 value2])
```
In this "simple" app, a `:timer` event is dispatched every second:
```clj
(defn dispatch-timer-event
[]
(let [now (js/Date.)]
(rf/dispatch [:timer now]))) ;; <-- dispatch used
;; call the dispatching function every second
(defonce do-timer (js/setInterval dispatch-timer-event 1000))
```
This is an unusual source of events. Normally, it is an app's UI widgets which
`dispatch` events (in response to user actions), or an HTTP POST's
`on-success` handler, or a websocket which gets a new packet.
### After dispatch
`dispatch` puts an event into a queue for processing.
So, **an event is not processed synchronously, like a function call**. The processing
happens **later** - asynchronously. Very soon, but not now.
The consumer of the queue is a `router` which looks after the event's processing.
The `router`:
1. inspects the 1st element of an event vector
2. looks in a registry for the event handler which is **registered**
for this kind of event
3. calls that event handler with the necessary arguments
As a re-frame app developer, your job, then, is to write and register a handler
for each kind of event.
## Event Handlers (domino 2)
Collectively, event handlers provide the control logic in a re-frame application.
In this application, 3 kinds of event are dispatched:
`:initialise`
`:time-color-change`
`:timer`
3 events means we'll be registering 3 event handlers.
### Two ways To register
Event handlers can be registered via either `reg-event-db`
or `reg-event-fx` (`-db` vs `-fx`).
Handler functions take coeffects (input args) and return `effects`,
however `reg-event-db` allows you to write simpler handlers.
The
handler functions it registers
(1) take just one coeffect - the current app state and (2) return only one `effect` -
the updated app state.
Whereas `reg-event-fx` registered handlers are more flexible.
Because of its simplicity, we'll be using the former here.
### reg-event-db
We register event handlers using re-frame's `reg-event-db`.
`reg-event-db` is used like this:
```clj
(reg-event-db
:the-event-id
the-event-handler-fn)
```
The handler function you provide should expect two parameters:
- `db` the current application state (contents of `app-db`)
- `v` the event vector
So, your function will have a signature like this: `(fn [db v] ...)`.
Each event handler must compute and return the new state of
the application, which means it normally returns a
modified version of `db`.
### :initialize
On startup, application state must be initialised. We
want to put a sensible value into `app-db` which will
otherwise contain `{}`.
So a `(dispatch [:initialize])` will happen early in the
apps life (more on this below), and we need to write an `event handler`
for it.
Now this event handler is slightly unusual because it doesn't
much care about the existing value in `db` - it just wants to plonk
in a new complete value.
Like this:
```clj
(rf/reg-event-db ;; sets up initial application state
:initialize
(fn [_ _] ;; the two parameters are not important here, so use _
{:time (js/Date.) ;; What it returns becomes the new application state
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
```
This particular handler `fn` ignores the two parameters
(usually called `db` and `v`) and simply returns
a map literal, which becomes the application
state.
Here's an alternative way of writing it which does pay attention to the existing value of `db`:
```clj
(rf/reg-event-db
:initialize
(fn [db _] ;; we use db this time, so name it
(-> db
(assoc :time (js/Date.))
(assoc :time-color "#f88")))
```
### :timer
Earlier, we set up a timer function to `(dispatch [:timer now])` every second.
Here's how we handle it:
```clj
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
:timer
(fn [db [_ new-time]] ;; <-- de-structure the event vector
(assoc db :time new-time))) ;; compute and return the new application state
```
Notes:
1. the `event` will be like `[:timer a-time]`, so the 2nd `v` parameter
destructures to extract the `a-time` value
2. the handler computes a new application state from `db`, and returns it
### :time-color-change
When the user enters a new colour value (via an input text box):
```clj
(rf/reg-event-db
:time-color-change ;; usage: (dispatch [:time-color-change 34562])
(fn [db [_ new-color-value]]
(assoc db :time-color new-color-value))) ;; compute and return the new application state
```
## Effect Handlers (domino 3)
Domino 3 actions/realises the `effects` returned by event handlers.
In this "simple" application, our event handlers are implicitly returning
only one effect: "update application state".
This particular `effect` is actioned by a re-frame supplied
`effect handler`. **So, there's nothing for us to do for this domino**. We are
using a standard re-frame effect handler.
And this is not unusual. You'll seldom have to write `effect handlers`, but
we'll understand more about them in a later tutorial.
## Subscription Handlers (domino 4)
Subscription handlers take application state as an argument,
and they compute a query over it, returning something of
a "materialised view" of that application state.
When the application state changes, subscription functions are
re-run by re-frame, to compute new values (a new materialised view).
Ultimately, the data returned by `query` functions is used
in the `view` functions (Domino 5).
One subscription can
source data from other subscriptions. So it is possible to
create a tree of dependencies.
The Views (Domino 5) are the leaves of this tree The tree's
root is `app-db` and the intermediate nodes between the two
are computations being performed by the query functions of Domino 4.
Now, the two examples below are trivial. They just extract part of the application
state and return it. So, there's virtually no computation. A more interesting tree
of subscriptions and more explanation can be found in the todomvc example.
### reg-sub
`reg-sub` associates a `query id` with a function that computes
that query, like this:
```clj
(reg-sub
:some-query-id ;; query id (used later in subscribe)
a-query-fn) ;; the function which will compute the query
```
If, later, a view function subscribes to a query like this:
`(subscribe [:some-query-id])` ;; note use of `:some-query-id`
then `a-query-fn` will be used to perform the query over the application state.
Each time application state changes, `a-query-fn` will be
called again to compute a new materialised view (a new computation over app state)
and that new value will be given to any view function which is subscribed
to `:some-query-id`. This view function, itself, will then also be called again
to compute new DOM (because it depends on a query value which changed).
Along this reactive chain of dependencies, re-frame will ensure the
necessary calls are made, at the right time.
Here's the code:
```clj
(rf/reg-sub
:time
(fn [db _] ;; db is current app state. 2nd unused param is query vector
(:time db))) ;; return a query computation over the application state
(rf/reg-sub
:time-color
(fn [db _]
(:time-color db)))
```
Like I said, both of these queries are trivial. See `todomvc.subs.clj` for more interesting ones.
## View Functions (domino 5)
`view` functions turn data into DOM. They are "State in, Hiccup out" and they are Reagent components.
Any SPA will have lots of `view`functions, and collectively,
they render the app's entire UI.
### Hiccup
`Hiccup` is a data format for representing HTML.
Here's a trivial view function which returns hiccup-formatted data:
```clj
(defn greet
[]
[:div "Hello viewers"]) ;; means <div>Hello viewers</div>
```
And if we call it:
```clj
(greet)
;; ==> [:div "Hello viewers"]
(first (greet))
;; ==> :div
```
Yep, that's a vector with two elements: a keyword and a string.
Now,`greet` is pretty simple because it only has the "Hiccup Out" part. There's no "Data In".
### Subscribing
To render the DOM representation of some-part-of app state, view functions must query
for that part of `app-db`, and that means using `subscribe`.
`subscribe` is always called like this:
```Clojure
(subscribe [query-id some optional query parameters])
```
There's only one (global) `subscribe` function and it takes one argument, assumed to be a vector.
The first element in the vector (shown above as `query-id`) identifies/names the query
and the other elements are optional
query parameters. With a traditional database a query might be:
```
select * from customers where name="blah"
```
In re-frame, that would be done as follows:
`(subscribe [:customer-query "blah"])`
which would return a `ratom` holding the customer state (a value which might change over time!).
> Because subscriptions return a ratom, they must always be dereferenced to
obtain the value. This is a recurring trap for newbies.
### The View Functions
This view function renders the clock:
```clj
(defn clock
[]
[:div.example-clock
{:style {:color @(rf/subscribe [:time-color])}}
(-> @(rf/subscribe [:time])
.toTimeString
(clojure.string/split " ")
first)])
```
As you can see, it uses `subscribe` twice to obtain two pieces of data from `app-db`.
If either change, re-frame will re-run this view function.
And this view function renders the input field:
```clj
(defn color-input
[]
[:div.color-input
"Time color: "
[:input {:type "text"
:value @(rf/subscribe [:time-color]) ;; subscribe
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
```
Notice how it does BOTH a `subscribe` to obtain the current value AND a `dispatch` to say when it has changed.
It is very common for view functions to render event-dispatching functions. The user's interaction with
the UI is usually the largest source of events.
And then something more standard:
```clj
(defn ui
[]
[:div
[:h1 "Hello world, it is now"]
[clock]
[color-input]])
```
Note: `view` functions tend to be organised into a hierarchy, often with data flowing from parent to child via
parameters. So, not every view function needs a subscription. Very often the values passed in from a parent component
are sufficient.
Note: a view function should never directly access `app-db`. Data is only ever sourced via
### Components Like Templates?
`view` functions are like the templates you'd find in
Django, Rails, Handlebars or Mustache -- they map data to HTML -- except for two massive differences:
1. you have the full power of ClojureScript available to you (generating a Clojure data structure). The
downside is that these are not "designer friendly" HTML templates.
2. these templates are reactive. When their input Signals change, they
are automatically rerun, producing new DOM. Reagent adroitly shields you from the details, but
the renderer of any `component` is wrapped by a `reaction`. If any of the the "inputs"
to that render change, the render is rerun.
## Kick Starting The App
Below, `run` is the called when the HTML page has loaded
to kick off the application.
It has two tasks:
1. load the initial application state
2. "mount" the GUI onto an existing DOM element.
```clj
(defn ^:export run
[]
(rf/dispatch-sync [:initialize]) ;; puts a value into application state
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
(js/document.getElementById "app")))
```
After `run` is called, the app passively waits for events.
Nothing happens without an `event`.
When it comes to establishing initial application state, you'll
notice the use of `dispatch-sync`, rather than `dispatch`. This is something of
cheat which ensures a correct
structure exists in `app-db` before any subscriptions or event handlers run.
## Summary
**Your job**, when building an app, is to:
- design your app's information model (data and schema layer)
- write and register event handler functions (control and transition layer) (domino 2)
- (once in a blue moon) write and register effect and coeffect handler
functions (domino 3) which do the mutative dirty work of which we dare not
speak in a pure, immutable functional context. Most of the time, you'll be
using standard, supplied ones.
- write and register query functions which implement nodes in a signal graph (query layer) (domino 4)
- write Reagent view functions (view layer) (domino 5)
## Further Code
You should also look at the [todomvc example application](https://github.com/Day8/re-frame/tree/develop/examples/todomvc).
***
Previous: [app-db (Application State)](ApplicationState.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Mental Model Omnibus](MentalModelOmnibus.md)

View File

@ -4,24 +4,26 @@ This tutorial explains `coeffects`.
It explains what they are, how they can be "injected", and how
to manage them in tests.
## Table Of Contexts
* [What Are They?](#what-are-they-)
* [An Example](#an-example)
* [How We Want It](#how-we-want-it)
* [Abracadabra](#abracadabra)
* [Which Interceptors?](#which-interceptors-)
* [`inject-cofx`](#-inject-cofx-)
* [More `inject-cofx`](#more--inject-cofx-)
* [Meet `reg-cofx`](#meet--reg-cofx-)
* [Example Of `reg-cofx`](#example-of--reg-cofx-)
* [Another Example Of `reg-cofx`](#another-example-of--reg-cofx-)
* [Secret Interceptors](#secret-interceptors)
* [Testing](#testing)
* [The 5 Point Summary](#the-5-point-summary)
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
## Coeffects
- [What Are They?](#what-are-they)
- [An Example](#an-example)
- [How We Want It](#how-we-want-it)
- [Abracadabra](#abracadabra)
- [Which Interceptors?](#which-interceptors)
- [`inject-cofx`](#inject-cofx)
- [More `inject-cofx`](#more-inject-cofx)
- [Meet `reg-cofx`](#meet-reg-cofx)
- [Example Of `reg-cofx`](#example-of-reg-cofx)
- [Another Example Of `reg-cofx`](#another-example-of-reg-cofx)
- [Secret Interceptors](#secret-interceptors)
- [Testing](#testing)
- [The 5 Point Summary](#the-5-point-summary)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### What Are They?
@ -30,7 +32,7 @@ to perform its computation.
Because the majority of event handlers only need `db` and
`event`, there's a specific registration function, called `reg-event-db`,
which delivers these two coeffects as arguments to an event
which delivers ONLY these two coeffects as arguments to an event
handler, making this common case easy to program.
But sometimes an event handler needs other data inputs
@ -60,7 +62,7 @@ pure, and impure functions cause well-documented paper cuts.
Our goal in this tutorial will be to rewrite this event handler so
that it __only__ uses data from arguments. This will take a few steps.
The first is that we first switch to
The first is that we switch to
using `reg-event-fx` (instead of `reg-event-db`).
Event handlers registered via `reg-event-fx` are slightly
@ -86,22 +88,22 @@ right value. Nice! But how do we make this magic happen?
### Abracadabra
Each time an event handler is executed, a brand new `context`
is created, and within that `context` is a brand new `:coeffect`
is created, and within that `context` is a brand new `:coeffects`
map, which is initially totally empty.
That pristine `context` value (containing a pristine `:coeffect` map) is threaded
That pristine `context` value (containing a pristine `:coeffects` map) is threaded
through a chain of Interceptors before it finally reaches our event handler,
sitting on the end of a chain, itself wrapped up in an interceptor. We know
this story well from a previous tutorial.
So, all members of the Interceptor chain have the opportunity to add to `:coeffects`
via their `:before` function. This is where `:coeffect` magic happens. This is how
new keys can be added to `:coeffect`, so that later our event handler magically finds the
via their `:before` function. This is where `:coeffects` magic happens. This is how
new keys can be added to `:coeffects`, so that later our event handler magically finds the
right data (like `:local-store`) in its `cofx` argument. It is the Interceptors.
### Which Interceptors?
If Interceptors put data in `:coeffect`, then we'll need to add the right ones
If Interceptors put data in `:coeffects`, then we'll need to add the right ones
when we register our event handler.
Something like this (this handler is the same as before, except for one detail):
@ -125,7 +127,7 @@ to our event handler (`cofx`).
`inject-cofx` is part of the re-frame API.
It is a function which returns an Interceptor whose `:before` function loads
a key/value pair into a `context's` `:coeffect` map.
a key/value pair into a `context's` `:coeffects` map.
`inject-cofx` takes either one or two arguments. The first is always the `id` of the coeffect
required (called a `cofx-id`). The 2nd is an optional addition value.
@ -160,7 +162,7 @@ Each `cofx-id` requires a different action.
This function is also part of the re-frame API.
It allows you to associate a`cofx-id` (like `:now` or `:local-store`) with a
It allows you to associate a `cofx-id` (like `:now` or `:local-store`) with a
handler function that injects the right key/value pair.
The function you register will be passed two arguments:
@ -267,7 +269,8 @@ In note form:
5. We must have previously registered a cofx handler via `reg-cofx`
---
***
Previous: [Effects](Effects.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Basic App Structure](Basic-App-Structure.md)

View File

@ -6,42 +6,52 @@ 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.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [The `debug` Interceptor](#the-debug-interceptor)
- [Using `debug`](#using-debug)
- [Too Much Repetition - Part 1](#too-much-repetition---part-1)
- [3. Checking DB Integrity](#3-checking-db-integrity)
- [Too Much Repetition - Part 2](#too-much-repetition---part-2)
- [What about the -fx variation?](#what-about-the--fx-variation)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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?
You might wonder: is my event handler making the right changes to `app-db`?
During development, the built-in `debug` interceptor can be helpful
in this regard. It shows, via `console.log`:
During development, the built-in `debug` interceptor can help.
It writes to `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.
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, showing exactly what
mutation has happened.
`debug` uses `clojure.data/diff` to compare `app-db`
before and after the handler ran, showing what changed.
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
and isn't interesting).
[clojure.data/diff returns a triple](https://clojuredocs.org/clojure.data/diff)
, the first two entries of which
`debug` will display in `console.log` (the 3rd says what hasn't changed and isn't interesting).
The output produced by `clojure.data/diff` can take some getting used to,
but you should stick with it -- your effort will be rewarded.
### Using `debug`
So, you will add this interceptor to your event handlers like this:
So, you will add this Interceptor like this:
```clj
(re-frame.core/reg-event-db
:some-id
[debug] ;; <---- here!
[debug] ;; <---- added here!
some-handler-fn)
```
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:
Except, of course, we need to be more deft - we only want
`debug` in development builds. We don't
want the overhead of those `clojure.data/diff` calculations in production.
So, this is better:
```clj
(re-frame.core/reg-event-db
:some-id
@ -54,38 +64,58 @@ It will be `true` when the build within `project.clj` is `:optimization :none` a
otherwise.
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.
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`.
### Too Much Repetition - Part 1
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.
Each event handler has its own interceptor stack.
Normally, standard interceptors are defined up the top of the `event.cljs` namespace:
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.
One thing you an do is to define standard interceptors the top of the `event.cljs` namespace:
```clj
(def standard-interceptors [(when ^boolean goog.DEBUG debug) other-interceptor])
(def standard-interceptors [(when ^boolean goog.DEBUG debug) another-interceptor])
```
And then, any one event handler, would look like:
And then, for any one event handler, the code would look like:
```clj
(re-frame.core/reg-event-db
:some-id
[standard-interceptors specific-interceptor]
standard-interceptors ;; <--- use the common definition
some-handler-fn)
```
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
or perhaps:
```clj
(re-frame.core/reg-event-db
:some-id
[standard-interceptors specific-interceptor] ;; mix with something specific
some-handler-fn)
```
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` along side `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.
result is a simple chain of interceptors. And also, as we have discussed,
nils are removed.
## 3. Checking DB Integrity
Always have a detailed schema for the data in `app-db`.
Always have a detailed schema for the data in `app-db`!
Why?
**First**, schemas serve as invaluable documentation. When I come to
a new app, the first thing I want to look at is the underlying
@ -96,15 +126,20 @@ or, perhaps, [a Prismatic Schema](https://github.com/Prismatic/schema).
**Second** a good spec allows you to assert the integrity and correctness of
the data in app-db.
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.
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.
When should we do this? Ideally every time a change is made!
This allows us to catch any errors very early, and easily assign blame (to an event handler).
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**.
Schemas are typically put into `db.cljs`. Here's an example using Prismatic Schema
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
(although a more modern choice would be to use [Clojure spec](http://clojure.org/about/spec)):
```clj
(ns my.namespace.db
@ -119,7 +154,10 @@ Schemas are typically put into `db.cljs`. Here's an example using Prismatic Sche
:c s/Int}
:d [{:e s/Keyword
:f [s/Num]}]})
```
And a function which will check a db value against that schema:
```clj
(defn valid-schema?
"validate the given db, writing any problems to console.error"
[db]
@ -128,19 +166,21 @@ Schemas are typically put into `db.cljs`. Here's an example using Prismatic Sche
(.error js/console (str "schema problem: " res)))))
```
Now, let's organise for `valid-schema?` to be run after every handler. We'll use the built-in `after` interceptor factory:
Now, let's organise for `valid-schema?` to be run **after** every handler.
We'll use the built-in `after` Interceptor factory function:
```clj
(def standard-interceptors [(when ^boolean goog.DEBUG debug)
(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.
### 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:
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:
```clj
(defn my-reg-event-db ;; alternative to reg-event-db
([id handler-fn]
@ -154,9 +194,37 @@ But there's a 2nd way to ensure that all event handlers get certain Interceptors
handler-fn)))
```
From now on, you can register your event handlers like this:
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:
```clj
(my-reg-event-db ;; <-- adds std interceptors automatically
:some-id
some-handler-fn)
```
### What about the -fx variation?
Above we created `my-reg-event-db` as a new registration function for `-db` handlers.
That's handlers which take `db` and `event` arguments, and return a new `db`.
So, they MUST return a new `db` value - which should be validated.
But what if we tried to do the same for `-fx` handlers, which return instead
an `effects` map which may, or may not, contain an `:db`? Our solution would
have to allow for the absence of a new `db` value (by doing no validity check, because nothing
was being changed).
```clj
(defn my-reg-event-fx ;; 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-fx
id
[(when ^boolean goog.DEBUG debug)
(when ^boolean goog.DEBUG (after #(if % (db/valid-schema? %))))
interceptors]
handler-fn)))
```
Actually, it would probably be better to write an alternative `after` which

View File

@ -2,7 +2,26 @@
This page describes a technique for
debugging re-frame apps. It proposes a particular combination
of tools. By the end, you'll be better at dominos.
of tools.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Know The Beast!](#know-the-beast)
- [re-frame's Step 3](#re-frames-step-3)
- [Observe The Beast](#observe-the-beast)
- [How To Trace?](#how-to-trace)
- [Your browser](#your-browser)
- [Your Project](#your-project)
- [Say No To Anonymous](#say-no-to-anonymous)
- [IMPORTANT](#important)
- [The result](#the-result)
- [Warning](#warning)
- [React Native](#react-native)
- [Appendix A - Prior to V0.8.0](#appendix-a---prior-to-v080)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Know The Beast!
@ -59,8 +78,8 @@ as luck would have it, ClojureScript is a lisp and it is readily **traceable**.
## How To Trace?
Below, I suggest a particular combination of technologies which, working together,
will write a trace to the devtools console. Sorry, but there's no fancy
SVG dashboard. We said simple, right?
will write a trace to the devtools console. Sorry, but there's no fancy
SVG dashboard. We said simple, right?
First, use clairvoyant to trace function calls and data flow. We've had
a couple of Clairvoyant PRs accepted, and they make it work well for us.
@ -248,8 +267,156 @@ From @mccraigmccraig we get the following (untested by me, but they look great):
> I finally had enough of all the boilerplate required to use clairvoyant with
> re-frame subs & handlers and wrote some code to tidy it up...
> https://www.refheap.com/799f89796b078b6e2459df038
```clj
(ns er-webui.re-frame
(:require
[clojure.string :as str]
[clojure.pprint :as pp]
[clairvoyant.core]
[cljs.analyzer :as analyzer]))
> gives you subs like this - https://www.refheap.com/e80f7f982f2bf75bd36bb1062
(def expand-macros
#{`reaction
`regsub
`reghandler})
> and handlers like this - https://www.refheap.com/e6a6a3a78eb768de386f54b49
(defn expand-op?
"should the op represented by the sym be expanded...
expands the sym to its fully namespaced version and
checks against expand-macros"
[sym env]
(when-let [{var-name :name} (analyzer/resolve-macro-var env sym)]
;; (pp/pprint ["expand-op?" sym var-name] *err*)
(expand-macros var-name)))
(defn maybe-expand
"recursively descend forms calling macroexpand-1
on any forms with a symbol from expand-macros in
first position"
[form env]
(if (and (seq? form)
(symbol? (first form)))
(let [[op & r] form
resolved-op (expand-op? op env)]
(if resolved-op
(maybe-expand
(macroexpand-1 (cons resolved-op r))
env)
(cons op
(doall (for [f r]
(maybe-expand
f
env))))))
form))
(defn maybe-expand-forms
[forms env]
(doall
(for [form forms]
(let [exp (maybe-expand form env)]
(when (not= exp form)
;; (pp/pprint exp *err*)
)
exp))))
(defn fn-name
"make a sensible fn name from
a possibly namespaced symbol or keyword"
([k] (fn-name k ""))
([k suffix]
(assert (or (keyword? k) (symbol? k)))
(-> k
(str suffix)
(str/replace #"^:" "")
(str/replace #"\." "-")
(str/replace "/" "--")
symbol)))
(defmacro reaction
"like reagent.core/reaction except it gives the fn a name
which makes for useful tracing"
[reaction-name & body]
(let [reaction-fn-name# (fn-name reaction-name)]
`(reagent.ratom/make-reaction
(~'fn ~reaction-fn-name#
[]
~@body))))
(defmacro regsub
"like re-frame.core/register-sub except it creates
the fn with a name for better tracing"
[sub-key params & body]
(assert (vector? params))
(let [sub-fn-name# (fn-name sub-key)]
`(re-frame.core/register-sub
~sub-key
(~'fn ~sub-fn-name#
~params
~@body))))
(defmacro reghandler
"like re-frame.core/register-handler except it
creates an fn with a name which makes for better tracing"
[handler-key middleware-or-params & body]
(let [handler-fn-name (fn-name handler-key "-h")
middleware (when (and (not (vector? middleware-or-params))
(vector? (first body)))
middleware-or-params)
params (if middleware
(first body)
middleware-or-params)
body (if middleware
(rest body)
body)]
(assert (vector? params))
`(re-frame.core/register-handler
~handler-key
~middleware
(~'fn ~handler-fn-name
~params
~@body))))
(defmacro trace-subs
[& body]
(let [body-forms# (maybe-expand-forms body &env)]
`(clairvoyant.core/trace-forms
{:tracer (re-frame-tracer.core/tracer :color "brown")}
~@body-forms#)))
(defmacro trace-handlers
[& body]
(let [body-forms# (maybe-expand-forms body &env)]
`(clairvoyant.core/trace-forms
{:tracer (re-frame-tracer.core/tracer :color "blue")}
~@body-forms#)))
(defmacro trace-views
[& body]
(let [body-forms# (maybe-expand-forms body &env)]
`(clairvoyant.core/trace-forms
{:tracer (re-frame-tracer.core/tracer :color "green")}
~@body-forms#)))
```
> gives you subs like this -
```clj
(regsub :initialised
[db _]
(reaction initialised-r
(get-in @db [:initialised])))
```
> and handlers like this -
```clj
(reghandler
:after-init
er-middleware
[db [_]]
(code-push/sync)
db)
```

View File

@ -3,34 +3,37 @@
This tutorial shows you how to implement pure event handlers that side-effect.
Yes, a surprising claim.
## Table Of Contents
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
* [Events Happen](#events-happen)
* [Handling The Happening](#handling-the-happening)
* [Your Handling](#your-handling)
* [90% Solution](#90--solution)
* [Bad, Why?](#bad--why-)
* [The 2nd Kind Of Problem](#the-2nd-kind-of-problem)
* [Effects And Coeffects](#effects-and-coeffects)
* [Why Does This Happen?](#why-does-this-happen-)
* [Doing vs Causing](#doing-vs-causing)
* [Et tu, React?](#et-tu--react-)
* [Pattern Structure](#pattern-structure)
* [Effects: The Two Step Plan](#effects--the-two-step-plan)
* [Step 1 Of Plan](#step-1-of-plan)
* [Another Example](#another-example)
* [The Coeffects](#the-coeffects)
* [Variations On A Theme](#variations-on-a-theme)
* [Summary](#summary)
- [Events Happen](#events-happen)
- [Handling The Happening](#handling-the-happening)
- [Your Handling](#your-handling)
- [90% Solution](#90%25-solution)
- [Bad, Why?](#bad-why)
- [The 2nd Kind Of Problem](#the-2nd-kind-of-problem)
- [Effects And Coeffects](#effects-and-coeffects)
- [Why Does This Happen?](#why-does-this-happen)
- [Doing vs Causing](#doing-vs-causing)
- [Et tu, React?](#et-tu-react)
- [Pattern Structure](#pattern-structure)
- [Effects: The Two Step Plan](#effects-the-two-step-plan)
- [Step 1 Of Plan](#step-1-of-plan)
- [Another Example](#another-example)
- [The Coeffects](#the-coeffects)
- [Variations On A Theme](#variations-on-a-theme)
- [Summary](#summary)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Effects
### Events Happen
Events "happen" when they are dispatched.
So, this makes an event happen:
```clj
(dispatch [:set-flag true])
(dispatch [:repair-ming-vase true])
```
Events are normally triggered by an external agent: the user clicks a button, or a server-pushed
@ -364,14 +367,14 @@ Just to be clear, the following two handlers achieve the same thing:
```clj
(reg-event-db
:set-flag
(fn [db [_ new-value]
(fn [db [_ new-value]]
(assoc db :flag new-value)))
```
vs
```clj
(reg-event-fx
:set-flag
(fn [cofx [_ new-value]
(fn [cofx [_ new-value]]
{:db (assoc (:db cofx) :flag new-value)}))
```
@ -392,7 +395,9 @@ In the next tutorial, we'll shine a light on `interceptors` which are
the mechanism by which event handlers are executed. That knowledge will give us a springboard
to then, as a next step, better understand coeffects and effects. We'll soon be writing our own.
---
***
Previous: [Infographic Overview](EventHandlingInfographic.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Interceptors](Interceptors.md)

View File

@ -10,24 +10,30 @@ make side effects a noop in event replays.
> &nbsp; &nbsp; -- @stuarthalloway
## Table Of Contexts
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
* [Where Effects Come From](#where-effects-come-from)
* [The Effects Map](#the-effects-map)
* [Infinite Effects](#infinite-effects)
* [Extensible Side Effects](#extensible-side-effects)
* [Writing An Effect Handler](#writing-an-effect-handler)
* [:db Not Always Needed](#-db-not-always-needed)
* [What Makes This Work?](#what-makes-this-work-)
* [Order Of Effects?](#order-of-effects-)
* [Effects With No Data](#effects-with-no-data)
* [Testing And Noops](#testing-and-noops)
* [Builtin Effect Handlers](#builtin-effect-handlers)
+ [:dispatch-later](#-dispatch-later)
+ [:dispatch](#-dispatch)
+ [:dispatch-n](#-dispatch-n)
+ [:deregister-event-handler](#-deregister-event-handler)
+ [:db](#-db)
- [Where Effects Come From](#where-effects-come-from)
- [The Effects Map](#the-effects-map)
- [Infinite Effects](#infinite-effects)
- [Extensible Side Effects](#extensible-side-effects)
- [Writing An Effect Handler](#writing-an-effect-handler)
- [:db Not Always Needed](#db-not-always-needed)
- [What Makes This Work?](#what-makes-this-work)
- [Order Of Effects?](#order-of-effects)
- [Effects With No Data](#effects-with-no-data)
- [Testing And Noops](#testing-and-noops)
- [Summary](#summary)
- [Builtin Effect Handlers](#builtin-effect-handlers)
- [:dispatch-later](#dispatch-later)
- [:dispatch](#dispatch)
- [:dispatch-n](#dispatch-n)
- [:deregister-event-handler](#deregister-event-handler)
- [:db](#db)
- [External Effects](#external-effects)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Where Effects Come From
@ -283,13 +289,6 @@ registered handlers) to which you can return.
XXX
---
Previous: [Interceptors](Interceptors.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](Readme.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Coeffects](Coeffects.md)
---
### Builtin Effect Handlers
#### :dispatch-later
@ -307,7 +306,7 @@ Which means: in 200ms do this: `(dispatch [:event-id "param"])` and in 100ms ...
#### :dispatch
`dispatch` one event. Excepts a single vector.
`dispatch` one event. Expects a single vector.
usage:
```clj
@ -351,12 +350,16 @@ usage:
- https://github.com/Day8/re-frame-http-fx (GETs and POSTs)
- https://github.com/Day8/re-frame-forward-events-fx (slightly exotic)
- https://github.com/Day8/re-frame-async-flow-fx (more complicated)
- https://github.com/micmarsh/re-frame-youtube-fx (YouTube iframe API wrapper)
- https://github.com/madvas/re-frame-web3-fx (Ethereum Web3 API)
- https://github.com/madvas/re-frame-google-analytics-fx (Google Analytics API)
Create a PR to include yours in this list.
XXX maybe put this list into the Wiki, so editable by all.
---
***
Previous: [Interceptors](Interceptors.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Coeffects](Coeffects.md)

View File

@ -0,0 +1,16 @@
## Event Handling Infographics
Three diagrams are provided:
- a beginners romp
- a useful, intermediate schematic depiction
- an advanced, accurate and detailed view
They should be reviewed in conjunction with a reading of the written tutorials.
<img src="/images/event-handlers.png?raw=true">
***
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effectful Handlers](EffectfulHandlers.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -2,6 +2,16 @@
Templates, examples, and other resources related to re-frame. For additions or modifications, please create an issue with a link and description or submit a pull request.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Templates](#templates)
- [Examples and Applications Using re-frame](#examples-and-applications-using-re-frame)
- [Videos](#videos)
- [Server Side Rendering](#server-side-rendering)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Templates
@ -18,6 +28,8 @@ Templates, examples, and other resources related to re-frame. For additions or m
### Examples and Applications Using re-frame
* [How to create decentralised apps with re-frame and Ethereum](https://medium.com/@matus.lestan/how-to-create-decentralised-apps-with-clojurescript-re-frame-and-ethereum-81de24d72ff5#.b9xh9xnis) by [Matus Lestan] - Tutorial with links to code and live example. Based on re-frame `0.8.0`
* [Elfeed-cljsrn](https://github.com/areina/elfeed-cljsrn) by [Toni Reina] - A mobile client for [Elfeed](https://github.com/skeeto/elfeed) rss reader, built with React Native. Based on re-frame `0.8.0`
* [Memory Hole](https://github.com/yogthos/memory-hole) by [Yogthos] - A small issue tracking app written with Luminus and re-frame. Based on re-frame `0.8.0`
@ -35,6 +47,7 @@ Templates, examples, and other resources related to re-frame. For additions or m
* [Angular Phonecat tutorial in re-frame](http://dhruvp.github.io/2015/03/07/re-frame/) by [Dhruv Parthasarathy] - A detailed step-by-step tutorial that ports the Angular Phonecat tutorial to re-frame. Based on re-frame `0.2.0`
* [Stately: State Machines](https://github.com/nodename/stately) also https://www.youtube.com/watch?v=klqorRUPluw
### Videos

View File

@ -0,0 +1,21 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Answer](#answer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
How can I detect exceptions in Event Handlers?
### Answer
A suggested solution can be found in [this issue](https://github.com/Day8/re-frame/issues/231#issuecomment-249991378).
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,3 +1,14 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Short Answer](#short-answer)
- [Better Answer](#better-answer)
- [Other Inspection Tools](#other-inspection-tools)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
@ -61,5 +72,6 @@ There's also [Data Frisk](https://github.com/Odinodin/data-frisk-reagent) which
provides a very nice solution for navigating and inspecting any data structure.
---
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,3 +1,12 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Answer](#answer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
I use logging method X, how can I make re-frame use my method?
@ -23,5 +32,6 @@ override that by providing your own set or subset of these functions using
...})
```
---
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,3 +1,12 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Answer](#answer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
If I `dispatch` a js event object (from a view), it is nullified
@ -17,5 +26,6 @@ So there's two things to say about this:
and debugging will become easier.
---
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,14 +1,23 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Frequently Asked Questions](#frequently-asked-questions)
- [Want To Add An FAQ?](#want-to-add-an-faq)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Frequently Asked Questions
1. [How can I Inspect app-db?](Inspecting-app-db.md)
2. [How do I use logging method X](Logging.md)
3. [Dispatched Events Are Null](Null-Dispatched-Events.md)
4.
6. [Why implement re-frame in `.cljc` files](Why-CLJC.md)
4. [Why implement re-frame in `.cljc` files](Why-CLJC.md)
5. [Why do we need to clear the subscription cache when reloading with Figwheel?](Why-Clear-Sub-Cache.md)
6. [How can I detect exceptions in Event Handlers?](CatchingEventExceptions.md)
### Want To Add An FAQ?
## Want To Add An FAQ?
We'd like that. Please supply a PR. Or just open an issue. Many Thanks!!

View File

@ -1,3 +1,12 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Answer](#answer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
Why is re-frame implemented in `.cljc` files? Aren't ClojureScript
@ -16,5 +25,6 @@ Necessary interop for each platform can be found in
See also: https://github.com/Day8/re-frame-test
---
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -0,0 +1,76 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Question](#question)
- [Answer](#answer)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### Question
Why do we call `clear-subscription-cache!` when reloading code with Figwheel?
### Answer
Pour yourself a drink, as this is a circuitous tale involving one of the hardest
problems in Computer Science.
**1: Humble beginnings**
When React is rendering, if an exception is thrown, it doesn't catch or
handle the errors gracefully. Instead, all of the React components up to
the root are destroyed. When these components are destroyed, none of their
standard lifecycle methods are called, like `ComponentDidUnmount`.
**2: Simple assumptions**
Reagent tracks the watchers of a Reaction to know when no-one is watching and
it can call the Reaction's `on-dispose`. Part of the book-keeping involved in
this requires running the `on-dispose` in a React `ComponentWillUnmount` lifecycle
method.
At this point, your spidey senses are probably tingling.
**3: The hardest problem in CS**
re-frame subscriptions are created as Reactions. re-frame helpfully deduplicates
subscriptions if multiple parts of the view request the same subscription. This
is a big efficiency boost. When re-frame creates the subscription Reaction, it
sets the `on-dispose` method of that subscription to remove itself from the
subscription cache. This means that when that subscription isn't being watched
by any part of the view, it can be disposed.
**4: The gnarly implications**
If you are
1. Writing a re-frame app
2. Write a bug in your subscription code (your one bug for the year)
3. Which causes an exception to be thrown in your rendering code
then:
1. React will destroy all of the components in your view without calling `ComponentWillUnmount`.
2. Reagent will not get notified that some subscriptions are not needed anymore.
3. The subscription on-dispose functions that should have been run, are not.
4. re-frame's subscription cache will not be invalidated correctly, and the subscription with the bug
is still in the cache.
At this point you are looking at a blank screen. After debugging, you find the problem and fix it.
You save your code and Figwheel recompiles and reloads the changed code. Figwheel attempts to re-render
from the root. This causes all of the Reagent views to be rendered and to request re-frame subscriptions
if they need them. Because the old buggy subscription is still sitting around in the cache, re-frame
will return that subscription instead of creating a new one based on the fixed code. The only way around
this (once you realise what is going on) is to reload the page.
**5: Coda**
re-frame 0.9.0 provides a new function: `re-frame.core/clear-subscription-cache!` which will run the
on-dispose function for every subscription in the cache, emptying the cache, and causing new subscriptions
to be created after reloading.
***
Up: [FAQ Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,26 +1,33 @@
## Introduction
This is an interceptors tutorial.
## re-frame Interceptors
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Introduction](#introduction)
- [Interceptors](#interceptors)
* [Why Interceptors?](#why-interceptors-)
* [What Do Interceptors Do?](#what-do-interceptors-do-)
* [Wait, I know That Pattern!](#wait--i-know-that-pattern-)
* [What's In The Pipeline?](#what-s-in-the-pipeline-)
* [Show Me](#show-me)
* [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
- [Why Interceptors?](#why-interceptors)
- [What Do Interceptors Do?](#what-do-interceptors-do)
- [Wait, I know That Pattern!](#wait-i-know-that-pattern)
- [What's In The Pipeline?](#whats-in-the-pipeline)
- [Show Me](#show-me)
- [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
- [Executing A Chain](#executing-a-chain)
* [The Links Of The Chain](#the-links-of-the-chain)
* [What Is Context?](#what-is-context-)
* [Self Modifying](#self-modifying)
* [Credit](#credit)
* [Write An Interceptor](#write-an-interceptor)
* [Wrapping Handlers](#wrapping-handlers)
- [The Links Of The Chain](#the-links-of-the-chain)
- [What Is Context?](#what-is-context)
- [Self Modifying](#self-modifying)
- [Credit](#credit)
- [Write An Interceptor](#write-an-interceptor)
- [Wrapping Handlers](#wrapping-handlers)
- [Summary](#summary)
- [Appendix](#appendix)
* [The Builtin Interceptors](#the-builtin-interceptors)
- [The Builtin Interceptors](#the-builtin-interceptors)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Introduction
This is a tutorial on re-frame Interceptors.
## Interceptors
### Why Interceptors?
@ -89,8 +96,7 @@ concept, right there.
### Show Me
At the time when you register an event handler, you can provide an
chain of interceptors too.
At the time when you register an event handler, you can provide a chain of interceptors too.
Using a 3-arity registration function:
```clj
@ -105,24 +111,24 @@ Using a 3-arity registration function:
### Handlers Are Interceptors Too
You might see that registration above as associating `:some-id` with two things: (1) a chain of interceptors
You might see that registration above as associating `:some-id` with two things: (1) a chain of 2 interceptors `[in1 in2]`
and (2) a handler.
Except, the handler is turned into an interceptor too. (We'll see how shortly)
Except, the handler is turned into an interceptor too (we'll see how shortly).
So `:some-id` is only associated with one thing: a 3-chain of interceptors,
with the handler wrapped in an interceptor and put on the end of the other two.
with the handler wrapped in an interceptor, called say `h`, and put on the end of the other two: `[in1 in2 h]`.
Except, the registration function itself, `reg-event-db`, actually takes this 3-chain
and inserts its own interceptors
(which do useful things) at the front (more on this soon too),
so ACTUALLY, there's about 5 interceptors in the chain.
and inserts its own standard interceptors, called say `std1` and `std2`
(which do useful things, more soon) at the front,
so ACTUALLY, there's about 5 interceptors in the chain: `[std1 std2 in1 in2 h]`
So, ultimately, that event registration associates the event id `:some-id`
with __just__ a chain of interceptors. Nothing more.
Later, when a `(dispatch [:some-id ...])` happens, that 5-chain of
interceptors will be "executed". And that's how events get handled.
interceptors will be "executed". And that's how an event gets handled.
## Executing A Chain
@ -136,8 +142,13 @@ Each interceptor has this form:
:after (fn [context] ...)} ;; `identity` would be a noop
```
That's essentially a map of two functions. Now imagine a vector of these maps - that's an
an interceptor chain.
That's essentially a map of two functions. Now imagine a vector of these maps - that's an interceptor chain.
Above we imagined an interceptor chain of `[std1 std2 in1 in2 h]`. Now we know that this is really
a vector of 5 maps: `[{...} {...} {...} {...} {...}]` where each of the 5 maps have
a `:before` and `:after` fn.
Sometimes, the `:before` and `:after` fns are noops (think `identity`).
To "execute" an interceptor chain:
1. create a `context` (a map, described below)
@ -207,6 +218,9 @@ and removed from the `:queue` by existing Interceptors.
> All truths are easy to understand once they are discovered <br>
> -- Galileo Galilei
<br>
> Things always become obvious after the fact <br>
> -- Nassim Nicholas Taleb
This elegant and flexible arrangement was originally
designed by the talented
@ -216,7 +230,7 @@ designed by the talented
Dunno about you, but I'm easily offended by underscores.
If we had a component which did this:
If we had a view which did this:
```clj
(dispatch [:delete-item 42])
```
@ -226,16 +240,16 @@ We'd have to write this handler:
(reg-event-db
:delete-item
(fn
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
(dissoc db key-to-delete)))
```
Do you see it there? In the event destructuring!!! Almost mocking us with that
passive aggressive, understated thing it has going on!! Co-workers
have said I'm "being overly sensitive", perhaps even horizontalist, but
you can see it, right? Of course you can.
have said I'm "being overly sensitive", perhaps even pixel-ist, but
you can see it, right? Of course you can.
What a relief it would be to get rid of it, but how? We'll write an interceptor: `trim-event`
What a relief it would be to not have it there, but how? We'll write an interceptor: `trim-event`
Once we have written `trim-event`, our registration will change to look like this:
```clj
@ -243,11 +257,11 @@ Once we have written `trim-event`, our registration will change to look like thi
:delete-item
[trim-event] ;; <--- interceptor added
(fn
[db [key-to-delete]] ;; <--- no leading underscore necessary
[db [key-to-delete]] ;; <---yaaah! no leading underscore
(dissoc db key-to-delete)))
```
`trim-event` will need to change the `:coeffects` map (within `context`). More specifically, it will be
`trim-event` will need to change the `:coeffects` map (within `context`). Specifically, it will be
changing the `:event` value within the `:coeffects`.
`:event` will start off as `[:delete-item 42]`, but will end up `[42]`. `trim-event` will remove that
@ -279,16 +293,20 @@ Notes:
We're going well. Let's do an advanced wrapping.
Earlier, in the "Handlers Are Interceptors Too" section, I explained that `event handlers`
are wrapped in an Interceptor and placed on the end of an Interceptor chain.
How does this wrapping happen?
are wrapped in an Interceptor and placed on the end of an Interceptor chain. Remember the
whole `[std1 std2 in1 in2 h]` thing?
We're now look at the `h` bit. How does an event handler get wrapped to be an Interceptor.
Reminder - there's two kinds of handler:
- the `-db` variety registered by `reg-event-db`
- the `-fx` variety registered by `reg-event-fx`
I'll now show how to wrap the `-db` variety. Here's what a `-db` handler looks like:
I'll now show how to wrap the `-db` variety.
Reminder: here's what a `-db` handler looks like:
```clj
(fn [db event] ;; takes two params
(fn [db event] ;; takes two params
(assoc db :flag true)) ;; returns a new db
```
@ -306,11 +324,11 @@ Interceptor which wraps that handler:
```
Notes:
1. Notice how this wrapper extracts data from the `context's` `:coeffect`
1. Notice how this wrapper extracts data from the `context's` `:coeffects`
and then calls the handler with that data (a handler must be called with `db` and `event`)
2. Equally notice how this wrapping takes the return value from the `-db`
handler and puts it into `context's` `:effect`
3. The modified `context` (it has a new `:effect`) is returned
handler and puts it into `context's` `:effects`
3. The modified `context` (it has a new `:effects`) is returned
3. This is all done in `:before`. There is no `:after` (it is a noop). But this
could have been reversed with the work happening in `:after` and `:before` a noop. Shrug.
Remember that this Interceptor will be on the end of a chain.
@ -331,7 +349,7 @@ __1.__ When you register an event handler, you can supply a collection of interc
```
__2.__ When you are registering an event handler, you are associating an event id with a chain of interceptors including:
- the ones your supply (optional)
- the ones you supply (optional)
- an extra one on the end, which wraps the handler itself
- a couple at the beginning of the chain, put there by the `reg-event-db` or `reg-event-fx`.
@ -347,12 +365,6 @@ __4.__ Interceptors do interesting things:
In the next Tutorial, we'll look at (side) Effects in more depth. Later again, we'll look at Coeffects.
---
Previous: [Effectful Handlers](EffectfulHandlers.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effects](Effects.md)
## Appendix
### The Builtin Interceptors
@ -377,7 +389,8 @@ To use them, first require them:
[re-frame.core :refer [debug path]])
```
---
***
Previous: [Effectful Handlers](EffectfulHandlers.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effects](Effects.md)

View File

@ -1,3 +1,19 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Bootstrapping Application State](#bootstrapping-application-state)
- [1. Register Handlers](#1-register-handlers)
- [2. Kick Start Reagent](#2-kick-start-reagent)
- [3. Loading Initial Data](#3-loading-initial-data)
- [Getting Data Into `app-db`](#getting-data-into-app-db)
- [The Pattern](#the-pattern)
- [Scales Up](#scales-up)
- [Cheating - Synchronous Dispatch](#cheating---synchronous-dispatch)
- [Loading Initial Data From Services](#loading-initial-data-from-services)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Bootstrapping Application State
To bootstrap a re-frame application, you need to:
@ -17,8 +33,8 @@ Point 3 is the interesting bit and will be the main focus of this page, but let'
## 1. Register Handlers
re-frame's multifarious handlers all work in the same way. You declare
and registered your handlers in the one step, like this "event handler" example:
re-frame's various handlers all work in the same way. You declare
and register your handlers in the one step, like this "event handler" example:
```clj
(re-frame/reg-event-db ;; event handler will be registered automatically
:some-id
@ -65,8 +81,7 @@ And now we use that subscription:
(defn main-panel
[]
(let [name (re-frame/subscribe [:name])] ;; <--- a subscription <---
(fn []
[:div "Hello " @name])))) ;; <--- use the result of the subscription
[:div "Hello " @name]))) ;; <--- use the result of the subscription
```
The user of our app will see funny things
@ -142,16 +157,14 @@ quick sketch of the entire pattern. It is very straight-forward.
(defn main-panel ;; the top level of our app
[]
(let [name (re-frame/subscribe :name)] ;; we need there to be good data
(fn []
[:div "Hello " @name]))))
[:div "Hello " @name])))
(defn top-panel ;; this is new
[]
(let [ready? (re-frame/subscribe [:initialised?])]
(fn []
(if-not @ready? ;; do we have good data?
[:div "Initialising ..."] ;; tell them we are working on it
[main-panel])))) ;; all good, render this component
(if-not @ready? ;; do we have good data?
[:div "Initialising ..."] ;; tell them we are working on it
[main-panel]))) ;; all good, render this component
(defn ^:export main ;; call this to bootstrap your app
[]
@ -193,11 +206,10 @@ This assumes boolean flags are set in `app-db` when data was loaded from these s
## Cheating - Synchronous Dispatch
In simple cases, you can simplify matters by using `(dispatch-sync [:initialise-db])` in
the main entry point function. The
[Simple Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/simple/src/simpleexample/core.cljs#L110)
and [TodoMVC Example](https://github.com/Day8/re-frame/blob/8cf42f57f50f3ee41e74de1754fdb75f80b31775/examples/todomvc/src/todomvc/core.cljs#L35)
both use `dispatch-sync` to initialise the app-db.
In simple cases, you can simplify matters by using `dispatch-sync` (instead of `dispatch`) in
the main function.
This technique can be seen in the [TodoMVC Example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/core.cljs#L35).
`dispatch` queues an event for later processing, but `dispatch-sync` acts
like a function call and handles an event immediately. That's useful for initial data
@ -214,11 +226,12 @@ tool in this context and, sometimes, when writing tests, but
Above, in our example `main`, we imagined using `(re-frame/dispatch [:load-from-service-1])` to request data
from a backend services. How would we write the handler for this event?
The next Tutorial will show you how.
The next Tutorial will show you how.
---
***
Previous: [Namespaced Keywords](Namespaced-Keywords.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Talking To Servers](Talking-To-Servers.md)

283
docs/MentalModelOmnibus.md Normal file
View File

@ -0,0 +1,283 @@
## Mental Model Omnibus
> If a factory is torn down but the rationality which produced it is
left standing, then that rationality will simply produce another
factory. If a revolution destroys a government, but the systematic
patterns of thought that produced that government are left intact,
then those patterns will repeat themselves. <br>
> -- Robert Pirsig, Zen and the Art of Motorcycle Maintenance
<img height="350px" align="right" src="/images/mental-model-omnibus.jpg?raw=true">
The re-frame tutorials initially focus on the **domino
narrative**. The goal is to efficiently explain the mechanics of re-frame,
and get you reading and writing code ASAP.
But **there are other perspectives** on re-frame
which will deepen your understanding.
This tutorial is a tour of these ideas, justifications and insights.
It is a little rambling, but I'm hoping it will deliver for you
at least one "Aaaah, I see" moment before the end.
> All models are wrong, but some are useful
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [What is the problem?](#what-is-the-problem)
- [Guiding Philosophy](#guiding-philosophy)
- [It does Event Sourcing](#it-does-event-sourcing)
- [It does a reduce](#it-does-a-reduce)
- [Derived Data All The Way Down](#derived-data-all-the-way-down)
- [It does FSM](#it-does-fsm)
- [Full Stack](#full-stack)
- [What Of This Romance?](#what-of-this-romance)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## What is the problem?
First, we decided to build our SPA apps with ClojureScript, then we
choose [Reagent], then we had a problem. It was mid 2014.
For all its considerable brilliance, Reagent (+ React)
delivers only the 'V' part of a traditional MVC framework.
But apps involve much more than V. We build quite complicated
SPAs which can run to 50K lines of code. So, I wanted to know:
where does the control logic go? How is state stored & manipulated? etc.
We read up on [Pedestal App], [Flux],
[Hoplon], [Om], early [Elm], etc and re-frame is the architecture that
emerged. Since then, we've tried to keep an eye on further developments like the
Elm Architecture, Om.Next, BEST, Cycle.js, Redux, etc. They have taught us much
although we have often made different choices.
re-frame does have parts which correspond to M, V, and C, but they aren't objects.
It is sufficiently different in nature
from (traditional, Smalltalk) MVC that calling it MVC would be confusing. I'd
love an alternative.
Perhaps it is a RAVES framework - Reactive-Atom Views Event
Subscription framework (I love the smell of acronym in the morning).
Or, if we distill to pure essence, `DDATWD` - Derived Data All The Way Down.
*TODO:* get acronym down to 3 chars! Get an image of stacked Turtles for `DDATWD`
insider's joke, conference T-Shirt.
## Guiding Philosophy
__First__, above all we believe in the one true [Dan Holmsand], creator of Reagent, and
his divine instrument the `ratom`. We genuflect towards Sweden once a day.
__Second__, we believe in ClojureScript, immutable data and the process of building
a system out of pure functions.
__Third__, we believe in the primacy of data, for the reasons described in
the main README. re-frame has a data oriented, functional architecture.
__Fourth__, we believe that Reactive Programming is one honking good idea.
How did we ever live without it? It is a quite beautiful solution to one half of re-frame's
data conveyance needs, **but** we're cautious about taking it too far - as far as, say, cycle.js.
It doesn't take over everything in re-frame - it just does part of the job.
__Finally__, many years ago I programmed briefly in Eiffel where I learned
about [command-query separation](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation).
Each generation of
programmers seems destined to rediscover this principle - CQRS is the recent re-rendering.
And yet we still see read/write `cursors` and two way data binding being promoted as a good thing.
Please, just say no. As your programs get bigger, the use of these two-way constructs
will encourage control logic into all the
wrong places and you'll end up with a tire fire of an Architecture.
## It does Event Sourcing
How did that error happen, you puzzle, shaking your head ruefully?
What did the user do immediately prior? What
state was the app in that this event was so problematic?
To debug, you need to know this information:
1. the state of the app immediately before the exception
2. What final `event` then caused your app to error
Well, with re-frame you need to record (have available):
1. A recent checkpoint of the application state in `app-db` (perhaps the initial state)
2. all the events `dispatch`ed since the last checkpoint, up to the point where the error occurred
Note: that's all just data. **Pure, lovely loggable data.**
If you have that data, then you can reproduce the error.
re-frame allows you to time travel, even in a production setting.
Install the "checkpoint" state into `app-db`
and then "play forward" through the collection dispatched events.
The only way the app "moves forwards" is via events. "Replaying events" moves you
step by step towards the error causing problem.
This is perfect for debugging assuming, of course, you are in a position to capture
a checkpoint of `app-db`, and the events since then.
Here's Martin Fowler's [description of Event Sourcing](http://martinfowler.com/eaaDev/EventSourcing.html).
## It does a reduce
Here's an interesting way of thinking about the re-frame
data flow ...
**First**, imagine that all the events ever dispatched in a
certain running app were stored in a collection (yes, event sourcing again).
So, if when the app started, the user clicked on button X
the first item in this collection would be the event
generated by that button, and then, if next the user moved
a slider, the associated event would be the next item in
the collection, and so on and so on. We'd end up with a
collection of event vectors.
**Second**, remind yourself that the `combining function`
of a `reduce` takes two arguments:
1. the current state of the reduction and
2. the next collection member to fold in
Then notice that `reg-event-db` event handlers take two arguments also:
1. `db` - the current state of `app-db`
2. `v` - the next event to fold in
Interesting. That's the same as a `combining function` in a `reduce`!!
So now we can introduce the new mental model: at any point in time,
the value in `app-db` is the result of performing a `reduce` over
the entire `collection` of events dispatched in the app up until
that time. The combining function for this reduce is the set of event handlers.
It is almost like `app-db` is the temporary place where this
imagined `perpetual reduce` stores its on-going reduction.
Now, in the general case, this perspective breaks down a bit,
because of `reg-event-fx` (has `-fx` on the end, not `-db`) which
allows:
1. event handlers can produce `effects` beyond just application state
changes.
2. Event handlers sometimes need coeffects (arguments) in addition to `db` and `v`.
But, even if it isn't the full picture, it is a very useful
and interesting mental model. We were first exposed to this idea
via Elm's early use of `foldp` (fold from the past), which was later enshrined in the
Elm Architecture.
## Derived Data All The Way Down
For the love of all that is good, please watch this terrific
[StrangeLoop presentation](https://www.youtube.com/watch?v=fU9hR3kiOK0) (40 mins).
See what happens when you re-imagine a database as a stream!! Look at
all the problems that evaporate.
Think about that: shared mutable state (the root of all evil),
re-imagined as a stream!! Blew my socks off.
If, by chance, you ever watched that video (you should!), you might then twig to
the idea that `app-db` is really a derived value ... the video talks
a lot about derived values. So, yes, app-db is a derived value of the `perpetual reduce`.
And yet, it acts as the authoritative source of state in the app. And yet,
it isn't, it is simply a piece of derived state. And yet, it is the source. Etc.
This is an infinite loop of sorts - an infinite loop of derived data.
## It does FSM
> Any sufficiently complicated GUI contains an ad hoc,
> informally-specified, bug-ridden, slow implementation
> of a hierarchical Finite State Machine <br>
> -- me, trying too hard to impress my two twitter followers
`event handlers` collectively
implement the "control" part of an application. Their logic
interprets arriving events in the context of existing state,
and they compute what the new state of the application.
`events` act, then, a bit like the `triggers` in a finite state machine, and
the event handlers act like the rules which govern how the state machine
moves from one logical state to the next.
In the simplest
case, `app-db` will contain a single value which represents the current "logical state".
For example, there might be a single `:phase` key which can have values like `:loading`,
`:not-authenticated` `:waiting`, etc. Or, the "logical state" could be a function
of many values in `app-db`.
Not every app has lots of logical states, but some do, and if you are implementing
one of them, then formally recognising it and using a technique like
[State Charts](https://www.amazon.com/Constructing-User-Interface-Statecharts-Horrocks/dp/0201342782)
will help greatly in getting a clean design and fewer bugs.
The beauty of re-frame from a FSM point of view is that all the state is
in one place - unlike OO systems where the state is distributed (and synchronized)
across many objects. So implementing your control logic as a FSM is
fairly natural in re-frame, whereas it is often difficult and
contrived in other kinds of architecture (in my experience).
So, members of the jury, I put it to you that:
- the first 3 dominoes implement an [Event-driven finite-state machine](https://en.wikipedia.org/wiki/Event-driven_finite-state_machine)
- the last 3 dominoes render of the FSM's current state for the user to observe
Depending on your app, this may or may not be a useful mental model,
but one thing is for sure ...
Events - that's the way we roll.
## Full Stack
If you like re-frame and want to take the principles full-stack, then
these resource might be interesting to you:
Commander Pattern
https://www.youtube.com/watch?v=B1-gS0oEtYc
Datalog All The Way Down
https://www.youtube.com/watch?v=aI0zVzzoK_E
## What Of This Romance?
My job is to be a relentless cheerleader for re-frame, right?
The gyrations of my Pom-Poms should be tectonic,
but the following quote makes me smile. It should
be taught in all ComSci courses.
> We begin in admiration and end by organizing our disappointment <br>
> &nbsp;&nbsp;&nbsp; -- Gaston Bachelard (French philosopher)
Of course, that only applies if you get passionate about a technology
(a flaw of mine).
But, no. No! Those French Philosophers and their pessimism - ignore him!!
Your love for re-frame will be deep, abiding and enriching.
***
Previous: [First Code Walk-Through](CodeWalkthrough.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Effectful Handlers](EffectfulHandlers.md)
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
[Reagent]:http://reagent-project.github.io/
[Dan Holmsand]:https://twitter.com/holmsand
[Flux]:http://facebook.github.io/flux/docs/overview.html#content
[Hiccup]:https://github.com/weavejester/hiccup
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
[Elm]:http://elm-lang.org/
[OM]:https://github.com/swannodette/om
[Prismatic Schema]:https://github.com/Prismatic/schema
[Hoplon]:http://hoplon.io/
[Pedestal App]:https://github.com/pedestal/pedestal-app

View File

@ -1,3 +1,11 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Namespaced Ids](#namespaced-ids)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Namespaced Ids
As an app gets bigger, you'll tend to get clashes on ids - event-ids, or query-ids (subscriptions), etc.
@ -20,7 +28,8 @@ fiction. I can have the keyword `:panel1/edit` even though
Naturally, you'll take advantage of this by using keyword namespaces
which are both unique and descriptive.
---
***
Previous: [Navigation](Navigation.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Loading Initial Data](Loading-Initial-Data.md)
Next: [Loading Initial Data](Loading-Initial-Data.md)

View File

@ -1,3 +1,11 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [What About Navigation?](#what-about-navigation)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## What About Navigation?
@ -49,7 +57,8 @@ A high level reagent view has a subscription to :active-panel and will switch to
Continue to [Namespaced Keywords](Namespaced-Keywords.md) to reduce clashes on ids.
---
***
Previous: [Basic App Structure](Basic-App-Structure.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Namespaced Keywords](Namespaced-Keywords.md)

View File

@ -1,12 +1,26 @@
## Eek! Performance Problems
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [1. Is It The `debug` Interceptor?](#1-is-it-the-debug-interceptor)
- [2. `=` On Big Structures](#2--on-big-structures)
- [An Example Of Problem 2](#an-example-of-problem-2)
- [Solutions To Problem 2](#solutions-to-problem-2)
- [3. Are you Using a React `key`?](#3-are-you-using-a-react-key)
- [4. Callback Functions](#4-callback-functions)
- [A Weapon](#a-weapon)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 1. Is It The `debug` Interceptor?
This first one is something of a non-problem.
Are you are using the `re-frame.core/debug` Interceptor?
You should be, it's useful. __But__ you do need to be aware of its possible performance implications.
`debug` reports what's changed after an event handler has run by using
`clojure.data/diff` to do deep, CPU intensive diff on `app-db`.
That diff could be taking a while, and leading to apparent performance problems.
@ -119,7 +133,7 @@ Look at this `div`:
```
Every time it is rendered, that `:on-mouse-over` function will be regenerated,
and it will NOT test equal to the last time it rendered. It will appear to be a new function.
and it will NOT test `=` to the last time it rendered. It will appear to be a new function.
It will appear to React that it has to replace the event handler.
Most of the time, this is not an issue. But if you are generating a LOT of DOM

View File

@ -1,28 +1,37 @@
### Understanding Event Handlers
- [-db Event Handlers] TODO
- [Effectful Handlers](EffectfulHandlers.md)
- [Interceptors](Interceptors.md)
- [Effects](Effects.md)
- [Coeffects](Coeffects.md)
### Introduction
- [This Repo's README](../README.md)
- [app-db (Application State)](ApplicationState.md)
- [First Code Walk-Through](CodeWalkthrough.md)
- [Mental Model Omnibus](MentalModelOmnibus.md)
### Structuring Your Application
### Event Handlers
- [Infographic Overview](EventHandlingInfographic.md)
- [Effectful Handlers](EffectfulHandlers.md)
- [Interceptors](Interceptors.md)
- [Effects](Effects.md)
- [Coeffects](Coeffects.md)
### App Structure
- [Basic App Structure](Basic-App-Structure.md)
- [Navigation](Navigation.md)
- [Namespaced Keywords](Namespaced-Keywords.md)
### Populating Your Application Data
### App Data
- [Loading Initial Data](Loading-Initial-Data.md)
- [Talking To Servers](Talking-To-Servers.md)
- [Subscribing to External Data](Subscribing-To-External-Data.md)
### Debugging And Testing
### Debugging And Testing
- [Debugging-Event-Handlers](Debugging-Event-Handlers.md)
- [Debugging Event Handlers](Debugging-Event-Handlers.md)
- [Debugging](Debugging.md)
@ -33,3 +42,8 @@
- [Solve the CPU hog problem](Solve-the-CPU-hog-problem.md)
- [Using Stateful JS Components](Using-Stateful-JS-Components.md)
- [The re-frame Logo](The-re-frame-logo.md)
<!-- We put these at the end so that there is nothing for doctoc to generate. -->
<!-- START doctoc -->
<!-- END doctoc -->

View File

@ -1,3 +1,18 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Solving The CPU Hog Problem](#solving-the-cpu-hog-problem)
- [The re-frame Solution](#the-re-frame-solution)
- [A Sketch](#a-sketch)
- [Why Does A Redispatch Work?](#why-does-a-redispatch-work)
- [Variations](#variations)
- [Cancel Button](#cancel-button)
- [Further Notes](#further-notes)
- [Forcing A One Off Render](#forcing-a-one-off-render)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Solving The CPU Hog Problem
Sometimes a handler has a lot of CPU intensive work to do, and
@ -57,7 +72,7 @@ Here's an `-fx` handler which counts up to some number in chunks:
;; We are at the beginning, so:
;; - modify db, causing popup of Modal saying "Working ..."
;; - begin iterative dispatch. Give initial version of "so-far"
{:disptch 1[:count-to false {:counter 0} finish-at] ;; dispatch to self
{:dispatch [:count-to false {:counter 0} finish-at] ;; dispatch to self
:db (assoc db :we-are-working true)}
(if (> (:counter so-far) finish-at)
;; We are finished:

View File

@ -1,3 +1,23 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Subscribing to External Data](#subscribing-to-external-data)
- [There Can Be Only One!!](#there-can-be-only-one)
- [Components Don't Know, Don't Care](#components-dont-know-dont-care)
- [A 2nd Source](#a-2nd-source)
- [Via A Subscription](#via-a-subscription)
- [The Subscription Handler's Job](#the-subscription-handlers-job)
- [Some Code](#some-code)
- [Any Good?](#any-good)
- [Warning: Undo/Redo](#warning-undoredo)
- [Query De-duplication](#query-de-duplication)
- [Thanks To](#thanks-to)
- [The Alternative Approach](#the-alternative--approach)
- [What Not To Do](#what-not-to-do)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Subscribing to External Data
In [Talking To Servers](Talking-To-Servers.md) we learned how to
@ -236,6 +256,7 @@ data into HTML and nothing more. they absolutely do not do imperative stuff.
Use one of the two alternatives described above.
---
***
Previous: [Talking to Servers](Talking-To-Servers.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -1,3 +1,17 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Talking To Servers](#talking-to-servers)
- [Triggering The Request](#triggering-the-request)
- [The Event Handler](#the-event-handler)
- [Version 1](#version-1)
- [Successful GET](#successful-get)
- [Problems In Paradise?](#problems-in-paradise)
- [Version 2](#version-2)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Talking To Servers
This page describes how a re-frame app might "talk" to a backend HTTP server.
@ -117,6 +131,7 @@ Here's our rewrite:
```clj
(ns my.app.events
(:require
[ajax.core :as ajax]
[day8.re-frame.http-fx]
[re-frame.core :refer [reg-event-fx]))
@ -128,6 +143,7 @@ Here's our rewrite:
;; we return a map of (side) effects
{:http-xhrio {:method :get
:uri "http://json.my-endpoint.com/blah"
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:process-response]
:on-failure [:bad-response]}
:db (assoc db :loading? true)}))
@ -139,7 +155,8 @@ Notes:
---
***
Previous: [Loading Initial Data](Loading-Initial-Data.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Up: [Index](README.md)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
Next: [Subscribing to External Data](Subscribing-To-External-Data.md)

222
docs/Testing.md Normal file
View File

@ -0,0 +1,222 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Testing](#testing)
- [Event Handlers - Part 1](#event-handlers---part-1)
- [Event Handlers - Part 2](#event-handlers---part-2)
- [Subscription Handlers](#subscription-handlers)
- [Components- Part 1](#components--part-1)
- [Components - Part 2A](#components---part-2a)
- [Components - Part 2B](#components---part-2b)
- [Components - Part 2C](#components---part-2c)
- [Summary](#summary)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
> IGNORE THIS DOCUMENT. IT IS WIP
## Testing
With a re-frame app, there's principally three things to test:
1. Event handlers
2. Subscription handlers
3. View functions
## Event Handlers - Part 1
Event Handlers are pure functions and consequently easy to test.
First, create an event handler like this:
```clj
(defn my-db-handler
[db v]
... return a modified version of db)
```
Then, register it in a separate step:
```clj
(re-event-db
:some-id
[some-interceptors]
my-handler)
```
With this setup, `my-db-handler` is available for direct testing.
Your unittests will pass in certain values for `db` and `v`, and then ensure it returns the right (modified version of) `db`.
## Event Handlers - Part 2
Event handlers mutate the value in `app-db` - that's their job.
I'd recommend defining a [Prismatic Schema](https://github.com/Prismatic/schema)
for the value in `app-db` and then checking for correctness after every,
single event handler. Every single one.
Using `after` middleware, this is easy to arrange. The todomvc example shows how:
- [define a Schema](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/db.cljs#L6-L28) for the value in `app-db`
- [create some middleware](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/handlers.cljs#L11-L19)
- [add the middleware](https://github.com/Day8/re-frame/blob/2ba8914d8dd5f0cf2b09d6f3942823a798c2ef5c/examples/todomvc/src/todomvc/handlers.cljs#L46) to your event handlers
## Subscription Handlers
Here's a subscription handler from [the todomvc example](https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/subs.cljs):
```clj
(reg-sub
:completed-count
(fn [db _]
(completed-count (:todos db))))
```
How do we test this?
We could split the handler function from its registration, like this:
```clj
(defn get-completed-count
[app-db _]
(reaction (completed-count (:todos @app-db))))
(register-sub
:completed-count
get-completed-count)
```
That makes `get-completed-count` available for direct testing. But you'll note it isn't a pure function.
It isn't values in, values out. Instead, it is atoms in, atoms out.
If this function was on a paint chart, they'd call in "Arctic Fusion" to indicate its
proximity to pure white, while hinting at taints.
We could accept this. We could create tests by passing in a `reagent/atom` holding the
certain values and then checking the values in what's returned. That would work.
The more pragmatic among us might even approve.
But let's assume that's not good enough. Let's refactor for pureness:
The 1st step in this refactor is to create a pure function which actually does ALL the hard work ...
```clj
(defn completed-count-handler
[db v] ;; db is a value, not an atom
..... return a value here based on the values db and v)
```
The 2nd step in the refactor is to register using a thin `reaction` wrapper:
```clj
(register-sub
:completed-count
(fn [app-db v]
(reaction (completed-count-handler @app-db v))))
```
Because `completed-count-handler` is now doing all the work, it is the thing we want
to test, and it is now a pure function. So I think we are there.
## Components- Part 1
Components/views are slightly more tricky. There's a few options.
First, I have to admit an ugly secret. I don't tend to write tests for my views.
Hey, don't give me that disproving frown! I have my reasons.
Remember that every line of code you write is a liability. So tests have to earn
their keep - they have to deliver a good cost / benefit ratio. And, in my experience
with the re-frame architecture, the Reagent view components tend to be an unlikely
source of bugs. There's just not much logic in them for me to get wrong.
Okay, fine, don't believe me, then!!
Here's how, theoretically, I'd write tests if I wasn't me ...
If a Components is a [Form-1](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-1-a-simple-function) structure, then it is fairly easy to test.
A trivial example:
```clj
(defn greet
[name]
[:div "Hello " name])
(greet "Wiki")
;;=> [:div "Hello " "Wiki"]
```
So, here, testing involves passing values into the function and checking the data structure returned for correctness.
What's returned is hiccup, of course. So how do you test hiccup for correctness?
hiccup is just a clojure data structure - vectors containing keywords, and maps, and other vectors, etc. Perhaps you'd use https://github.com/nathanmarz/specter to declaratively check on the presence of certain values and structures? Or do it more manually.
## Components - Part 2A
But what if the Component has a subscription (via a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function) structure)?
```clj
(defn my-view
[something]
(let [val (subscribe [:query-id])] <-- reactive subscription
(fn [something] <-- returns the render function
[:div .... using @val in here])))
```
There's no immediately obvious way to test this as a lovely pure function. Because it is not pure.
Of course, less pure ways are very possible. For example, a plan might be:
1. setup `app-db` with some values in the right places (for the subscription)
2. call `my-view` (with a parameter) which will return `the renderer`
3. call `the renderer` (with a parameter) which will return hiccup
4. check the hiccup structure for correctness.
Continuing on, in a second phase you could then:
5. change the value in `app-db` (which will cause the subscription to fire)
6. call `the renderer` again (hiccup returned).
7. check that the hiccup
Which is all possible, if a little messy, and with one gotcha. After you change the value in `app-db` the subscription won't hold the new value straight away. It won't get calculated until the next animationFrame. And the next animationFrame won't happen until you hand back control to the browser. I think. Untested. Please report back here if you try. And you might also be able to use `reagent.core/flush` to force the view to be updated.
## Components - Part 2B
Or ... instead of the not-very-pure method above, you could use `with-redefs` on `subscribe` to stub out re-frame altogether:
```clj
(defn subscription-stub [x]
(atom
(case x
[:query-id] 42)))
(deftest some-test
(with-redefs [re-frame/subscribe (subscription-stub)]
(testing "some rendering"
..... somehow call or render the component and check the output)))
```
For more integration level testing, you can use `with-mounted-component` from the [reagent-template](https://github.com/reagent-project/reagent-template/blob/master/src/leiningen/new/reagent/test/cljs/reagent/core_test.cljs) to render the component in the browser and validate the generated DOM.
## Components - Part 2C
Or ... you can structure in the first place for easier testing and pure functions.
The trick here is to create an outer and inner component. The outer sources the data
(via a subscription), and passes it onto the inner as props (parameters).
As a result, the inner component, which does the testable work, is pure and
easily tested. The outer is fairly trivial.
To get a more concrete idea, I'll direct you to another page on this Wiki
which has nothing to do with testing, but it does use this `simple-outer-subscribe-with-complicated-inner-render`
pattern for a different purpose: [[Using-Stateful-JS-Components]]
Note this technique could be made simple and almost invisible via the
use of macros. (Contribute one if you have it).
This pattern has been independently discovered by many. For example, here
it is called the [Container/Component pattern](https://medium.com/@learnreact/container-components-c0e67432e005#.mb0hzgm3l).
## Summary
So, we stumbled slightly at the final hurdle with Form-2 Components. But prior
to this, the testing story for re-frame was as good as it gets: you are testing
a bunch of simple, pure functions. No dependency injection in sight!

View File

@ -1,3 +1,14 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [The re-frame Logo](#the-re-frame-logo)
- [Who](#who)
- [Genesis Theories](#genesis-theories)
- [Assets Where?](#assets-where)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## The re-frame Logo
![logo](/images/logo/re-frame_256w.png?raw=true)

View File

@ -13,6 +13,19 @@ thrill of that forbidden fruit.
I won't tell, if you don't. But careful plans must be made ...
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [The overall plan](#the-overall-plan)
- [Example Using Google Maps](#example-using-google-maps)
- [Pattern Discovery](#pattern-discovery)
- [Code Credit](#code-credit)
- [D3 Examples](#d3-examples)
- [Advanced Lifecycle Methods](#advanced-lifecycle-methods)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
### The overall plan
@ -87,6 +100,8 @@ D3 (from @zachcp):
- Code: https://github.com/zachcp/simplecomponent
- Example: http://zachcp.github.io/simplecomponent/
A different take on using D3:
https://gadfly361.github.io/gadfly-blog/2016-10-22-d3-in-reagent.html
### Advanced Lifecycle Methods

232
docs/WIP/Flow.md Normal file
View File

@ -0,0 +1,232 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [Flow](#flow)
- [1 -> 2](#1---2)
- [2 -> 3](#2---3)
- [3->4->5->6](#3-4-5-6)
- [On Flow](#on-flow)
- [How Flow Happens In Reagent](#how-flow-happens-in-reagent)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Flow
It is time to talk of the reactive flow through dominoes 4-5-6. through dominoes It is time to talk about de-duplicated signal graphs.
This tutorial focuses mainly on how data flows between dominoes 3-4-5-6.
We'll look at the underlying reactive mechanism.
BUT we'll start by looking at the overall picture ...
## Interconnections
Ask a Systems Theorist, and they'll tell you that a system has **parts** and **interconnections**.
Human brains tend to focus first on the **parts**, and then, later, maybe on
**interconnections**. But we know better, right? We
know interconnections are often critical to a system.
"Focus on the lines between the boxes" we lecture anyone kind enough to listen (in my case, glassy-eyed family members).
In the case of re-frame, dominoes are the **parts**, so, tick, yes, we have
looked at them first. Our brains are happy. But what about the **interconnections**?
If the **parts** are functions, what does it even mean to talk about **interconnections between functions?**
To answer that question, I'll rephrase it as:
how are the domino functions **composed**?
At the language level,
Uncle Alonzo and Uncle John tell us how a function like `count` composes:
```clj
(str (count (filter odd? [1 2 3 4 5])))
```
We know when `count` is called, and with what
argument, and how the value it computes becomes the arg for a further function.
We know how data "flows" into and out of the functions.
Sometimes, we'd rewrite this code as:
```clj
(->> [1 2 3 4 5]
(filter odd?)
count
str)
```
With this arrangement, we talk of "threading" data
through functions. **It seems to help our comprehension to frame function
composition in terms of data flow**.
re-frame delivers architecture
by supplying the interconnections - it threads the data - it composes the dominoes - it is the lines between the boxes.
But re-frame has no universal method for this. The technique it uses varies from one domino neighbour
pair to the next.
## Between 1 and 2
There's a queue.
When you `dispatch` an event, it is put into a FIFO queue to be processed "vey soon".
It is important to the design of re-frame that event processing is async.
On the end of the queue, is a `router` which (very soon) will:
- pick up events one after the other
- for each, it extracts `kind` of event (first element of the event vector)
- for each, it looks up the associated event handler and calls it
## Between 2 and 3
I lied above.
I said the `router` called the event handler associated with an event. This is a
useful simplification, but we'll see in future tutorials that there's more going on.
I'll wave my hands about now and give you a sense of the real story.
Instead of there being a single handler function, there's actually a pipeline of functions which
we call an interceptor chain. The handler you write is inserted into the middle of this pipeline.
This function pipeline manages three things:
- it prepares the `coeffect` for the event handler (the set of inputs required by the handler)
- it calls the event handler (Domino 2)
- it handles the `effects` produced by the event handler (Domino 3)
The router actually looks up the associated "interceptor chain", which happens to have the handler wrapped on the end.
And then it processes the interceptor chain. Which is to say it calls a
There's
- calls the handler , looks at their first , looks at their
first element, and runs the associated
Between 1 and 2 it is a queue & router,
between 2 and 3 it is an interceptor pipeline, and along the 3-4-5-6 domino axis there's a reactive signal graph. The right
tool for the job in each case, I'd argue.
While interconnections are critical to how **re-frame works**,
you can happily **use re-frame** for a long time and be mostly ignorant of their details.
Which is a good thing - back we go to happy brains focusing on the **parts**.
## 1 -> 2
`dispatch` queues events and they are not immediately processed. Event handling is done async.
A router reads events from this queue, looks up the associated handler and calls it.
## 2 -> 3
Except I lied in the previous section. The router doesn't really look
up a single "handler". Instead it looks up an interceptor chain. The method by which
an Interceptor chain is executed is discussed in great detail shortly.
## 3->4->5->6
So now we are at the meat and potatoes. The real subject of this tutorial.
## On Flow
Arguments from authority ...
> Everything flows, nothing stands still. (Panta rhei)
> No man ever steps in the same river twice for it's not the same river and he's not the same man.
[Heraclitus 500 BC](http://en.wikiquote.org/wiki/Heraclitus). Who, being Greek, had never seen a frozen river. [alt version](http://farm6.static.flickr.com/5213/5477602206_ecb78559ed.jpg).
> Think of an experience from your childhood. Something you remember clearly, something you can see,
feel, maybe even smell, as if you were really there. After all you really were there at the time,
werent you? How else could you remember it? But here is the bombshell: you werent there. Not a
single atom that is in your body today was there when that event took place .... Matter flows
from place to place and momentarily comes together to be you. Whatever you are, therefore, you
are not the stuff of which you are made. If that does not make the hair stand up on the back of
your neck, read it again until it does, because it is important.
Steve Grand
### How Flow Happens In Reagent
To implement a reactive flow, Reagent provides a `ratom` and a `reaction`.
re-frame uses both of these
building blocks, so let's now make sure we understand them.
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
perspective, we'll tweak that paradigm slightly and **view a `ratom` as having a value that
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
`ratom` as a Signal. [Pause and read this](http://elm-lang.org:1234/guide/reactivity).
The 2nd building block, `reaction`, acts a bit like a function. It's a macro which wraps some
`computation` (a block of code) and returns a `ratom` holding the result of that `computation`.
The magic thing about a `reaction` is that the `computation` it wraps will be automatically
re-run whenever 'its inputs' change, producing a new output (return) value.
Eh, how?
Well, the `computation` is just a block of code, and if that code dereferences one or
more `ratoms`, it will be automatically re-run (recomputing a new return value) whenever any
of these dereferenced `ratoms` change.
To put that yet another way, a `reaction` detects a `computation's` input Signals (aka input `ratoms`)
and it will `watch` them, and when, later, it detects a change in one of them, it will re-run that
computation, and it will `reset!` the new result of that computation into the `ratom` originally returned.
So, the `ratom` returned by a `reaction` is itself a Signal. Its value will change over time when
the `computation` is re-run.
So, via the interplay between `ratoms` and `reactions`, values 'flow' into computations and out
again, and then into further computations, etc. "Values" flow (propagate) through the Signal graph.
But this Signal graph must be without cycles, because cycles cause mayhem! re-frame achieves
a unidirectional flow.
Right, so that was a lot of words. Some code to clarify:
```Clojure
(ns example1
(:require-macros [reagent.ratom :refer [reaction]]) ;; reaction is a macro
(:require [reagent.core :as reagent]))
(def app-db (reagent/atom {:a 1})) ;; our root ratom (signal)
(def ratom2 (reaction {:b (:a @app-db)})) ;; reaction wraps a computation, returns a signal
(def ratom3 (reaction (condp = (:b @ratom2) ;; reaction wraps another computation
0 "World"
1 "Hello")))
;; Notice that both computations above involve de-referencing a ratom:
;; - app-db in one case
;; - ratom2 in the other
;; Notice that both reactions above return a ratom.
;; Those returned ratoms hold the (time varying) value of the computations.
(println @ratom2) ;; ==> {:b 1} ;; a computed result, involving @app-db
(println @ratom3) ;; ==> "Hello" ;; a computed result, involving @ratom2
(reset! app-db {:a 0}) ;; this change to app-db, triggers re-computation
;; of ratom2
;; which, in turn, causes a re-computation of ratom3
(println @ratom2) ;; ==> {:b 0} ;; ratom2 is result of {:b (:a @app-db)}
(println @ratom3) ;; ==> "World" ;; ratom3 is automatically updated too.
```
So, in FRP-ish terms, a `reaction` will produce a "stream" of values over time (it is a Signal),
accessible via the `ratom` it returns.
Okay, that was all important background information for what is to follow.

535
docs/WIP/scratch-pad.md Normal file
View File

@ -0,0 +1,535 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table Of Contents
- [It solves a dilemma](#it-solves-a-dilemma)
- [Implements Reactive Data Flows](#implements-reactive-data-flows)
- [Flow](#flow)
- [Reactive Programming](#reactive-programming)
- [Components](#components)
- [Truth Interlude](#truth-interlude)
- [React etc.](#react-etc)
- [Subscribe](#subscribe)
- [The Signal Graph](#the-signal-graph)
- [A More Efficient Signal Graph](#a-more-efficient-signal-graph)
- [](#)
- [Prefer Dumb Views - Part 1](#prefer-dumb-views---part-1)
- [Prefer Dumb Views - Part 2](#prefer-dumb-views---part-2)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## It solves a dilemma
SPAs are fundamentally mutative in nature.
They change the DOM, databases, localstore, cookies, send emails etc. Its a veritable frenzy of mutation. And
this is a good thing. Any user of these SPAs wants to be changing the world,
right, otherwise what's the point?
But we are wide-eyed functional zealots, heroically resisting the
entire notion of mutation, and insisting instead on the wonders of pure functions.
re-frame solves this dilemma and allows you
compose a mutative application from pure functions.
XXX
I'll be using [Reagent] at an intermediate level, so you will need to have done some
introductory Reagent tutorials before going on. Try:
- [The Introductory Tutorial](http://reagent-project.github.io/) or
- [this one](https://github.com/jonase/reagent-tutorial) or
- [Building Single Page Apps with Reagent](http://yogthos.net/posts/2014-07-15-Building-Single-Page-Apps-with-Reagent.html).
## Implements Reactive Data Flows
This document describes how re-frame implements
the reactive data flows in dominoes 4 and 5 (queries and views).
It explains
the low level mechanics of the process which not something you
need to know initially. So, you can defer reading and understanding
this until later, if you wish. But you should at some point circle
back and grok it. It isn't hard at all.
## Flow
## Reactive Programming
We'll get to the meat in a second, I promise, but first one final, useful diversion ...
Terminology in the FRP world seems to get people hot under the collar. Those who believe in continuous-time
semantics might object to me describing re-frame as having FRP-nature. They'd claim that it does something
different from pure FRP, which is true.
But, these days, FRP seems to have become a
["big tent"](http://soft.vub.ac.be/Publications/2012/vub-soft-tr-12-13.pdf)
(a broad church?).
Broad enough perhaps that re-frame can be in the far, top, left paddock of the tent, via a series of
qualifications: re-frame has "discrete, dynamic, asynchronous, push FRP-ish-nature" without "glitch free" guarantees.
(Surprisingly, "glitch" has specific meaning in FRP).
**If you are new to FRP, or reactive programming generally**, browse these resources before
going further (certainly read the first two):
- [Creative Explanation](http://paulstovell.com/blog/reactive-programming)
- [Reactive Programming Backgrounder](https://gist.github.com/staltz/868e7e9bc2a7b8c1f754)
- [presentation (video)](http://www.infoq.com/presentations/ClojureScript-Javelin) by Alan Dipert (co-author of Hoplon)
- [serious pants Elm thesis](https://www.seas.harvard.edu/sites/default/files/files/archived/Czaplicki.pdf)
## Components
Extending the diagram, we introduce `components`:
```
app-db --> components --> Hiccup
```
When using Reagent, your primary job is to write one or more `components`.
This is the view layer.
Think about `components` as `pure functions` - data in, Hiccup out. `Hiccup` is
ClojureScript data structures which represent DOM. Here's a trivial component:
```Clojure
(defn greet
[]
[:div "Hello ratoms and reactions"])
```
And if we call it:
```Clojure
(greet)
;; ==> [:div "Hello ratoms and reactions"]
```
You'll notice that our component is a regular Clojure function, nothing special. In this case, it takes
no parameters and it returns a ClojureScript vector (formatted as Hiccup).
Here is a slightly more interesting (parameterised) component (function):
```Clojure
(defn greet ;; greet has a parameter now
[name] ;; 'name' is a ratom holding a string
[:div "Hello " @name]) ;; dereference 'name' to extract the contained value
;; create a ratom, containing a string
(def n (reagent/atom "re-frame"))
;; call our `component` function, passing in a ratom
(greet n)
;; ==> [:div "Hello " "re-frame"] returns a vector
```
So components are easy - at core they are a render function which turns data into
Hiccup (which will later become DOM).
Now, let's introduce `reaction` into this mix. On the one hand, I'm complicating things
by doing this, because Reagent allows you to be ignorant of the mechanics I'm about to show
you. (It invisibly wraps your components in a `reaction` allowing you to be blissfully
ignorant of how the magic happens.)
On the other hand, it is useful to understand exactly how the Reagent Signal graph is wired,
because in a minute, when we get to `subscriptions`, we'll be directly using `reaction`, so we
might as well bite the bullet here and now ... and, anyway, it is pretty easy...
```Clojure
(defn greet ;; a component - data in, Hiccup out.
[name] ;; name is a ratom
[:div "Hello " @name]) ;; dereference name here, to extract the value within
(def n (reagent/atom "re-frame"))
;; The computation '(greet n)' returns Hiccup which is stored into 'hiccup-ratom'
(def hiccup-ratom (reaction (greet n))) ;; <-- use of reaction !!!
;; what is the result of the initial computation ?
(println @hiccup-ratom)
;; ==> [:div "Hello " "re-frame"] ;; returns hiccup (a vector of stuff)
;; now change 'n'
;; 'n' is an input Signal for the reaction above.
;; Warning: 'n' is not an input signal because it is a parameter. Rather, it is
;; because 'n' is dereferenced within the execution of the reaction's computation.
;; reaction notices what ratoms are dereferenced in its computation, and watches
;; them for changes.
(reset! n "blah") ;; n changes
;; The reaction above will notice the change to 'n' ...
;; ... and will re-run its computation ...
;; ... which will have a new "return value"...
;; ... which will be "reset!" into "hiccup-ratom"
(println @hiccup-ratom)
;; ==> [:div "Hello " "blah"] ;; yep, there's the new value
```
So, as `n` changes value over time (via a `reset!`), the output of the computation `(greet n)`
changes, which in turn means that the value in `hiccup-ratom` changes. Both `n` and
`hiccup-ratom` are FRP Signals. The Signal graph we created causes data to flow from
`n` into `hiccup-ratom`.
Derived Data, flowing.
### Truth Interlude
I haven't been entirely straight with you:
1. Reagent re-runs `reactions` (re-computations) via requestAnimationFrame. So a
re-computation happens about 16ms after an input Signals change is detected, or after the
current thread of processing finishes, whichever is the greater. So if you are in a bREPL
and you run the lines of code above one after the other too quickly, you might not see the
re-computation done immediately after `n` gets reset!, because the next animationFrame
hasn't run (yet). But you could add a `(reagent.core/flush)` after the reset! to force
re-computation to happen straight away.
2. `reaction` doesn't actually return a `ratom`. But it returns something that has
ratom-nature, so we'll happily continue believing it is a `ratom` and no harm will come to us.
On with the rest of my lies and distortions...
### React etc.
Okay, so we have some unidirectional, dynamic, async, discrete FRP-ish data flow happening here.
Question: To which ocean does this river of data flow? Answer: The DOM ocean.
The full picture:
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
```
Best to imagine this process as a pipeline of 3 functions. Each
function takes data from the
previous step, and produces (derived!) data for the next step. In the next
diagram, the three functions are marked (f1, f2, f3). The unmarked nodes are derived data,
produced by one step, to be input to the following step. Hiccup,
VDOM and DOM are all various forms of HTML markup (in our world that's data).
```
app-db --> components --> Hiccup --> Reagent --> VDOM --> React --> DOM
f1 f2 f3
```
In abstract ClojureScript syntax terms, you could squint and imagine the process as:
```Clojure
(-> app-db
components ;; produces Hiccup
Reagent ;; produces VDOM (virtual DOM that React understands)
React ;; produces HTML (which magically and efficiently appears on the page).
Browser ;; produces pixels
Monitor) ;; produces photons?
```
Via the interplay between `ratom` and `reaction`, changes to `app-db` stream into the pipeline, where it
undergoes successive transformations, until pixels colour the monitor you to see.
Derived Data, flowing. Every step is acting like a pure function and turning data into new data.
All well and good, and nice to know, but we don't have to bother ourselves with most of the pipeline.
We just write the `components`
part and Reagent/React will look after the rest. So back we go to that part of the picture ...
## Subscribe
`components` (view layer) need to query aspects of `app-db` (data layer).
But how?
Let's pause to consider **our dream solution** for this part of the flow. `components` would:
* obtain data from `app-db` (their job is to turn this data into hiccup).
* obtain this data via a (possibly parameterised) query over `app-db`. Think database kind of query.
* automatically recompute their hiccup output, as the data returned by the query changes, over time
* use declarative queries. Components should know as little as possible about the structure of `app-db`. SQL? Datalog?
re-frame's `subscriptions` are an attempt to live this dream. As you'll see, they fall short on the declarative
query part, but they comfortably meet the other requirements.
As a re-frame app developer, your job will be to write and register one or more
"subscription handlers" - functions that do a named query.
Your subscription functions must return a value that changes over time (a Signal). I.e. they'll
be returning a reaction or, at least, the `ratom` produced by a `reaction`.
Rules:
- `components` never source data directly from `app-db`, and instead, they use a subscription.
- subscriptions are only ever used by components (they are never used in, say, event handlers).
Here's a component using a subscription:
```Clojure
(defn greet ;; outer, setup function, called once
[]
(let [name-ratom (subscribe [:name-query])] ;; <---- subscribing happens here
(fn [] ;; the inner, render function, potentially called many times.
[:div "Hello" @name-ratom])))
```
First, note this is a [Form-2](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function)
`component` ([there are 3 forms](https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components)).
Previously in this document, we've used the simplest, `Form-1` components (no setup was required, just render).
With `Form-2` components, there's a function returning a function:
- the returned function is the render function. Behind the scenes, Reagent will wrap this render function
in a `reaction` to make it produce new Hiccup when its input Signals change. In our example above, that
means it will rerun every time `name-ratom` changes.
- the outer function is a setup function, called once for each instance of the component. Notice the use of
'subscribe' with the parameter `:name-query`. That creates a Signal through which new values are supplied
over time; each new value causing the returned function (the actual renderer) to be run.
>It is important to distinguish between a new instance of the component versus the same instance of a component reacting to a new value. Simplistically, a new component is returned for every unique value the setup function (i.e. the outer function) is called with. This allows subscriptions based on initialisation values to be created, for example:
``` Clojure
(defn my-cmp [row-id]
(let [row-state (subscribe [row-id])]
(fn [row-id]
[:div (str "Row: " row-id " is " @row-state)])))
```
In this example, `[my-cmp 1][my-cmp 2]` will create two instances of `my-cmp`. Each instance will re-render when its internal `row-state` signal changes.
`subscribe` is always called like this:
```Clojure
(subscribe [query-id some optional query parameters])
```
There is only one (global) `subscribe` function and it takes one parameter, assumed to be a vector.
The first element in the vector (shown as `query-id` above) identifies/names the query and the other elements are optional
query parameters. With a traditional database a query might be:
```
select * from customers where name="blah"
```
In re-frame, that would be done as follows:
`(subscribe [:customer-query "blah"])`
which would return a `ratom` holding the customer state (a value which might change over time!).
So let's now look at how to write and register the subscription handler for `:customer-query`
```Clojure
(defn customer-query ;; a query over 'app-db' which returns a customer
[db, [sid cid]] ;; query fns are given 'app-db', plus vector given to subscribe
(assert (= sid :customer-query)) ;; subscription id was the first element in the vector
(reaction (get-in @db [:path :to :a :map cid]))) ;; re-runs each time db changes
;; register our query handler
(register-sub
:customer-query ;; the id (the name of the query)
customer-query) ;; the function which will perform the query
```
Notice how the handler is registered to handle `:customer-query` subscriptions.
**Rules and Notes**:
- you'll be writing one or more handlers, and you will need to register each one.
- handlers are functions which take two parameters: the db atom, and the vector given to subscribe.
- `components` tend to be organised into a hierarchy, often with data flowing from parent to child via
parameters. So not every component needs a subscription. Very often the values passed in from a parent component
are sufficient.
- subscriptions can only be used in `Form-2` components and the subscription must be in the outer setup
function and not in the inner render function. So the following is **wrong** (compare to the correct version above)
```Clojure
(defn greet ;; a Form-1 component - no inner render function
[]
(let [name-ratom (subscribe [:name-query])] ;; Eek! subscription in renderer
[:div "Hello" @name-ratom]))
```
Why is this wrong? Well, this component would be re-rendered every time `app-db` changed, even if the value
in `name-ratom` (the result of the query) stayed the same. If you were to use a `Form-2` component instead, and put the
subscription in the outer functions, then there'll be no re-render unless the value queried (i.e. `name-ratom`) changed.
## The Signal Graph
Let's sketch out the situation described above ...
`app-db` would be a bit like this (`items` is a vector of maps):
```Clojure
(def L [{:name "a" :val 23 :flag "y"}
{:name "b" :val 81 :flag "n"}
{:name "c" :val 23 :flag "y"}])
(def app-db (reagent/atom {:items L
:sort-by :name})) ;; sorted by the :name attribute
```
The subscription-handler might be written:
```Clojure
(register-sub
:sorted-items ;; the query id (the name of the query)
(fn [db [_]] ;; the handler for the subscription
(reaction
(let [items (get-in @db [:items]) ;; extract items from db
sort-attr (get-in @db [:sort-by])] ;; extract sort key from db
(sort-by sort-attr items))))) ;; return them sorted
```
Subscription handlers are given two parameters:
1. `app-db` - that's a reagent/atom which holds ALL the app's state. This is the "database"
on which we perform the "query".
2. the vector originally supplied to `subscribe`. In our case, we ignore it.
In the example above, notice that the `reaction` depends on the input Signal: `db`.
If `db` changes, the query is re-run.
In a component, we could use this query via `subscribe`:
```Clojure
(defn items-list ;; Form-2 component - outer, setup function, called once
[]
(let [items (subscribe [:sorted-items]) ;; <-- subscribe called with name
num (reaction (count @items)) ;; Woh! a reaction based on the subscription
top-20 (reaction (take 20 @items))] ;; Another dependent reaction
(fn []
[:div
(str "there's " @num " of these suckers. Here's top 20") ;; rookie mistake to leave off the @
(into [:div ] (map item-render @top-20))]))) ;; item-render is another component, not shown
```
There's a bit going on in that `let`, most of it tortuously contrived, just so I can show off chained
reactions. Okay, okay, all I wanted really was an excuse to use the phrase "chained reactions".
The calculation of `num` is done by a `reaction` which has `items` as an input Signal. And,
as we saw, `items` is itself a reaction over two other signals (one of them the `app-db`).
So this is a Signal Graph. Data is flowing through computation into renderer, which produce Hiccup, etc.
## A More Efficient Signal Graph
But there is a small problem. The approach above might get inefficient, if `:items` gets long.
Every time `app-db` changes, the `:sorted-items` query is
going to be re-run and it's going to re-sort `:items`. But `:items` might not have changed. Some other
part of `app-db` may have changed.
We don't want to perform this computationally expensive re-sort
each time something unrelated in `app-db` changes.
Luckily, we can easily fix that up by tweaking our subscription function so
that it chains `reactions`:
```Clojure
(register-sub
:sorted-items ;; the query id
(fn [db [_]]
(let [items (reaction (get-in @db [:some :path :to :items]))] ;; reaction #1
sort-attr (reaction (get-in @db [:sort-by]))] ;; reaction #2
(reaction (sort-by @sort-attr @items))))) ;; reaction #3
```
The original version had only one `reaction` which would be re-run completely each time `app-db` changed.
This new version, has chained reactions.
The 1st and 2nd reactions just extract from `db`. They will run each time `app-db` changes.
But they are cheap. The 3rd one does the expensive
computation using the result from the first two.
That 3rd, expensive reaction will be re-run when either one of its two input Signals change, right? Not quite.
`reaction` will only re-run the computation when one of the inputs has **changed in value**.
`reaction` compares the old input Signal value with the new Signal value using `identical?`. Because we're
using immutable data structures
(thank you ClojureScript), `reaction` can perform near instant checks for change on even
deeply nested and complex
input Signals. And `reaction` will then stop unneeded propagation of `identical?` values through the
Signal graph.
In the example above, reaction #3 won't re-run until `:items` or `:sort-by` are different
(do not test `identical?`
to their previous value), even though `app-db` itself has changed (presumably somewhere else).
Hideously contrived example, but I hope you get the idea. It is all screamingly efficient.
Summary:
- you can chain reactions.
- a reaction will only be re-run when its input Signals test not `identical?` to previous value.
- As a result, unnecessary Signal propagation is eliminated using highly efficient checks,
even for large, deep nested data structures.
Back to the more pragmatic world ...
[SPAs]:http://en.wikipedia.org/wiki/Single-page_application
[SPA]:http://en.wikipedia.org/wiki/Single-page_application
[Reagent]:http://reagent-project.github.io/
[Dan Holmsand]:https://twitter.com/holmsand
[Flux]:http://facebook.github.io/flux/docs/overview.html#content
[Hiccup]:https://github.com/weavejester/hiccup
[FRP]:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
[Elm]:http://elm-lang.org/
[OM]:https://github.com/swannodette/om
[Prismatic Schema]:https://github.com/Prismatic/schema
[Hoplon]:http://hoplon.io/
[Pedestal App]:https://github.com/pedestal/pedestal-app
-----------------
## Prefer Dumb Views - Part 1
Many events are dispatched by the DOM in response to user actions.
For example, a button view might be like this:
```clj
(defn yes-button
[]
[:div {:class "button-class"
:on-click #(dispatch [:yes-button-clicked])}
"Yes"])
```
Notice that `on-click` DOM handler:
```clj
#(dispatch [:yes-button-clicked])
```
With re-frame, we want the DOM as passive as possible. We do
not want our views containing any imperative control logic.
All of that should be computed by event handlers.
We want that "on-click" as simple as we can make it.
**Rule**: `views` are as passive and minimal as possible when it
comes to handling events. They `dispatch` pure data and nothing more.
## Prefer Dumb Views - Part 2
Neither do we want views computing the data they render.
That's the job of a subscription:
So this is bad:
```clj
(defn show-items
[]
(let [sorted-items (sort @(subscribe [:items]))] ;; <--
(into [:div] (for [i sorted-items] [item-view i]))))
```
The view is not simply taking the data supplied by the

View File

@ -1,9 +1,24 @@
# Reagent example app now using re-frame
# A Simple App
Run "`lein do clean, figwheel`" in a terminal to compile the app, and then open `http://localhost:3449/example.html`.
This tiny application is meant to provide a quick start of the basics of re-frame.
Any changes to ClojureScript source files (in `src`) will be reflected in the running page immediately (while "`lein figwheel`" is running).
All the code is in one namespace `/src/simpleexample/core.cljs`
Run "`lein do clean, with-profile prod compile`" to compile an optimized version, and then open `resources/public/example.html`.
### Run It And Change It
Original reagent example code found at https://github.com/reagent-project/reagent
Steps:
A. Check out the re-frame repo
1. Get a command line
2. `cd` to the root of this sub project (where this README exists)
3. run "`lein do clean, figwheel`" to compile the app,
4. open `http://localhost:3449/example.html` to see the app
Whileever step 3 is running, any changes you make to the ClojureScript
source files (in `src`) will be re-compiled and reflected in the running
page immediately.
### Production Version
Run "`lein do clean, with-profile prod compile`" to compile an optimized
version, and then open `resources/public/example.html`.

View File

@ -1,7 +0,0 @@
(ns simpleexample.dev
(:require [simpleexample.core :as example]
[figwheel.client :as fw]))
(fw/start {:on-jsload example/run
:websocket-url "ws://localhost:3449/figwheel-ws"})

View File

@ -1,4 +1,4 @@
(defproject simple-re-frame "0.8.0"
(defproject simple "0.8.0"
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.227"]
[reagent "0.6.0-rc"]
@ -10,8 +10,8 @@
:hooks [leiningen.cljsbuild]
:profiles {:dev {:cljsbuild
{:builds {:client {:source-paths ["devsrc"]
:compiler {:main "simpleexample.dev"
{:builds {:client {:figwheel {:on-jsload "simple.core/run"}
:compiler {:main "simple.core"
:asset-path "js"
:optimizations :none
:source-map true

View File

@ -12,7 +12,7 @@
<script src="js/client.js"></script>
<script>
window.onload = function () {
simpleexample.core.run();
simple.core.run();
}
</script>
</body>

View File

@ -0,0 +1,86 @@
(ns simple.core
(:require [reagent.core :as reagent]
[re-frame.core :as rf]))
;; -- Domino 1 - Event Dispatch -----------------------------------------------
(defn dispatch-timer-event
[]
(let [now (js/Date.)]
(rf/dispatch [:timer now]))) ;; <-- dispatch used
;; Call the dispatching function every second.
;; `defonce` is like `def` but it ensures only instance is ever
;; created in the face of figwheel hot-reloading of this file.
(defonce do-timer (js/setInterval dispatch-timer-event 1000))
;; -- Domino 2 - Event Handlers -----------------------------------------------
(rf/reg-event-db ;; sets up initial application state
:initialize ;; usage: (dispatch [:initialize])
(fn [_ _] ;; the two parameters are not important here, so use _
{:time (js/Date.) ;; What it returns becomes the new application state
:time-color "#f88"})) ;; so the application state will initially be a map with two keys
(rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562])
:time-color-change ;; dispatched when the user enters a new colour into the UI
(fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector)
(assoc db :time-color new-color-value))) ;; compute and return the new application state
(rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date])
:timer ;; every second an event of this kind will be dispatched
(fn [db [_ new-time]] ;; note how the 2nd parameter is desctructure to obtain the data value
(assoc db :time new-time))) ;; compute and return the new application state
;; -- Domino 4 - Query -------------------------------------------------------
(rf/reg-sub
:time
(fn [db _] ;; db is current app state. 2nd usused param is query vector
(-> db
:time)))
(rf/reg-sub
:time-color
(fn [db _]
(:time-color db)))
;; -- Domino 5 - View Functions ----------------------------------------------
(defn clock
[]
[:div.example-clock
{:style {:color @(rf/subscribe [:time-color])}}
(-> @(rf/subscribe [:time])
.toTimeString
(clojure.string/split " ")
first)])
(defn color-input
[]
[:div.color-input
"Time color: "
[:input {:type "text"
:value @(rf/subscribe [:time-color])
:on-change #(rf/dispatch [:time-color-change (-> % .-target .-value)])}]]) ;; <---
(defn ui
[]
[:div
[:h1 "Hello world, it is now"]
[clock]
[color-input]])
;; -- Entry Point -------------------------------------------------------------
(defn ^:export run
[]
(rf/dispatch-sync [:initialize]) ;; puts a value into application state
(reagent/render [ui] ;; mount the application's ui into '<div id="app" />'
(js/document.getElementById "app")))

View File

@ -1,113 +0,0 @@
(ns simpleexample.core
(:require-macros [reagent.ratom :refer [reaction]])
(:require [reagent.core :as reagent]
[re-frame.core :refer [reg-event-db
path
reg-sub
dispatch
dispatch-sync
subscribe]]))
;; trigger a dispatch every second
(defonce time-updater (js/setInterval
#(dispatch [:timer (js/Date.)]) 1000))
(def initial-state
{:timer (js/Date.)
:time-color "#f88"})
;; -- Event Handlers ----------------------------------------------------------
(reg-event-db ;; setup initial state
:initialize ;; usage: (dispatch [:initialize])
(fn
[db _]
(merge db initial-state))) ;; what it returns becomes the new state
(reg-event-db
:time-color ;; usage: (dispatch [:time-color 34562])
(path [:time-color]) ;; this is middleware
(fn
[time-color [_ value]] ;; path middleware adjusts the first parameter
value))
(reg-event-db
:timer
(fn
;; the first item in the second argument is :timer the second is the
;; new value
[db [_ value]]
(assoc db :timer value))) ;; return the new version of db
;; -- Subscription Handlers ---------------------------------------------------
(reg-sub
:timer
(fn
[db _] ;; db is the value currently in the app-db atom
(:timer db)))
(reg-sub
:time-color
(fn
[db _]
(:time-color db)))
;; -- View Components ---------------------------------------------------------
(defn greeting
[message]
[:h1 message])
(defn clock
[]
(let [time-color (subscribe [:time-color])
timer (subscribe [:timer])]
(fn clock-render
[]
(let [time-str (-> @timer
.toTimeString
(clojure.string/split " ")
first)
style {:style {:color @time-color}}]
[:div.example-clock style time-str]))))
(defn color-input
[]
(let [time-color (subscribe [:time-color])]
(fn color-input-render
[]
[:div.color-input
"Time color: "
[:input {:type "text"
:value @time-color
:on-change #(dispatch
[:time-color (-> % .-target .-value)])}]])))
(defn simple-example
[]
[:div
[greeting "Hello world, it is now"]
[clock]
[color-input]])
;; -- Entry Point -------------------------------------------------------------
(defn ^:export run
[]
(dispatch-sync [:initialize])
(reagent/render [simple-example]
(js/document.getElementById "app")))

BIN
images/Readme/6dominoes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/Readme/Dominoes.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
images/Readme/todolist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
images/event-handlers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

BIN
images/example_app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -7,8 +7,8 @@ module.exports = function (config) {
browsers: ['Chrome'],
files: [
root + '/../test.js', // same as :output-to
{pattern: root + '/../test.js.map', included: false},
{pattern: root + '/**/*.+(cljs|cljc|clj|js|js.map)', included: false}
{pattern: root + '/../test.js.map', included: false, watched: false},
{pattern: root + '/**/*.+(cljs|cljc|clj|js|js.map)', included: false, watched: false}
],
client: {

View File

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Michael Thompson
Copyright (c) 2015-2016 Michael Thompson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1,4 +1,4 @@
(defproject re-frame "0.8.1"
(defproject re-frame "0.9.0-SNAPSHOT"
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
:url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"}
@ -47,7 +47,7 @@
:cljsbuild {:builds [{:id "test"
:source-paths ["test" "src"]
:compiler {:preloads [devtools.preload]
:external-config {:devtools/config {:features-to-install :all}}
:external-config {:devtools/config {:features-to-install [:formatters :hints]}}
:output-to "run/compiled/browser/test.js"
:source-map true
:output-dir "run/compiled/browser/test"
@ -61,8 +61,10 @@
:output-dir "run/compiled/karma/test"
:optimizations :whitespace
:main "re_frame.test_runner"
:pretty-print true}}]}
:pretty-print true
:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true}}}]}
:aliases {"test-once" ["do" "clean," "cljsbuild" "once" "test," "shell" "open" "test/test.html"]
"test-auto" ["do" "clean," "cljsbuild" "auto" "test,"]
"karma-once" ["do" "clean," "cljsbuild" "once" "karma,"]})
"karma-once" ["do" "clean," "cljsbuild" "once" "karma,"]
"karma-auto" ["do" "clean," "cljsbuild" "auto" "karma,"]})

View File

@ -33,7 +33,7 @@
Given an `id`, and an optional value, lookup the registered coeffect
handler (previously registered via `reg-cofx`) and it with two arguments:
the current value of `:coeffect` and, optionally, the value. The registered handler
the current value of `:coeffects` and, optionally, the value. The registered handler
is expected to return a modified coeffect.
"
([id]

View File

@ -12,7 +12,8 @@
[re-frame.interceptor :as interceptor]
[re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor
fx-handler->interceptor
ctx-handler->interceptor]]))
ctx-handler->interceptor]]
[clojure.set :as set]))
;; -- dispatch
@ -45,11 +46,11 @@
;; -- subscriptions
(defn reg-sub-raw
"Associate a given `query id` with a given subscription handler fucntion `handler-fn`
"Associate a given `query id` with a given subscription handler function `handler-fn`
which is expected to take two arguments: app-db and query vector, and return
a `reaction`.
This is a low level, advanced function. You should probably be using re-sub
This is a low level, advanced function. You should probably be using reg-sub
instead."
[query-id handler-fn]
(registrar/register-handler subs/kind query-id handler-fn))
@ -58,6 +59,7 @@
(def subscribe subs/subscribe)
(def clear-sub (partial registrar/clear-handlers subs/kind))
(def clear-subscription-cache! subs/clear-subscription-cache!)
;; -- effects
(def reg-fx fx/register)
@ -133,14 +135,14 @@
(fn []
;; call `dispose!` on all current subscriptions which
;; didn't originally exist.
#_(->> subs/query->reaction
vals
(remove (set (vals subs-cache))) ;;
(map interop/dispose!)
(doall))
(let [original-subs (set (vals subs-cache))
current-subs (set (vals @subs/query->reaction))]
(doseq [sub (set/difference current-subs original-subs)]
(interop/dispose! sub)))
;; reset the atoms
(reset! subs/query->reaction subs-cache)
;; Reset the atoms
;; We don't need to reset subs/query->reaction, as
;; disposing of the subs removes them from the cache anyway
(reset! registrar/kind->id->handler handlers)
(reset! db/app-db app-db)
nil)))

View File

@ -4,7 +4,8 @@
[re-frame.interop :refer [empty-queue debug-enabled?]]
[re-frame.registrar :refer [get-handler register-handler]]
[re-frame.loggers :refer [console]]
[re-frame.interceptor :as interceptor]))
[re-frame.interceptor :as interceptor]
[re-frame.trace :as trace :include-macros true]))
(def kind :event)
@ -56,6 +57,9 @@
(if *handling*
(console :error (str "re-frame: while handling \"" *handling* "\", dispatch-sync was called for \"" event-v "\". You can't call dispatch-sync within an event handler."))
(binding [*handling* event-v]
(interceptor/execute event-v interceptors))))))
(trace/with-trace {:operation event-id
:op-type kind
:tags {:event event-v}}
(interceptor/execute event-v interceptors)))))))

View File

@ -84,9 +84,9 @@
Returns updated `context`. Ie. the `context` which has been threaded
through all interceptor functions.
Generally speaking, an interceptor's `:before` fucntion will (if present)
add to a `context's` `:coeffect`, while it's `:after` function
will modify the `context`'s `:effect`. Very approximately.
Generally speaking, an interceptor's `:before` function will (if present)
add to a `context's` `:coeffects`, while it's `:after` function
will modify the `context`'s `:effects`. Very approximately.
But because all interceptor functions are given `context`, and can
return a modified version of it, the way is clear for an interceptor
@ -109,7 +109,7 @@
"Add a collection of `interceptors` to the end of `context's` execution `:queue`.
Returns the updated `context`.
In an advanced case, this function would allow an interceptor could add new
In an advanced case, this function could allow an interceptor to add new
interceptors to the `:queue` of a context."
[context interceptors]
(update context :queue
@ -179,7 +179,7 @@
The first few interceptors in a chain will likely have `:before`
functions which \"prime\" the `context` by adding the event, and
the current state of app-db into `:coeffects`. But interceptors can
add whatever they want to `:coeffect` - perhaps the event handler needs
add whatever they want to `:coeffects` - perhaps the event handler needs
some information from localstore, or a random number, or access to
a DataScript connection.

View File

@ -71,3 +71,13 @@
there isn't often much point firing a timed event in a test."
[f ms]
(next-tick f))
(defn now []
;; currentTimeMillis may count backwards in some scenarios, but as this is used for tracing
;; it is preferable to the slower but more accurate System.nanoTime.
(System/currentTimeMillis))
(defn reagent-id
"Doesn't make sense in a Clojure context currently."
[reactive-val]
nil)

View File

@ -36,3 +36,21 @@
(defn set-timeout! [f ms]
(js/setTimeout f ms))
(defn now []
(if (exists? js/performance.now)
(js/performance.now)
(js/Date.now)))
(defn reagent-id
"Produces an id for reactive Reagent values
e.g. reactions, ratoms, cursors."
[reactive-val]
(when (implements? reagent.ratom/IReactiveAtom reactive-val)
(str (condp instance? reactive-val
reagent.ratom/RAtom "ra"
reagent.ratom/RCursor "rc"
reagent.ratom/Reaction "rx"
reagent.ratom/Track "tr"
"other")
(hash reactive-val))))

View File

@ -44,3 +44,8 @@
[new-loggers]
(assert (empty? (difference (set (keys new-loggers)) (-> @loggers keys set))) "Unknown keys in new-loggers")
(swap! loggers merge new-loggers))
(defn get-loggers
"Get the current logging functions used by re-frame."
[]
@loggers)

View File

@ -1,7 +1,8 @@
(ns re-frame.router
(:require [re-frame.events :refer [handle]]
[re-frame.interop :refer [after-render empty-queue next-tick]]
[re-frame.loggers :refer [console]]))
[re-frame.loggers :refer [console]]
[re-frame.trace :as trace :include-macros true]))
;; -- Router Loop ------------------------------------------------------------
@ -85,7 +86,7 @@
(-exception [this ex])
(-pause [this later-fn])
(-resume [this])
(-call-post-event-callbacks[this event]))
(-call-post-event-callbacks [this event]))
;; Concrete implementation of IEventQueue
@ -122,41 +123,46 @@
;; Given a "trigger", and the existing FSM state, it computes the
;; new FSM state and the transition action (function).
(let [[new-fsm-state action-fn]
(case [fsm-state trigger]
(trace/with-trace {:op-type ::fsm-trigger}
(let [[new-fsm-state action-fn]
(case [fsm-state trigger]
;; You should read the following "case" as:
;; [current-FSM-state trigger] -> [new-FSM-state action-fn]
;;
;; So, for example, the next line should be interpreted as:
;; if you are in state ":idle" and a trigger ":add-event"
;; happens, then move the FSM to state ":scheduled" and execute
;; that two-part "do" fucntion.
[:idle :add-event] [:scheduled #(do (-add-event this arg)
(-run-next-tick this))]
;; You should read the following "case" as:
;; [current-FSM-state trigger] -> [new-FSM-state action-fn]
;;
;; So, for example, the next line should be interpreted as:
;; if you are in state ":idle" and a trigger ":add-event"
;; happens, then move the FSM to state ":scheduled" and execute
;; that two-part "do" function.
[:idle :add-event] [:scheduled #(do (-add-event this arg)
(-run-next-tick this))]
;; State: :scheduled (the queue is scheduled to run, soon)
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
[:scheduled :run-queue] [:running #(-run-queue this)]
;; State: :scheduled (the queue is scheduled to run, soon)
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
[:scheduled :run-queue] [:running #(-run-queue this)]
;; State: :running (the queue is being processed one event after another)
[:running :add-event ] [:running #(-add-event this arg)]
[:running :pause ] [:paused #(-pause this arg)]
[:running :exception ] [:idle #(-exception this arg)]
[:running :finish-run] (if (empty? queue) ;; FSM guard
[:idle]
[:scheduled #(-run-next-tick this)])
;; State: :running (the queue is being processed one event after another)
[:running :add-event] [:running #(-add-event this arg)]
[:running :pause] [:paused #(-pause this arg)]
[:running :exception] [:idle #(-exception this arg)]
[:running :finish-run] (if (empty? queue) ;; FSM guard
[:idle]
[:scheduled #(-run-next-tick this)])
;; State: :paused (:flush-dom metadata on an event has caused a temporary pause in processing)
[:paused :add-event] [:paused #(-add-event this arg)]
[:paused :resume ] [:running #(-resume this)]
;; State: :paused (:flush-dom metadata on an event has caused a temporary pause in processing)
[:paused :add-event] [:paused #(-add-event this arg)]
[:paused :resume] [:running #(-resume this)]
(throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
{:fsm-state fsm-state, :trigger trigger})))]
(throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
{:fsm-state fsm-state, :trigger trigger})))]
;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
(set! fsm-state new-fsm-state)
(when action-fn (action-fn))))
;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
(trace/merge-trace! {:operation [fsm-state trigger]
:tags {:current-state fsm-state
:new-state new-fsm-state}})
(set! fsm-state new-fsm-state)
(when action-fn (action-fn)))))
(-add-event
[_ event]

View File

@ -6,7 +6,8 @@
[re-frame.registrar :as registrar]
[re-frame.db :refer [app-db]]
[clojure.data :as data]
[re-frame.cofx :as cofx]))
[re-frame.cofx :as cofx]
[re-frame.utils :as utils]))
;; XXX provide a way to set what handler should be called when there is no registered handler.
@ -63,7 +64,14 @@
:id :trim-v
:before (fn trimv-before
[context]
(update-in context [:coeffects :event] subvec 1))))
(-> context
(update-in [:coeffects :event] subvec 1)
(assoc-in [:coeffects ::untrimmed-event] (get-coeffect context :event))))
:after (fn trimv-after
[context]
(-> context
(utils/dissoc-in [:coeffects ::untrimmed-event])
(assoc-in [:coeffects :event] (get-coeffect context ::untrimmed-event))))))
;; -- Interceptor Factories - PART 1 ---------------------------------------------------------------
@ -133,7 +141,7 @@
(defn path
"An interceptor factory which supplies a sub-path of `:db` to the handler.
It's action is somewhat annologous to `update-in`. It grafts the return
It's action is somewhat analogous to `update-in`. It grafts the return
value from the handler back into db.
Usage:
@ -144,7 +152,7 @@
Notes:
1. cater for `path` appearing more than once in an interceptor chain.
2. `:effect` may not contain `:db` effect. Which means no change to
2. `:effects` may not contain `:db` effect. Which means no change to
`:db` should be made.
"
[& args]
@ -208,7 +216,9 @@
:after (fn enrich-after
[context]
(let [event (get-coeffect context :event)
db (get-effect context :db)]
db (or (get-effect context :db)
;; If no db effect is returned, we provide the original coeffect.
(get-coeffect context :db))]
(->> (f db event)
(assoc-effect context :db))))))
@ -218,8 +228,9 @@
"Interceptor factory which runs a given function `f` in the \"after\"
position, presumably for side effects.
`f` is called with two arguments: the `effects` value of `:db` and the event. It's return
value is ignored so `f` can only side-effect.
`f` is called with two arguments: the `effects` value of `:db`
(or the `coeffect` value of db if no db effect is returned) and the event.
Its return value is ignored so `f` can only side-effect.
Example use:
- `f` runs schema validation (reporting any errors found)
@ -229,7 +240,9 @@
:id :after
:after (fn after-after
[context]
(let [db (get-effect context :db)
(let [db (or (get-effect context :db)
;; If no db effect is returned, we provide the original coeffect.
(get-coeffect context :db))
event (get-coeffect context :event)]
(f db event) ;; call f for side effects
context)))) ;; context is unchanged
@ -274,5 +287,3 @@
(assoc-in new-db out-path)
(assoc-effect context :db))
context)))))

View File

@ -1,11 +1,11 @@
(ns re-frame.subs
(:require
[re-frame.db :refer [app-db]]
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref?]]
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref? dispose! reagent-id]]
[re-frame.loggers :refer [console]]
[re-frame.utils :refer [first-in-vector]]
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]))
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
[re-frame.trace :as trace :include-macros true]))
(def kind :sub)
(assert (re-frame.registrar/kinds kind))
@ -17,11 +17,29 @@
;; Two subscriptions are "equal" if their query vectors test "=".
(def query->reaction (atom {}))
(defn clear-subscription-cache!
"Runs on-dispose for all subscriptions we have in the subscription cache.
Used to force recreation of new subscriptions. Should only be necessary
in development.
The on-dispose functions for the subscriptions will remove themselves from the
cache.
Useful when reloading Figwheel code after a React exception, as React components
aren't cleaned up properly. This means a subscription's on-dispose function isn't
run when the components are destroyed. If a bad subscription caused your exception,
then you can't fix it without reloading your browser."
[]
(doseq [[k rxn] @query->reaction]
(dispose! rxn))
(if (not-empty @query->reaction)
(console :warn "Subscription cache should be empty after clearing it.")))
(defn clear-all-handlers!
"Unregisters all existing subscription handlers"
[]
(clear-handlers kind)
(reset! query->reaction {}))
(clear-subscription-cache!))
(defn cache-and-return
"cache the reaction r"
@ -29,9 +47,14 @@
(let [cache-key [query-v dynv]]
;; when this reaction is no longer being used, remove it from the cache
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
#_(console :log "Removing subscription:" cache-key)))
(trace/with-trace {:operation (first-in-vector query-v)
:op-type :sub/dispose
:tags {:query-v query-v
:reaction (reagent-id r)}}
nil)))
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
(swap! query->reaction assoc cache-key r)
(trace/merge-trace! {:tags {:reaction (reagent-id r)}})
r)) ;; return the actual reaction
(defn cache-lookup
@ -46,33 +69,48 @@
(defn subscribe
"Returns a Reagent/reaction which contains a computation"
([query-v]
(trace/with-trace {:operation (first-in-vector query-v)
:op-type :sub/create
:tags {:query-v query-v}}
(if-let [cached (cache-lookup query-v)]
(do ;(console :log "Using cached subscription: " query-v)
(do
(trace/merge-trace! {:tags {:cached? true
:reaction (reagent-id cached)}})
cached)
(let [query-id (first-in-vector query-v)
handler-fn (get-handler kind query-id)]
;(console :log "Subscription created: " query-v)
(if-not handler-fn
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
(cache-and-return query-v [] (handler-fn app-db query-v)))))
(trace/merge-trace! {:tags {:cached? false}})
(if (nil? handler-fn)
(do (trace/merge-trace! {:error true})
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
(cache-and-return query-v [] (handler-fn app-db query-v)))))))
([v dynv]
(trace/with-trace {:operation (first-in-vector v)
:op-type :sub/create
:tags {:query-v v
:dyn-v dynv}}
(if-let [cached (cache-lookup v dynv)]
(do ;(console :log "Using cached subscription: " v " and " dynv)
(do
(trace/merge-trace! {:tags {:cached? true
:reaction (reagent-id cached)}})
cached)
(let [query-id (first-in-vector v)
handler-fn (get-handler kind query-id)]
(trace/merge-trace! {:tags {:cached? false}})
(when debug-enabled?
(when-let [not-reactive (not-empty (remove ratom? dynv))]
(console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom:" not-reactive)))
(if (nil? handler-fn)
(when-not handler-fn
(trace/merge-trace! {:error true})
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription."))
(let [dyn-vals (make-reaction (fn [] (mapv deref dynv)))
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value.
;(console :log "Subscription created: " v dynv)
(cache-and-return v dynv (make-reaction (fn [] @@sub)))))))))
(cache-and-return v dynv (make-reaction (fn [] @@sub))))))))))
;; -- reg-sub -----------------------------------------------------------------
@ -87,11 +125,13 @@
(defn- deref-input-signals
[signals query-id]
(cond
(let [signals (cond
(sequential? signals) (map deref signals)
(map? signals) (map-vals deref signals)
(deref? signals) @signals
:else (console :error "re-frame: in the reg-sub for " query-id ", the input-signals function returns: " signals)))
(map? signals) (map-vals deref signals)
(deref? signals) @signals
:else (console :error "re-frame: in the reg-sub for " query-id ", the input-signals function returns: " signals))]
(trace/merge-trace! {:tags {:input-signals (map reagent-id signals)}})
signals))
(defn reg-sub
@ -143,29 +183,43 @@
f)
;; one sugar pair
2 (let [ret-val (subscribe (second input-args))]
(fn inp-fn
([_] ret-val)
([_ _] ret-val)))
2 (fn inp-fn
([_] (subscribe (second input-args)))
([_ _] (subscribe (second input-args))))
;; multiple sugar pairs
(let [pairs (partition 2 input-args)
vecs (map last pairs)
ret-val (map subscribe vecs)]
vecs (map last pairs)]
(when-not (every? vector? vecs)
(console :error err-header "expected pairs of :<- and vectors, got:" pairs))
(fn inp-fn
([_] ret-val)
([_ _] ret-val))))]
([_] (map subscribe vecs))
([_ _] (map subscribe vecs)))))]
(register-handler
kind
query-id
(fn subs-handler-fn
([db query-vec]
(let [subscriptions (inputs-fn query-vec)]
(make-reaction
(fn [] (computation-fn (deref-input-signals subscriptions query-id) query-vec)))))
(let [subscriptions (inputs-fn query-vec)
reaction-id (atom nil)
reaction (make-reaction
(fn [] (trace/with-trace {:operation (first-in-vector query-vec)
:op-type :sub/run
:tags {:query-v query-vec
:reaction @reaction-id}}
(computation-fn (deref-input-signals subscriptions query-id) query-vec))))]
(reset! reaction-id (reagent-id reaction))
reaction))
([db query-vec dyn-vec]
(let [subscriptions (inputs-fn query-vec dyn-vec)]
(make-reaction
(fn [] (computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec)))))))))
(let [subscriptions (inputs-fn query-vec dyn-vec)
reaction-id (atom nil)
reaction (make-reaction
(fn []
(trace/with-trace {:operation (first-in-vector query-vec)
:op-type :sub/run
:tags {:query-v query-vec
:dyn-v dyn-vec
:reaction @reaction-id}}
(computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec))))]
(reset! reaction-id (reagent-id reaction))
reaction))))))

76
src/re_frame/trace.cljc Normal file
View File

@ -0,0 +1,76 @@
(ns re-frame.trace
"Tracing for re-frame.
Alpha quality, subject to change/break at any time."
(:require [re-frame.interop :as interop]
[re-frame.loggers :refer [console]]))
(def id (atom 0))
(def ^:dynamic *current-trace* nil)
(defn reset-tracing! []
(reset! id 0))
#?(:cljs (goog-define trace-enabled? false)
:clj (def ^boolean trace-enabled? false))
(defn ^boolean is-trace-enabled?
"See https://groups.google.com/d/msg/clojurescript/jk43kmYiMhA/IHglVr_TPdgJ for more details"
[]
trace-enabled?)
(def trace-cbs (atom {}))
(defn register-trace-cb
"Registers a tracing callback function which will receive a collection of one or more traces.
Will replace an existing callback function if it shares the same key."
[key f]
(swap! trace-cbs assoc key f))
(defn remove-trace-cb [key]
(swap! trace-cbs dissoc key)
nil)
(defn next-id [] (swap! id inc))
(defn start-trace [{:keys [operation op-type tags child-of]}]
{:id (next-id)
:operation operation
:op-type op-type
:tags tags
:child-of (or child-of (:id *current-trace*))
:start (interop/now)})
#?(:clj (defmacro finish-trace [trace]
`(when (is-trace-enabled?)
(let [end# (interop/now)
duration# (- end# (:start ~trace))]
(doseq [[k# cb#] @trace-cbs]
(try (cb# [(assoc ~trace
:duration duration#
:end (interop/now))])
#?(:clj (catch Exception e#
(console :error "Error thrown from trace cb" k# "while storing" ~trace e#)))
#?(:cljs (catch :default e#
(console :error "Error thrown from trace cb" k# "while storing" ~trace e#)))))))))
#?(:clj (defmacro with-trace
"Create a trace inside the scope of the with-trace macro
Common keys for trace-opts
:op-type - what kind of operation is this? e.g. :sub/create, :render.
:operation - identifier for the operation, for an subscription it would be the subscription keyword
tags - a map of arbitrary kv pairs"
[{:keys [operation op-type tags child-of] :as trace-opts} & body]
`(if (is-trace-enabled?)
(binding [*current-trace* (start-trace ~trace-opts)]
(try ~@body
(finally (finish-trace *current-trace*))))
(do ~@body))))
#?(:clj (defmacro merge-trace! [m]
;; Overwrite keys in tags, and all top level keys.
`(when (is-trace-enabled?)
(let [new-trace# (-> (update *current-trace* :tags merge (:tags ~m))
(merge (dissoc ~m :tags)))]
(set! *current-trace* new-trace#))
nil)))

View File

@ -2,10 +2,23 @@
(:require
[re-frame.loggers :refer [console]]))
(defn dissoc-in
"Dissociates an entry from a nested associative structure returning a new
nested structure. keys is a sequence of keys. Any empty maps that result
will not be present in the new structure.
The key thing is that 'm' remains identical? to istelf if the path was never present"
[m [k & ks :as keys]]
(if ks
(if-let [nextmap (get m k)]
(let [newmap (dissoc-in nextmap ks)]
(if (seq newmap)
(assoc m k newmap)
(dissoc m k)))
m)
(dissoc m k)))
(defn first-in-vector
[v]
(if (vector? v)
(first v)
(console :error "re-frame: expected a vector, but got:" v)))

View File

@ -3,7 +3,9 @@
[cljs.test :refer-macros [is deftest async use-fixtures]]
[re-frame.core :as re-frame]
[re-frame.fx]
[re-frame.interop :refer [set-timeout!]]))
[re-frame.interop :refer [set-timeout!]]
[re-frame.loggers :as log]
[clojure.string :as str]))
;; ---- FIXTURES ---------------------------------------------------------------
@ -43,3 +45,20 @@
1000)
;; kick off main handler
(re-frame/dispatch [::later-test]))))
(re-frame/reg-event-fx
::missing-handler-test
(fn [_world _event-v]
{:fx-not-exist [:nothing :here]}))
(deftest report-missing-handler
(let [logs (atom [])
log-fn (fn [& args] (swap! logs conj (str/join args)))
original-loggers (log/get-loggers)]
(try
(log/set-loggers! {:error log-fn})
(re-frame/dispatch-sync [::missing-handler-test])
(is (re-matches #"re-frame: no :fx handler registered for::fx-not-exist" (first @logs)))
(is (= (count @logs) 1))
(finally
(log/set-loggers! original-loggers)))))

View File

@ -1,17 +1,22 @@
(ns re-frame.interceptor-test
(:require [cljs.test :refer-macros [is deftest]]
[reagent.ratom :refer [atom]]
(:require [cljs.test :refer-macros [is deftest testing]]
[reagent.ratom :refer [atom]]
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect]]
[re-frame.std-interceptors :refer [trim-v path on-changes
db-handler->interceptor fx-handler->interceptor]]))
[re-frame.std-interceptors :refer [debug trim-v path enrich after on-changes
db-handler->interceptor fx-handler->interceptor]]
[re-frame.interceptor :as interceptor]))
(enable-console-print!)
(deftest test-trim-v
(let [c (-> (context [:a :b :c] [])
((:before trim-v)))]
(is (= (get-coeffect c :event)
[:b :c]))))
(let [ctx (context [:event-id :b :c] [])
ctx-trimmed ((:before trim-v) ctx)
ctx-untrimmed ((:after trim-v) ctx-trimmed)]
(is (= (get-coeffect ctx-trimmed :event)
[:b :c]))
(is (= (get-coeffect ctx-untrimmed :event)
[:event-id :b :c]))
(is (= ctx-untrimmed ctx))))
(deftest test-one-level-path
@ -47,13 +52,22 @@
((:after p))
(get-effect :db))))
;; test #2 - set dbto nil
;; test #2 - set db to nil
(is (= {:1 {:2 nil}}
(-> b4
(assoc-effect :db nil) ;; <-- db becomes nil
((:after p))
(get-effect :db)))))))
(deftest path-with-no-db-returned
(let [path-interceptor (path :a)]
(-> (context [] [path-interceptor] {:a 1})
(interceptor/invoke-interceptors :before)
interceptor/change-direction
(interceptor/invoke-interceptors :after)
(get-effect :db)
(nil?) ;; We don't expect an effect to be added.
(is))))
(deftest test-db-handler-interceptor
(let [event [:a :b]
@ -115,5 +129,19 @@
((:after change-i))
(get-effect :db))))))
(deftest test-after
(testing "when no db effect is returned"
(let [after-db-val (atom nil)]
(-> (context [:a :b]
[(after (fn [db] (reset! after-db-val db)))]
{:a 1})
(interceptor/invoke-interceptors :before)
interceptor/change-direction
(interceptor/invoke-interceptors :after))
(is (= @after-db-val {:a 1})))))
(deftest test-enrich
(testing "when no db effect is returned"
(let [ctx (context [] [] {:a 1})]
(is (= ::not-found (get-effect ctx :db ::not-found)))
(-> ctx (:after (enrich (fn [db] (is (= db {:a 1})))))))))

View File

@ -0,0 +1,74 @@
(ns re-frame.restore-test
(:require [cljs.test :refer-macros [is deftest async use-fixtures testing]]
[re-frame.core :refer [make-restore-fn reg-sub subscribe]]
[re-frame.subs :as subs]))
;; TODO: future tests in this area could check DB state and registrations are being correctly restored.
(use-fixtures :each {:before subs/clear-all-handlers!})
(defn one? [x] (= 1 x))
(defn two? [x] (= 2 x))
(defn register-test-subs []
(reg-sub
:test-sub
(fn [db ev]
(:test-sub db)))
(reg-sub
:test-sub2
(fn [db ev]
(:test-sub2 db))))
(deftest make-restore-fn-test
(testing "no existing subs, then making one subscription"
(register-test-subs)
(let [original-subs @subs/query->reaction
restore-fn (make-restore-fn)]
(is (zero? (count original-subs)))
@(subscribe [:test-sub])
(is (one? (count @subs/query->reaction)))
(is (contains? @subs/query->reaction [[:test-sub] []]))
(restore-fn)
(is (zero? (count @subs/query->reaction))))))
(deftest make-restore-fn-test2
(testing "existing subs, making more subscriptions"
(register-test-subs)
@(subscribe [:test-sub])
(let [original-subs @subs/query->reaction
restore-fn (make-restore-fn)]
(is (one? (count original-subs)))
@(subscribe [:test-sub2])
(is (contains? @subs/query->reaction [[:test-sub2] []]))
(is (two? (count @subs/query->reaction)))
(restore-fn)
(is (not (contains? @subs/query->reaction [[:test-sub2] []])))
(is (one? (count @subs/query->reaction))))))
(deftest make-restore-fn-test3
(testing "existing subs, making more subscriptions with different params on same subscriptions"
(register-test-subs)
@(subscribe [:test-sub])
(let [original-subs @subs/query->reaction
restore-fn (make-restore-fn)]
(is (one? (count original-subs)))
@(subscribe [:test-sub :extra :params])
(is (two? (count @subs/query->reaction)))
(restore-fn)
(is (one? (count @subs/query->reaction))))))
(deftest nested-restores
(testing "running nested restores"
(register-test-subs)
(let [restore-fn-1 (make-restore-fn)
_ @(subscribe [:test-sub])
_ (is (one? (count @subs/query->reaction)))
restore-fn-2 (make-restore-fn)]
@(subscribe [:test-sub2])
(is (two? (count @subs/query->reaction)))
(restore-fn-2)
(is (one? (count @subs/query->reaction)))
(restore-fn-1)
(is (zero? (count @subs/query->reaction))))))

View File

@ -1,15 +1,15 @@
(ns re-frame.subs-test
(:require [cljs.test :refer-macros [is deftest]]
(:require [cljs.test :as test :refer-macros [is deftest]]
[reagent.ratom :refer-macros [reaction]]
[re-frame.subs :as subs]
[re-frame.db :as db]
[re-frame.core :as re-frame]))
(test/use-fixtures :each {:before (fn [] (subs/clear-all-handlers!))})
;=====test basic subscriptions ======
(deftest test-reg-sub
(subs/clear-all-handlers!)
(re-frame/reg-sub-raw
:test-sub
(fn [db [_]] (reaction (deref db))))
@ -20,8 +20,6 @@
(is (= 1 @test-sub))))
(deftest test-chained-subs
(subs/clear-all-handlers!)
(re-frame/reg-sub-raw
:a-sub
(fn [db [_]] (reaction (:a @db))))
@ -44,8 +42,6 @@
(is (= {:a 1 :b 3} @test-sub))))
(deftest test-sub-parameters
(subs/clear-all-handlers!)
(re-frame/reg-sub-raw
:test-sub
(fn [db [_ b]] (reaction [(:a @db) b])))
@ -56,8 +52,6 @@
(deftest test-sub-chained-parameters
(subs/clear-all-handlers!)
(re-frame/reg-sub-raw
:a-sub
(fn [db [_ a]] (reaction [(:a @db) a])))
@ -77,12 +71,13 @@
(reset! db/app-db {:a 1 :b 2})
(is (= {:a [1 :c], :b [2 :c]} @test-sub))))
(deftest test-nonexistent-sub
(is (nil? (re-frame/subscribe [:non-existence]))))
;============== test cached-subs ================
(def side-effect-atom (atom 0))
(deftest test-cached-subscriptions
(subs/clear-all-handlers!)
(reset! side-effect-atom 0)
(re-frame/reg-sub-raw
@ -106,8 +101,6 @@
;============== test register-pure macros ================
(deftest test-reg-sub-macro
(subs/clear-all-handlers!)
(subs/reg-sub
:test-sub
(fn [db [_]] db))
@ -118,8 +111,6 @@
(is (= 1 @test-sub))))
(deftest test-reg-sub-macro-singleton
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
@ -138,8 +129,6 @@
(is (= {:a 1} @test-sub))))
(deftest test-reg-sub-macro-vector
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
@ -163,8 +152,6 @@
(is (= {:a 1 :b 3} @test-sub))))
(deftest test-reg-sub-macro-map
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
@ -188,8 +175,6 @@
(is (= {:a 1 :b 3} @test-sub))))
(deftest test-sub-macro-parameters
(subs/clear-all-handlers!)
(subs/reg-sub
:test-sub
(fn [db [_ b]] [(:a db) b]))
@ -199,8 +184,6 @@
(is (= [1 :c] @test-sub))))
(deftest test-sub-macros-chained-parameters
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_ a]] [(:a db) a]))
@ -222,8 +205,6 @@
(deftest test-sub-macros-<-
"test the syntactial sugar"
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
@ -239,8 +220,6 @@
(deftest test-sub-macros-chained-parameters-<-
"test the syntactial sugar"
(subs/clear-all-handlers!)
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
@ -258,3 +237,34 @@
(let [test-sub (subs/subscribe [:a-b-sub :c])]
(reset! db/app-db {:a 1 :b 2})
(is (= {:a 1 :b 2} @test-sub) )))
(deftest test-registering-subs-doesnt-create-subscription
(let [sub-called? (atom false)]
(with-redefs [subs/subscribe (fn [& args] (reset! sub-called? true))]
(subs/reg-sub
:a-sub
(fn [db [_]] (:a db)))
(subs/reg-sub
:b-sub
(fn [db [_]] (:b db)))
(subs/reg-sub
:fn-sub
(fn [[_ c] _]
[(subs/subscribe [:a-sub c])
(subs/subscribe [:b-sub c])])
(fn [db [_]] (:b db)))
(subs/reg-sub
:a-sugar-sub
:<- [:a-sub]
(fn [[a] [_ c]] {:a a}))
(subs/reg-sub
:a-b-sub
:<- [:a-sub]
:<- [:b-sub]
(fn [[a b] [_ c]] {:a a :b b})))
(is (false? @sub-called?))))

View File

@ -6,7 +6,9 @@
;; Test Namespaces -------------------------------
[re-frame.interceptor-test]
[re-frame.subs-test]
[re-frame.fx-test]))
[re-frame.fx-test]
[re-frame.trace-test]
[re-frame.restore-test]))
(enable-console-print!)
@ -19,7 +21,9 @@
(cljs-test/run-tests
're-frame.interceptor-test
're-frame.subs-test
're-frame.fx-test))
're-frame.fx-test
're-frame.trace-test
're-frame.restore-test))
;; ---- KARMA -----------------------------------------------------------------
@ -28,4 +32,6 @@
karma
're-frame.interceptor-test
're-frame.subs-test
're-frame.fx-test))
're-frame.fx-test
're-frame.trace-test
're-frame.restore-test))

View File

@ -0,0 +1,34 @@
(ns re-frame.trace-test
(:require [cljs.test :as test :refer-macros [is deftest]]
[re-frame.trace :as trace :include-macros true]
[re-frame.core :as rf]))
(def test-traces (atom []))
(test/use-fixtures :once {:before (fn []
(trace/register-trace-cb :test
(fn [traces]
(doseq [trace traces]
(swap! test-traces conj trace)))))
:after (fn []
(trace/remove-trace-cb :test))})
(test/use-fixtures :each {:before (fn []
(reset! test-traces [])
(trace/reset-tracing!))})
; Disabled, as goog-define doesn't work in optimizations :whitespace
;(deftest trace-cb-test
; (trace/with-trace {:operation :test1
; :op-type :test})
; (is (= 1 (count @test-traces)))
; (is (= (select-keys (first @test-traces) [:id :operation :op-type :tags])
; {:id 1 :operation :test1 :op-type :test :tags nil})))
;
;(enable-console-print!)
;
;(deftest sub-trace-test
; (rf/subscribe [:non-existence])
; (is (= 1 (count @test-traces)))
; (is (= (select-keys (first @test-traces) [:id :operation :op-type :error])
; {:id 1 :op-type :sub/create :operation :non-existence :error true})))