Fork me on GitHub
#re-frame
<
2022-01-15
>
DrLjótsson09:01:50

Is there a way to watch a subscription and trigger a side-effect whenever it changes, i.e., outside of a component that depends on that subscription? It looks like reagent track! could be suitable but I’m not sure I understand how it works.

p-himik09:01:12

Subscriptions are a way to propagate and cache values for an eventual usage in views. From the re-frame's POV, every other usage is a misuse. With that being said, if you need to track some value, the right approach would be to: 1. Register an effect that does those side-effects 2. Use a global interceptor to compute that value and store in in app-db 3. Use a second global interceptor to monitor changes of that cached value and add the effect from point 1 to the effects map when needed

🙏 1
DrLjótsson12:01:01

Yes, of course, thanks @U2FRKM4TW ! One could even just use one :after interceptor and compare the :db in effects and coeffects.

p-himik12:01:14

Yeah, but it would have to be a global interceptor still - to avoid any potential issues if you eventually introduce a new event that changes the relevant data.

👍 1
emccue18:01:33

@UGDTSFM4M what is the thing you want to do? We haven’t found many legitimate uses for interceptors outside of debugging so im skeptical that you couldn’t do what you need with just db and fx

DrLjótsson01:01:11

@U3JH98J4R I need to trigger a state change in a fsm (https://github.com/lucywang000/clj-statecharts/tree/master/src/statecharts), which calculates data based on data in app-db. That calculation takes a while to run and has multiple steps. So I use the fsm to give back control to the browser within and between the calculation steps. If specific data in app-db changes, the fsm has to start over again, which i why I need the side-effect of triggering the state change. The db-event that changes the data is not ”aware” of the fsm and therefore can't dispatch the state change. Instead, the fsm will fill the app-db with the data when the calculation is completed.

DrLjótsson07:01:45

One could say that the fsm subscribes to the data. Only subscriptions are not available outside components, hence my original question.

emccue22:01:50

you can just wrap your events that might touch the relevant data

emccue22:01:35

(rf/reg-event-fx
  [{:keys [db]} [_ event-data]]
  {:db (update-in db [:some-stuff :abc] func event-data)})

emccue22:01:43

start with this, write a function like this

emccue22:01:02

(defn handle-resetting-fsm-processes
  [old-db cofx]
  (if (and (:db cofx)
           (not= (:some-key old-db) (:some-key cofx)))
    (update cofx :fx (fnil conj []) [[::tell-fsm-golem]])
    cofx))

(rf/reg-event-fx
  [{:keys [db]} [_ event-data]]
  (handle-resetting-fsm-processes
    db
    {:db (update-in db [:some-stuff :abc] func event-data)}))

emccue22:01:28

syntax might be a bit off, but basically that

DrLjótsson14:01:10

Thanks! I’m not quite sure if I follow though. I get that handle-resetting-fsm-processes resets the FSM. But it looks like the second function both calls the fsm-resetter and modifies the app-db with the value that the FSM depends upon? If I did understand the correctly, it is precisely what I wanted to avoid. I don’t want to couple the event that modifies the app-data with any FSM logic, since, as I designed it, the db-modifiying even does not need to be aware that there is a FSM “listening” to the change. I am trying to design a flow where the FSM depends on the change in app-db. Because there are actually multiple events that change the app-db in a way that will force the FSM to restart.

DrLjótsson14:01:17

So to reduce coupling it seems easiest for the FSM to register a global interceptor that watches for changes, and unregister that interceptor when the FSM is no longer working.

emccue14:01:09

Okay so thats the tradeoff - you reduce coupling but in exchange the behavior of your system is driven by the global system of interceptors

emccue14:01:57

definitely easier, i’d argue not as simple

p-himik14:01:05

I'd actually argue the exact opposite. :D

isak16:01:44

To me it seems like the real-trade off is performance. Imagine if you are building a big app, and every developer just adds a bunch of global interceptors. I would think after a while your application would start to get pretty slow. As for local interceptor vs wrapping functions, I'm not sure there is a compelling reason to prefer one over the other, it is more of a superficial difference. Maybe the function one is slightly better, because then you are just using the language instead of re-frame constructs for little benefit.

p-himik16:01:05

> I would think after a while your application would start to get pretty slow. Depends. If the order of magnitude is tens, maybe even hundreds, then it depends on the implementation of the interceptors. And you can trivially filter out known events that are in hot spots.

isak16:01:56

Ok, didn't know you could filter out events, that is good to know. But still, I'm hesitant to use stuff that can get out of hand by default, it becomes a kind of ticking time bomb that you may not detect. Stuff like that can linger for years, look at Teams by Microsoft, for example.

p-himik16:01:45

Now clue how Teams is relevant, but I feel like the discussion is getting off topic. The OP has a rather clear concern, which doesn't include designing an open-ended system.

DrLjótsson16:01:56

I have also thought that there may be A performance hit. OTOH, re-frame also adds a bunch of interceptors before and after each event. I hope that adding a simple function that checks for equality between a key in the effect and co-effect to that chain matters very little

DrLjótsson17:01:13

I have actually written an inject-interceptor fn that can add an interceptor to any event. I am not using it in this specific case though since there are too many relevant events. But I have found it handy to add logging to events that I don’t control

isak17:01:04

Right, but the stuff re-frame adds is a fixed quantity. So I'm just zooming out and making a point about architecture - is it smart to go for an approach where by default, your app will get O(n) slower as you add features? I'm thinking no, because the alternative is to just track the few events where path X can change, and then you avoid that problem.

DrLjótsson07:01:36

@U08JKUHA9, I agree! In this case I need to weigh the complexity of maintaining and refactoring the app (i.e., coupling and remembering to put the fsm restart event in every relevant app-db modifying event) against the performance loss (and potentially establishing a principle of mindlessly adding interceptors). My original idea was to use track! on a subscription, but I never explored that solution. I guess that solution would not suffer from the same performance penalty?

isak16:01:42

@UGDTSFM4M yea I see what you mean. For track!, it depends what you are tracking. If it is a 'Materialized View', then it wouldn't have the same problem (assuming the extractors it is subscribing to are already active signals). But if it is an extractor, it would, because they would all run any time the app-db changes. Btw, I'm using the terms on this page: https://day8.github.io/re-frame/subscriptions/#the-four-layers

DrLjótsson16:01:24

@U08JKUHA9, I’ve realized that the FSM actually needs to depend on different changes in app-db depending on its current state. And none of these are actually subscriptions that any component would use. So using an interceptor that is only active while the FSM is active seems to be the easiest (or even simplest 😉) solution. But your point about not adding unnecessary global interceptors is well taken!

1