Fork me on GitHub
#re-frame
<
2022-06-13
>
danielstockton06:06:17

Does anyone have any solutions for subscribing to data within events, that doesn't trigger the 'Subscribe was called outside reactive context' warnings? We're currently using a modified version of vimsical.re-frame.cofx.inject that prevents any memory leaks, but it is still a subscribe outside of a reactive context.

p-himik06:06:29

The solutions are the same as always - either extract common functionality from a sub and use it both in the sub and in the events that need it or create a global interceptor that recomputes the value on relevant data changes and puts it somewhere in the app-db - then you'll be able to create a simple getter sub and just get that value in events.

👍 3
p-himik06:06:37

Even if you use a modified version of inject that doesn't cause any obvious memory leaks, it's still dangerous to use reactive stuff outside of a reactive context when you have complex subscription hierarchies. Namely, you might see outdated values in some scenarios, such as having a diamond shape in your subscription graph.

2
danielstockton10:06:10

I'd not thought about the second point before. Not sure I fully understand how that can arise, and what different it makes whether the context is reactive..

danielstockton10:06:51

I've seen a github issue to allow subscribe to be called safely from any context. Is that not the likely future trajectory? It's a little clunky to have to extract common functionality, and not really what re-frame pushes you towards.

p-himik10:06:58

> what different it makes whether the context is reactive.. I don't recall all of the details myself, but in essence the implementation of how reactions propagate value changes explicitly differs between reactive and non-reactive contexts. > Is that not the likely future trajectory? Can't say, I'm not a maintainer of re-frame. :) But I imagine that if it gets implemented, it'll make subscriptions dual in some way. Using them in a reactive context would use the existing mechanisms and using them outside of a reactive context would use something completely new. Alternatively, maybe Reagent will be changed in a way that would make it safe to use reactions outside of a reactive context.

👍 1
kennytilton10:06:27

@U051KLSJF wrote: "solutions for subscribing to data within events...outside of a reactive context" Do you really mean "subscribing"? Or do you just want to read the data? ie, if the data changes after you get it, do you need some kind of notification? "Not sure I fully understand how that can arise, ..." In complex state with values derived from derived values, it can happen that a single derivation D using multiple inputs may end up with two or more dependencies that trace back to a single source value S. When S changes, a naive propagation scheme will recompute D more than once, and depending on the ordering by re-frame, individual recomputations may "see" obsolete state not yet updated. re-frame avoids this by taking the trouble internally to, as they say, "de-dup" the signal graph. I am told Reagent likewise avoids this, but iff a view function is "asking". Which brings us to... "... and what different it makes whether the context is reactive.." The phrase "context is reactive" seems to mean, based on what I was told about Reagent, just means that the reactive internals can "see" our references and fix up any inconsistencies before running some code, such as Reagent doing so before dispatching a view function. It may help to know that some libraries such as JS MobX take a different approach altogether. These order recomputation dynamically to guarantee values are always consistent with the latest state change, and thus these reactive values can be read from anywhere. I guess one could say that the "reactive context" is the read operation: "If you read me, I will run reactive code JIT to guarantee consistency before returning my value."

danielstockton10:06:51

It's a method of code reuse, rather than subscribing to anything in a reactive sense (expecting reactive updates/notifications). If data changes location in your DB, you don't want to have to refactor in multiple locations. You can factor out common functionality, but it's less convenient and more code/prone to bugs than being able to reuse subscriptions.

p-himik10:06:08

@U0PUGPSFR It's not entirely like that. Re-frame sub deduplication is of no importance, and there's nothing that explicitly fixes up inconsistencies. The reason why Reagent works at all is that it doesn't recompute the graph from the roots to the leaves - it computes it in the reverse order. What happens from the roots to the leaves is marking the nodes as "dirty". I myself have never been able to reproduce glitches with Reagent, even in non-reactive contexts (i.e. outside of views and reactions). But few people claim they could at some point do that, albeit they couldn't show the code when asked. Perhaps, they were using some old Reagent version that was susceptible to such issues. Perhaps, they'd done something wrong. Or perhaps, Reagent does still have this issue but it's rather hard to reproduce. If anyone's interested in more details, here's a long discussion: https://clojurians.slack.com/archives/C03RZGPG3/p1640264341111900

kennytilton11:06:57

@U2FRKM4TW Does our new Slack history go back to when I posted the Reagent glitch example? They do not arise often or easily, but if one understands reactive DAGs one can concoct a glitch in an hour or two. I am told it was "outside the reactive context", which I am sure is true, I was just focused on the reactivity and prolly did not have a view function involved (because I routinely do reactivity for much more than views). If we could find that post maybe someone could stick in a view function and confirm that reagent sorts things out before rendering. Oh, and I have not looked at Reagent in years, so let's hope we can find the post. 🙂

👍 1
kennytilton11:06:52

When I posted, btw, no one responded. That made me think it was a feature.

p-himik11:06:20

The history is supposed to go all the way back to the start now - it'd be great if you could find that example!

kennytilton11:06:26

"Re-frame sub deduplication is of no importance" Are you sure? Why does it get mentioned prominently? I was not able to create a glitchy re-frame example using the same approach that broke Reagent: A depends on B and C. C depends on B. Now futz around until the internals, given their own notification ordering, update A before updating C. btw, it might be necessary to have another intermediate value or two to provoke the glitch. With Cells I ended up with what Philip Eby christened the Pentagram of Death. But that was designed to defeat naive solutions to glitches, so it went a bit further with dynamic dependencies...ah, come to think of it, re-frame does not do dynamic dependencies, so it has the information to avoid glitches.

p-himik11:06:01

> Why does it get mentioned prominently? Because it, well, dedupes. :) It reduces the amount of memory and computations in the app. > re-frame does not do dynamic dependencies It does - you can have if in a subscription that makes the sub depend on different subs, depending on the values.

p-himik11:06:43

(reg-sub-raw :look-at-this-sub
  (fn [_ [_ val]]
    (subscribe (if val [:x] [:y]))))

kennytilton11:06:54

The reagent glitch: https://clojurians.slack.com/archives/C0620C0C8/p1531617904000017?thread_ts=1531504845.000219&amp;cid=C0620C0C8 The messages following that ^^ explain how to set values and observe the glitch. Not sure how to put that in a view function and test.

kennytilton11:06:24

Ugh, re-frame is rusty, too. In your example, does val itself originate in a subscription, or would it be supplied by a view function? Maybe I should take another crack at glitching re-frame. But, as I said and mentioned in that thread, conditional dependency is not necessary to create a glitch, we just need more than one path in the DAG connecting two nodes. "It reduces the amount of memory and computations in the app." Understood, and I suppose it is worth calling out if that is all it does. I wager it also guarantees consistency in edge cases. :thinking_face:

p-himik11:06:26

> does val itself originate in a subscription, or would it be supplied by a view function? It can come from anywhere.

p-himik11:06:18

> as I said and mentioned in that thread, conditional dependency is not necessary to create a glitch You also added "I think" in that thread. :) I think I understand your example, and to me it seems that conditional deref is exactly what makes it glitch out.

p-himik11:06:00

With your example, re-frame will also be susceptible in a sense that that zz track will be computed twice, with different values. If you replace tracks with raw reactions - what re-frame uses internally - the behavior will be the same (I just checked). And since you don't re-create or duplicate any tracks here, re-frame deduplication won't have any effect. And views don't care about the issue at all because the inconsistent values apparently don't make it to them - at least, in my attempts to change your code. But of course it's not great that zz can be inconsistent internally, even in a reactive context...

kennytilton11:06:07

"does val itself originate in a subscription, or would it be supplied by a view function? It can come from anywhere." OK, so the conditionality is outside the reactive system. What we are looking for is a subscription S whose body is (if A B C) where A, B, and C are subscriptions. But again, we do this not to create the glitch, we do this to motivate a reactive solution that avoids a dependency on both B and C at the same time.

p-himik11:06:30

> the conditionality is outside the reactive system Not sure what you mean. It can come from a subscription as well. It can be

(if @(subscribe [:xx])
  (+ @(subscribe [:xx]) @(subscribe [:bb]))
  @(subscribe [:xx]))

kennytilton11:06:30

"views don't care about the issue at all because the inconsistent values apparently don't make it to them" That ^^^ is the million dollar question: why not? You already know my guess. 🙂

p-himik12:06:54

The whole reactive graph computation is done in a single "block" - there's no concurrency there. Whenever some ratom that's been deref'ed in a view changes, that view is marked as "dirty". After all computations are done, React has a chance to actually render something - so it happens only once. So the root cause of views not caring about it all is because they aren't re-rendered during computations - they are re-rendered afterwards.

kennytilton12:06:02

"So the root cause of views not caring about it all is because they aren't re-rendered during computations - they are re-rendered afterwards." Yep, glitches eventually resolve themselves; when all propagations have been run, the data is consistent (and the views run fine).

p-himik12:06:01

Don't have the time to debug it in-depth, but created https://github.com/reagent-project/reagent/issues/568.

indy12:06:39

Is there a way for an interceptor to not allow the handler from being invoked?

sansarip13:06:57

I believe you can change the event being dispatched with a :before interceptor thereby also avoiding its handler, though I’ve never done this https://github.com/day8/re-frame/blob/master/docs/Interceptors.md#what-is-context Doing it this way seems a bit like a code smell to me though 👃 Unless the interceptor path is absolutely necessary, here are two alternatives_:_ • Just don’t dispatch the event unless certain conditions are met • In the event, don’t call the handler unless certain conditions are met

indy14:06:16

Tried dissocing the event from coeffects key in the context, did’t work, the handler was invoked but with the event vector as nil. Yeah it seems code smell to me too. But there is a situation in our codebase where we need to shortcircuit the handling for a lot of events based on a condition and refactoring each of the handlers seemed a large refactor.

indy12:06:59

Thanks I’ll keep the queue key in mind, for now I’ve taken the long route and refactored all the handlers.

👍 1