re-frame why only 1 handler per [:some-event-id]?

188 views
Skip to first unread message

Luke Horton

unread,
Mar 28, 2016, 4:13:39 PM3/28/16
to ClojureScript
Why does the re-frame framework restrict a 1:1 between dispatches and handlers?

At first I found it a little weird. I can think of lots of realistic cases where:

```
some event happens -> module a responds; module b responds;
```

Then I thought... well, if one module has a handler that changes the app-db, every other module only needs to subscribe to the same query, and they won't ​*need*​ to also handle that event, it will happen reactively:

```
some event -> module a responds (change db value) -> module b reacts to change;
```

But then I remembered that very frequently I need to handle this behavior:

some event -> module a responds (starts fetching new data) ... eventually updates db;

Which I can't seem to fit into the paradigm of 1:1 handler to event.

If we only have a 1-handler per event restriction, how can I also have:

some event -> capture event in module a (start fetching new data); ??? ; capture event in module b (start fetching a different set of data);

Receiving the data would be relatively straightforward ... dispatch separate events (from a receiving chan) for [:a-data] and [:b-data], but how we implement in a decoupled manner the initial "go get data" across different modules is lost on me.

Any suggestions / patterns for solving this would be helpful.

Christopher Small

unread,
Mar 29, 2016, 12:03:38 PM3/29/16
to ClojureScript

There's a whole section in the wiki dealing with more custom handling, routing, etc: https://github.com/Day8/re-frame/wiki/Alternative-dispatch,-routing-&-handling

That should help.
Message has been deleted

Luke Horton

unread,
Mar 29, 2016, 3:54:37 PM3/29/16
to ClojureScript
Thanks for the link Chris. I've actually read over this wiki link before, but didn't think about it too hard. I was hoping I wouldn't have to make drastic changes to the infrastructure to handle my particular use, but it seems that might be the case. If I'm going to essentially write my own messaging bus that handles dynamic subscriptions and many to many handlers, I should probably just not bother with re-frame. The other quirk that kind of bothers me is how each handler defines its own middleware. I tend to think of middleware as an application-wide orthogonal offering for logging, debugging, reporting, etc. I don't like the idea of having to manually insert middleware for each and every handler, even if I generate a middleware factory.

Daniel Kersten

unread,
Mar 29, 2016, 3:59:58 PM3/29/16
to ClojureScript
I assume the reason that you can't simply register two handlers for the same event is that order of execution then becomes non-deterministic. This can be problematic (and hard to detect!) if you have two handlers for the same event that access the same state. By simply not allowing more than one handler, this isn't something that you can accidentally do.
You can, of course, implement this yourself using the techniques from that wiki page, but you have to make a conscious effort to do this, so you can think about ordering.
In your case, you said that each handler will access different state, so you don't have this problem, but I guess the authors of re-frame are playing it safe and not making that assumption, so unfortunately you have to jump through a few hoops (luckily its not too hard to do).

On Tue, 29 Mar 2016 at 20:54 Luke Horton <luke.w...@gmail.com> wrote:
Thanks for the link Chris. I've actually read over this wiki link before, but didn't think about it too hard. I was hoping I wouldn't have to make drastic changes to the infrastructure to handle my particular use, but it seems that might be the case. If I'm going to essentially write my own messaging bus that handles dynamic subscriptions and many to many handlers, I should probably just not bother with re-frame. The other quirk that kind of bothers me is how each handler defines its own middleware. I tend to think of middleware as an application-wide orthogonal offering for logging, debugging, reporting, etc. I don't like the idea of having to manually insert middleware for each and every handler, even if I generate a middleware factory.

--
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 https://groups.google.com/group/clojurescript.

Luke Horton

unread,
Mar 29, 2016, 4:11:21 PM3/29/16
to ClojureScript
Good point Daniel. As you mentioned, because I'm dealing with unrelated state, I didn't have to think about this issue.

I can imagine a convenient queuing system that captures everyone who wants to make a change, attempts the change, and throws if two changes attempt to modify the same piece of state... or some sort of explicit priority system (yuck).

I wonder if there is a better way to achieve my end-goal through a different means that's more in-line with the re-frame philosophy?

In my naivete I would attempt solving the problem this way:

[:button-clicked] event
top-level handler captures event, sets state [:clicked true]
component A, B both detect [:clicked ] flag change, do some dispatch of their own?
A, B handlers capture their respective dispatches

This seems awful for a few reasons. Namely the "circular dispatching" that make things really hard to debug/reason about.

Might you suggest another way? I could conceive of a "top level" component holding onto A and B and being the coordinator between the two... when [:button-clicked] shows up, it knows about A and B and how to get the data for both. The problem with this approach is it's not very modular, and doesn't scale well. How could you have a handful of A and B components smattered about, or a handful of top-level components, for that matter? Get's ugly quick.

Daniel Kersten

unread,
Mar 29, 2016, 4:36:40 PM3/29/16
to ClojureScript
I was working on an app last year where I separated events that are triggered in the components (I called them signals) and events handled by handlers (I called these events as these are the normal re-frame events) and then you could map signals to events. By default, a signal was the event of the same name, but you could override this and connect them any way you wanted. A signal could be connected to multiple events and multiple signals could be mapped to the same event.

In a way, the events were basically just slot names in a lightweight signals and slots system.

Of course this didn't handle ordering but like you, my handlers always accessed different state, so this wasn't a problem.

This is probably a overkill unless this is a common use case.

I implemented it by having an emit function that would look up the connections and then dispatch the correct re-frame events. Then my components always used that emit function and not re-frame's dispatch function.

Which technique you use depends on your goals and needs, I guess... I don't think one way is necessarily better than the other.

Mike Thompson

unread,
Mar 29, 2016, 4:47:42 PM3/29/16
to ClojureScript
At one point, I certainly considered allowing multiple handlers for events but I decided to put off implementing it until such time as I had a real need for it. But then, somehow over time, I just never *actually* needed it. It only ever stayed as a conceptual "I might need that one day" requirement. As a result, these days, born out of experience, I'm pretty sure it isn't generally necessary - and we write large, complex apps.

If you were to implement it, you'd have to be careful of "Glitches" (google for FRP and glitches). You'd also have a tougher time with figwheel and reloading handlers. So you'd have to be careful your weren't paying a high price for something that you didn't end up using that much.

I tried to position the reference implementation of re-frame in the "Goldilocks zone" for reactivity. But the chosen implementation is, of course, simply one point in the design space.

BTW, do you have an OO background? Your talk of code firing in module a and module b, and then async channels, etc, gave me the sense of someone trying to understand re-frame through a "message passing" mindset. I think, once you get into it, that might drop away a bit.

--
Mike





Luke Horton

unread,
Mar 29, 2016, 4:48:01 PM3/29/16
to ClojureScript
I like that approach. I think making the distinction between 'normal event' and 'bigger coordinated effort event' is a good idea.

I thought of a parallel solution to this, but I think I like yours better:

Dispatches all go into a channel, and components register to this channel. They get to say "on [:some-event], also dispatch [:my-other-event], [:another-event]". When a dispatch happens, it gathers up all registered events and does a dispatch for each event, including the original.

Essentially the same Idea, but not an independent "signals channel" versus "events channel".

How did that work out for you? Any pain-points trying to trace down [:some-event] -> ack what is happening here, 4 more thingies just happened and I don't know why?

Daniel Kersten

unread,
Mar 29, 2016, 4:58:30 PM3/29/16
to ClojureScript
I actually built a debug panel that was only included when running from figwheel. This panel displayed my state and also logged all signals, so I could always inspect what was triggered.

Over all it suited my needs and worked out well. It could probably have been designed without needing this but I liked being able to keep different capabilities independent of each other (and not worry that maybe a different capability registered a handler for the same events).

Luke Horton

unread,
Mar 29, 2016, 5:00:50 PM3/29/16
to ClojureScript
> BTW, do you have an OO background? Your talk of code firing in module a and module b, and then async channels, etc, gave me the sense of someone trying to understand re-frame through a "message passing" mindset. I think, once you get into it, that might drop away a bit.
>
> --
> Mike

Hi Mike,

That very well could be my problem. 2 years ago I wrote a custom event-driven architecture to serve a very data heavy application with some other special needs related to event sourcing. I'm also ~6 months into a redux-style architecture for another non-trivial workflow app. Maybe I just don't "get it", or maybe I'm not willing to "let go and trust the system".

I have a huge hang-up around modularity, having been bitten in the past. Architectures such as redux really don't solve that modularity problem out of the box, and I suspect (correctly or not) re-frame might suffer the same fate. To me, message-passing at the top-most level between components (in the DDD sense, aggregate roots) is "the way" to maintain decoupling and reuse. I get nervous when I don't see any strategy for communicating between two completely independent pieces through a well-defined API (message passing or others).

With respect to this concern, I wrote up a quick gh issue that may or may not prove helpful to re-frame: https://github.com/Day8/re-frame/issues/160

Luke Horton

unread,
Mar 29, 2016, 5:06:05 PM3/29/16
to ClojureScript

> Over all it suited my needs and worked out well. It could probably have been designed without needing this but I liked being able to keep different capabilities independent of each other (and not worry that maybe a different capability registered a handler for the same events).


I'm glad to hear it worked out well for you. I am currently working with a large redux application where we had to use sagas to coordinate independent components. The sagas work wonderfully (generators and yields, like core.async, are great mental models), but it does break a lot of the debugging tools because you have now given up 100% "pure 1-to-1 this happened then that happened". Generators/sagas/core.async hold onto state internally, so it's a pretty big sacrifice to give up wholly serializable state.

Mike Thompson

unread,
Mar 29, 2016, 6:17:04 PM3/29/16
to ClojureScript


If redux has proved a poor fit for your app, then you are probably right to be wary of re-frame, or at least its reference implementation.

If your app is "workflow" related, I'm imagining "plugins", hence your discussion around modules.

Perhaps you should consider taking the re-frame reference impl and tweaking it into the shape you want (multiple handlers). Normally, you'd be completely mad to invent your own framework, right? But there's really not many lines of code in re-frame (200?) and something like multiple-handlers would probably require a change to about 10 of them.

--
Mike

Mike Thompson

unread,
Mar 29, 2016, 6:31:42 PM3/29/16
to ClojureScript
On Wednesday, March 30, 2016 at 8:00:50 AM UTC+11, Luke Horton wrote:
>
> With respect to this concern, I wrote up a quick gh issue that may or may not prove helpful to re-frame: https://github.com/Day8/re-frame/issues/160


Thanks for that issue, I'll cycle back around and address it fully in due course. But more quickly I can easily answer your question on middleware:

> middleware - this isn't a weakness of re-frame, more just a design decision, > but I would be interested to hear how people feel about having to attach
> middleware to each handler independently? I have always thought middleware
> were orthogonal, app-wide wrappers initialized once and forgotten. I find it > strange that re-frame has a per-handler middleware scenario.


It is very easy to add middleware to all your handlers in one fell swoop. But we find it useful to add middleware selectively. For example, not every handler involves an undoable action.

But if you did want a global set of middleware (eg undoable and denug?) added to all handlers you'd do this:

(defn my-register-handler
([id handler]
(re-frame.core/register-handler id [(undoable) debug] handler)))

And, from then on, you'd just use my-register-hander to register all your handlers. That would always give you undoable and debug on all your handlers.

In fact, you'd probably write a multi-arity version of my-register-handler so you could selectively add further middleware to the global middleware set like this:

(defn my-register-handler
([id handler]
(re-frame.core/register-handler id nil handler))
([id extra-middleware handler]
(re-frame.core/register-handler id [(undoable) debug extra-middleware] handler)))

--
Mike

Luke Horton

unread,
Mar 29, 2016, 6:40:46 PM3/29/16
to ClojureScript
I see, thanks for that. I guess I was avoiding that strategy initially because it sort of felt like monkey patching in ruby/js, which is just bound to break eventually. It's not monkey-patching in this respect, though, it's just app-specific api design.

Mike Thompson

unread,
Mar 29, 2016, 6:57:50 PM3/29/16
to ClojureScript
On Wednesday, March 30, 2016 at 9:40:46 AM UTC+11, Luke Horton wrote:
> I see, thanks for that. I guess I was avoiding that strategy initially because it sort of felt like monkey patching in ruby/js, which is just bound to break eventually. It's not monkey-patching in this respect, though, it's just app-specific api design.


Yes, indeed, it isn't monkey patching at all. re-frame was designed specifically for this to happen. Notice how you can supply vectors of middleware with arb nesting, to allow for middleware composition in this way.

And, sometimes we also have an app specific "dispatch" too. We generally name it "emit", and this function can do whatever you want before eventually calling "dispatch". If you needed it to, "emit" can add an entirely different level of routing, including the ability to "look something up" based on the event supplied, and then dispatch twice or three times based on the results of that lookup - I'm thinking of module "a" and module "b" from your original post.

So it is all pretty flexible as-is.

--
Mike

Christopher Small

unread,
Mar 29, 2016, 7:09:26 PM3/29/16
to clojur...@googlegroups.com
Great conversation; I'll just add that I think it's a pretty fundamental part of re-frame (as I see it, thinking about the "materialized views all the way down | derived data all the way down | streams everywhere | however you want to sell it" approach that is behind re-frame and samza) that there is a _single_ app db from which the entire app emanates. (Of course, we have Mike T. himself here in this thread so he's welcome to object or qualify his own vision here :-)) It's what (as Mike T. pointed out) what's behind the glitch free semantics of re-frame, and brings with it some nice conceptual properties. Of course, your criteria of building aspects of the app as independent "modules" raises an interesting question about how you handle state in such a situation. As I see it, there are a couple of approaches to this problem:

* Have the top level of your application state (app-db, whatevs) reflect the collection of modules that you have in the application. So maybe it would be a map with keys corresponding to your different modules. You could build up helpers around this structure which make it fairly straight forward to add module data to the top level app db. There's still a bit of thinking to do regarding how you orchestrate/register the handlers for each of these modules (ideally this would be deterministic as far as state updates go). But there are already a lot of ideas in this thread which can be applied towards a solution given this app-db structure (methinks).
* If datascript/datomic are appealing to you, you could use a datascript db on the client together with qualified attribute names reflecting the module breakdown. If you set things up right, you don't really have to think about "organizing" data for one module versus the other. You can look into the posh library which gives you some nice tools for creating reactive datascript queries (materialized views/signals/etc) for use in your reagent components. It's really quite lovely :-) However, at the moment it's not out of the box interoperable with everything in re-frame. But we're currently in the process of figuring out how we'd get such interoperability (or at least implement the abstract re-frame pattern using these pieces).

Chris


On Tue, Mar 29, 2016 at 3:40 PM, Luke Horton <luke.w...@gmail.com> wrote:
I see, thanks for that. I guess I was avoiding that strategy initially because it sort of felt like monkey patching in ruby/js, which is just bound to break eventually. It's not monkey-patching in this respect, though, it's just app-specific api design.
--
Note that posts from new members are moderated - please be patient with your first post.
---
You received this message because you are subscribed to a topic in the Google Groups "ClojureScript" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clojurescript/qIvYyk5Ptek/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojurescrip...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages