re-frame: how to create dependent handlers and run them without delay

1,649 views
Skip to first unread message

Karsten Schmidt

unread,
Apr 3, 2015, 5:50:45 PM4/3/15
to clojur...@googlegroups.com
I've been really enjoying my last month working on a largish app based
on re-frame and not run into any probs. However, today I was going to
refactor another project using animation and came across an issue
which I'm somewhat stumped by:

I created a global tick handler, which is supposed to be an event
generator for various other handlers to update their data in the app
db. Currently this :next-tick handler re-triggers itself like shown
below and runs as predicted (~60Hz via requestAnimationFrame), but for
the purpose of this question this could be triggered at any (lower)
frequency:

(defn re-trigger-timer [] (reagent/next-tick (fn [] (dispatch [:next-tick]))))

(register-handler
:next-tick
(fn [db _]
(re-trigger-timer)
(update db :tick inc)))

Now I'd like to have other handlers which (dynamically) subscribe to
these tick changes, but they should be run without causing another
16ms delay caused from using the subscribe/dispatch mechanism - and I
keep drawing a blank how to achieve this w/o modifying
re-frame.db/app-db directly (outside an handler).

Of course I could create a subscription for the :tick value, run the
related code and return the computed data as derived view during
re-drawing of the related components, but that kind of defeats the
whole purpose and I'd really like these updates to happen in handlers.
I'm sure there's a better way... no?! :)

Could someone please clarify for me?

FWIW the demo app in question is here:
http://demo.thi.ng/geom/physics/strands.html

For starters I'd like to update physics sim via an handler reacting to
:tick changes, but there'll be others too...

Thanks, K.

Daniel Kersten

unread,
Apr 3, 2015, 6:07:56 PM4/3/15
to clojur...@googlegroups.com

--
Note that posts from new members are moderated - please be patient with your first post.
---
You received this message because you are subscribed to the Google Groups "ClojureScript" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojurescript+unsubscribe@googlegroups.com.
To post to this group, send email to clojur...@googlegroups.com.
Visit this group at http://groups.google.com/group/clojurescript.

Karsten Schmidt

unread,
Apr 3, 2015, 6:51:07 PM4/3/15
to clojur...@googlegroups.com
Thanks, Daniel. Didn't know about `dispatch-sync` and this would
definitely help to avoid the delay, but the fundamental question to me
still is how to create reactions to handler changes *outside* reagent
components. I'm not sure if re-frame or reagent actually supports this
at all.

AFAIK `(subscribe [:tick])` only makes sense inside a component fn.
Given the following subscription handler:

(register-sub :tick (fn [db _] (reaction (:tick @db))))

I would like do to something like to do this outside a component:

- subscribe to :tick
- update dispatch-sync to dependent handler(s)
- setup more subscriptions (:a, :b, :c ...)
- have a set of components subscribing to :a, :b, :c etc.

So every time :tick updates, everything will be re-run/re-drawn. The
reasons why these dependent updates should be in handlers is that some
of them will require results from earlier (intermediate) handler
updates.

Maybe I'm barking up the wrong tree here and should just solve this
via core.async mult/tap or its pub/sub?
>> email to clojurescrip...@googlegroups.com.
>> To post to this group, send email to clojur...@googlegroups.com.
>> Visit this group at http://groups.google.com/group/clojurescript.
>
> --
> Note that posts from new members are moderated - please be patient with your
> first post.
> ---
> You received this message because you are subscribed to the Google Groups
> "ClojureScript" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to clojurescrip...@googlegroups.com.
> To post to this group, send email to clojur...@googlegroups.com.
> Visit this group at http://groups.google.com/group/clojurescript.



--
Karsten Schmidt
http://postspectacular.com | http://toxiclibs.org | http://toxi.co.uk

Karsten Schmidt

unread,
Apr 3, 2015, 11:06:00 PM4/3/15
to clojur...@googlegroups.com
After hours of fiddling around with this and studying the reagent
source I came up with this solution using watches:

(defn register-reactive
"Registers an handler which will be automatically triggered when
value at given path in app-db has changed. The new handler will be
provided with the new path value and also have the pure & trim-v
middlewares injected."
[id path f]
(add-watch
app-db id
(fn [_ _ o n]
(let [o (get-in o path)
n (get-in n path)]
(when-not (= o n) (dispatch-sync [id n])))))
(register-handler id trim-v f))

Use like:

(register-reactive
:foo [:tick]
(fn [db tick]
;; (info :foo-tick tick)
;; do something
(assoc db :foo ...)))

Mike Thompson

unread,
Apr 3, 2015, 11:53:11 PM4/3/15
to clojur...@googlegroups.com, in...@toxi.co.uk
Hi Karsten,

First, its really nice to hear that re-frame has worked well for you so far!!

I've read your problem description and summarized it this way:
1. you need to run N different tick event handlers every 16ms.
2. the set of "tick" event handlers changes over time.
Some get added and removed.

To state the problem anther way: you want to register N event handlers for the one event, but re-frame doesn't allow for that. It only allows one event handler to to run for an event.

Also, performance is an issue here. You want those tick handlers all run as soon as possible. No 16ms delay.


If that is a good description of the problem, then we need to look for solution by asking ... how do I get N event handlers run for the one event?

There's a couple of ways of doing that but they'd all involve you "adding your own layer to re-frame". Here's one ...


Sketch
------

In app-db, store the list of event handler ids which should be run on each tick.


(register-handler
:add-tick-handler-id ;; usage: (dispatch [:add-tick-handler-id :some-other-id])
(fn [db [_ tick-handler-id]
(update-in db [:tick-handlers] conj tick-handler-id)))


(register-handler
:remove-tick-handler
(fn [db [_ tick-handler-id]
...))

;; this happens every tick
(register-handler
:tick
(fn [db v]
(let [hs (map re-frame.handlers/lookup-handler (:tick-handlers db))]
(reduce #(%2 %1 v) db hs)))


--
Mike

Mike Thompson

unread,
Apr 4, 2015, 12:10:00 AM4/4/15
to clojur...@googlegroups.com
On Saturday, April 4, 2015 at 9:07:56 AM UTC+11, Daniel Kersten wrote:
> Have you tried using dispatch-sync? https://github.com/Day8/re-frame/wiki/Bootstrap-An-Application#a-cheat and https://github.com/Day8/re-frame/blob/master/src/re_frame/router.cljs#L54


"dispatch-sync" should never be called from within an event handler (only an async "dispatch" can be called).

Why not? Well the the first event handler is given a snapshot of the db as a parameter. And whatever this first event handler returns will be put back into app-db when it finishes. So any changes made by an intermediate call to dispatch-sync will be lost.


So the problem sequence would be:
1. first event handler called with db snapshot
2. sync-dispatch called, making changes to app-db
3. first event handler finishes, and the value it returns is written into app-db

The changes in step 2 are lost.

As a general rule, don't use dispatch-sync. (Although I cheat and use it to initialize).

--
Mike

Karsten Schmidt

unread,
Apr 4, 2015, 12:33:29 AM4/4/15
to clojur...@googlegroups.com
Thanks, Mike! Trees and forests... So obvious, I didn't/couldn't think of it! :)

Btw. In my above example, dispatch-sync works fine (at least for the
physics part), since that is using internally mutable types... but of
course, the approach would fail for immutable CLJS data structures.
> --
> Note that posts from new members are moderated - please be patient with your first post.
> ---
> You received this message because you are subscribed to the Google Groups "ClojureScript" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to clojurescrip...@googlegroups.com.
> To post to this group, send email to clojur...@googlegroups.com.
> Visit this group at http://groups.google.com/group/clojurescript.



Mike Thompson

unread,
Apr 4, 2015, 3:07:32 AM4/4/15
to clojur...@googlegroups.com, in...@toxi.co.uk
I've written this approach up as tersely as possible and I've put it into the Wiki:

https://github.com/Day8/re-frame/wiki/Alternative-dispatch,-routing-&-handling#multiple-handlers-for-the-one-event

Feel free to edit that page if you think the explanation is lacking.

--
Mike

Karsten Schmidt

unread,
Apr 4, 2015, 12:20:39 PM4/4/15
to Mike Thompson, clojur...@googlegroups.com
Thanks, Mike. I implemented this and had to make some changes to make
it work. I.e. since `register-handler` auto-injects the `pure`
middleware, any of these :next-tick child handlers need to be
registered via `register-base`...

I updated the wiki, hope you don't mind... :)

https://github.com/Day8/re-frame/wiki/Alternative-dispatch,-routing-&-handling#multiple-handlers-for-the-one-event

Cheers!

Mike Thompson

unread,
Apr 4, 2015, 3:51:01 PM4/4/15
to clojur...@googlegroups.com, m.l.tho...@gmail.com, in...@toxi.co.uk
On Sunday, April 5, 2015 at 2:20:39 AM UTC+10, Karsten Schmidt wrote:
> Thanks, Mike. I implemented this and had to make some changes to make
> it work. I.e. since `register-handler` auto-injects the `pure`
> middleware, any of these :next-tick child handlers need to be
> registered via `register-base`...
>
> I updated the wiki, hope you don't mind... :)
>
> https://github.com/Day8/re-frame/wiki/Alternative-dispatch,-routing-&-handling#multiple-handlers-for-the-one-event
>


Of course! Thanks for correcting the Wiki.

--
Mike

--
Mike

whodidthis

unread,
Apr 5, 2015, 7:13:57 AM4/5/15
to clojur...@googlegroups.com
On Saturday, April 4, 2015 at 7:10:00 AM UTC+3, Mike Thompson wrote:
> So the problem sequence would be:
> 1. first event handler called with db snapshot
> 2. sync-dispatch called, making changes to app-db
> 3. first event handler finishes, and the value it returns is written into app-db
>
> The changes in step 2 are lost.

I tried to replicate this but with no luck:
(register-handler
::input-change
(fn [db [_ value]]
(println "input changed to" value)
(assoc db :my-value value)))
(register-handler
::badtimes
(fn [db _]
(let [my-future (+ (.getTime (js/Date.)) 3000)]
(println "badtimes START")
(loop []
(if (< (.getTime (js/Date.)) my-future)
(recur)))
(println "badtimes END")
(assoc db :badtimes my-future))))

::badtimes called with dispatch, ::input-change called with dispatch-sync while badtimes running i get:

badtimes START
badtimes END
input change to a
input change to ab
input change to abc... etc

Since handler functions are always synchronous could the warnings on dispatch-sync be a non-issue? Dispatch-synced events get in front of the line but they still have to wait for the current handler call to end. So there should be no problems as everyone gets the new db snapshot. Unless i'm missing something huge

Mike Thompson

unread,
Apr 5, 2015, 3:00:20 PM4/5/15
to clojur...@googlegroups.com
What I'm saying is that you can't use dispatch-sync inside an event handler ...

(register-handler
:a
(fn [db _]
(assoc db :a 100)))

(register-handler
:b
(fn [db _]
(dispatch-sync [:a]) ;; <-- problem - change is lost
(assoc db :b 5)))

If you:
(dispatch [:b])
then afterwards, in app-db:
- :b will have a value of 5
- the change to :a (to 100) will not be there

--
Mike


Reply all
Reply to author
Forward
0 new messages