Final cut of FAQ

This commit is contained in:
Mike Thompson 2017-09-01 15:22:45 +10:00
parent 0274c784b6
commit f4b9fa0d65

View File

@ -1,50 +1,52 @@
### Question ### Question
When the user switches to a certain panel, I'd like to kickoff a regular poll of my When the user switches to a certain panel, I'd like to start regularly polling my
backend (database) - say every 60 seconds. And, then later, when the user switches backend (database) - say every 60 seconds. And, then later, when the user switches
away from that panel, I want to stop that polling. away from that panel, I want to stop that polling.
### First, An Architectural Note ### First, An Architectural Note
The broader React community often uses the "load data from server on Component mount" The broader React community sometimes likes to collocate queries with view components
model of architecture. They'll collocate queries with view components and perform a and initiate those queries (via a GET?) within the View's `componentDidMount`.
GET (to the server) within the View's `componentDidMount`. And then, perhaps cleanup/stop any database polling in the Component's `componentWillUnmount`.
This approach is not idiomatic for re-frame. Views are not imperative in nature This approach is not idiomatic for re-frame. Views are not imperative
(they don't issue database queries), they simply render current application state. (they don't issue database queries), they simply render current application state.
This and more This and more
[is discussed in PurelyFunctional.tv's writeup](https://purelyfunctional.tv/article/react-vs-re-frame/) [is discussed in PurelyFunctional.tv's writeup](https://purelyfunctional.tv/article/react-vs-re-frame/)
re-frame is CQRS in nature - "imperative stuff" only ever happens With re-frame, "imperative stuff" only ever happens
because of an `event`. When the user clicks on a panel-changing widget (a button or a tab?), because of an `event`. When the user clicks on a panel-changing widget (a button or a tab?),
an event is dispatched, and it is the event handler for this event which knows an event is dispatched, and it is the event handler for this event which knows
that action X, Y and Z needs to happen. X might be change application state so that actions X, Y and Z needs to happen. X might be change application state so
that the view displayed changes, Y might be change application state so that a that the view displayed changes, and Y might be change application state so that a
"twirly thing" is shown over the top, and Z might be to issue a database query. "twirly thing" is shown over the top, and Z might be to issue a database query.
So, having got that out the way ... So, having got that issue out the way ...
### Answer ### Answer
We'll create an effect. It will be general in nature. We'll create an effect. It will be general in nature.
It will start and stop the timed/scheduled dispatch of an event (every 60 seconds). It will start and stop the timed/scheduled dispatch of an event.
For this FAQ, For this FAQ,
this event will poll the backend but this is a general pattern we want an event dispatched every 60 seconds and each event will
and the regularly dispatched event could do anything we wanted. trigger a backend poll, but the effect we are about to create
will be useful well beyond this narrow case.
We'll be creating an `effect` called, say, `:interval`. We must We'll be creating an `effect` called, say, `:interval`. So, event handlers
design the data format (micro DSL) returned by an will be returning:
event handler for this effect. This data format must allow an event handler to ```
start and stop a regular dispatch. {:interval something}
```
So now we design the "something" bit. It will be a data format (DSL) which
allows an event handler to start and stop a regular event dispatch.
To `:start` a regular dispatch, an event handler would return To `:start` a regular dispatch, an event handler would return
data in this format: data in this format:
```clj ```clj
{:interval {:action :start {:interval {:action :start
:id :panel-1-query ;; my id for this (so I cancel later) :id :panel-1-query ;; my id for this (so I can cancel later)
:frequency 60000 ;; how many ms between dispatches :frequency 60000 ;; how many ms between dispatches
:event [:panel-query 1]}} ;; what to dispatch :event [:panel-query 1]}} ;; what to dispatch
``` ```
@ -55,7 +57,8 @@ And to later cancel the regular dispatch, an event handler would return this:
:id :panel-1-query}} ;; the id provided to :start :id :panel-1-query}} ;; the id provided to :start
``` ```
Now, register an `effect handler` for the `:interval` effect: So that's the design work done. Let's implement it by registering an
`effect handler` for the `:interval` effect:
```clj ```clj
(re-frame.core/reg-fx ;; the re-frame API for registering effect handlers (re-frame.core/reg-fx ;; the re-frame API for registering effect handlers
:interval ;; the effect id :interval ;; the effect id
@ -69,6 +72,46 @@ Now, register an `effect handler` for the `:interval` effect:
You'd probably want a bit more error checking, but that's the (untested) sketch. You'd probably want a bit more error checking, but that's the (untested) sketch.
### Figwheel ### A Side Note About Effect Handlers and Figwheel
[Figwheel](https://github.com/bhauman/lein-figwheel) provides for hot reloading of code, which
is terrific.
But, during development, as Figwheel is reloading code, effectful handlers, like the
one above, can be get into a messed up state - existing timers will be lost (and
become never stoppable).
Stateful things are grubby in the face of reloading, and all we can do is
try to manage for it as best we can, on a case by case basis.
You could try putting all your grubby effect handlers into their own
separate namespace `effects.cljs` - one that isn't edited often, so Figwheel isn't
regularly reloading it.
OR, you can code defensively for reloading, perhaps like this:
```clj
(defonce interval-handler ;; notice use of defonce
(let [live-intervals (atom {})] ;; storage for live interevals
(fn handler [{:keys [action id frequency event]}] ;; the effect handler
(condp = action
:clean (doseq ;; clean up all existing
#(handler {:action :end :id %1})
(keys @live-intervals))
:start (swap! live-intervals assoc id (js/setInterval #(dispatch event) frequency)))
:end (do (js/clearInterval (get live-intervals id))
(swap! live-intervals dissoc id))))
;; when this code is reloaded `:clean` existing intervals
(interval-handler {:action :clean})
;; now register
(re-frame.core/reg-fx ;; the re-frame API for registering effect handlers
:interval ;; the effect id
interval-handler)
```
Every effect handler is grubby in its own special way. So you'll have to
come up with strategies to handle Figwheel reloads on a case by case basis.
Figwheel is a really