This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2023-10-31
Channels
- # ai (5)
- # announcements (11)
- # beginners (19)
- # biff (1)
- # calva (8)
- # cider (3)
- # clj-kondo (12)
- # clojure (97)
- # clojure-europe (39)
- # clojure-nl (1)
- # clojure-norway (74)
- # clojure-uk (35)
- # clojurescript (8)
- # component (8)
- # conjure (4)
- # cursive (13)
- # data-science (1)
- # datahike (55)
- # datomic (2)
- # emacs (3)
- # etaoin (6)
- # gratitude (1)
- # hoplon (12)
- # hyperfiddle (54)
- # introduce-yourself (1)
- # lsp (70)
- # missionary (40)
- # music (1)
- # off-topic (79)
- # re-frame (78)
- # releases (4)
- # sql (5)
- # squint (9)
- # tree-sitter (4)
- # xtdb (20)
The doc mentions LocalStore as good use for cofx, what about IndexedDB? The problem is that IndexedDB api are async only, is there any good pull-pattern that works well with re-frame before I go and re-invent the wheel?
I don't know much (or rather anything) about IndexedDB, but I guess you ought to handle the async aspect similar to e.g. an HTTP API: provide data to the cofx describing what to do once the async operation is complete, e.g. :on-success [::my-success-event]
or something like that and let the cofx handler dispatch the event (including the query results in the event)
That I guess would probably end up being exactly https://github.com/day8/re-frame-async-flow-fx which makes a set of pulls into a set of pushes, ie: just fx, no cofx
ah, yes. I was mixing cofx & fx
Just a bit of clarification needed on re-frame components, and when to return a fn
rather than hiccup. Is there a rule of thumb? I thought the outer function defining the component as well as the fn
returned had to have the same argument list, but I’ve seen the todo-mvc in re-frame that does not do that. So now I’m more confused.
It's not re-frame components, it's Reagent components. And Reagent has great documentation pointing out all the differences, common patterns and mistakes. It's easy enough to find when you know that the simplest component is called a "form-1 component", a component that returns a function is called "form-2", and "form-3" is a class-based component.
Right, I just researched those. But that’s a good point, re-frame != reagent. So… that said, I guess the time to use that inner anonymous function is when the component needs arguments to re-render itself and the values of those arguments come from subscriptions in a parent component. Right?
In other words, when change needs to propagate down
Like ‘smart’ components with their own subscriptions and ‘dumb’ components just re-rendering when the arguments change.
> when change needs to propagate down Yes. Don't conflate it with subscriptions though, since Reagent has no notion of them and different kinds of components can be used and useful without any ratoms/reactions at all.
Hey #re-frame,
I'm excited to announce we have a new feature in alpha.
A flow
is like a subscription, but with more affinity to re-frame's interceptor model, and less bound to react.
This makes flow
s more predictable, composable, optimizable.
• Live docs: https://day8.github.io/re-frame/Flows/
• Example project: https://github.com/day8/re-frame/tree/master/examples/flow
• Github discussion: https://github.com/day8/re-frame/discussions/795
We're gathering as much feedback as we can before we push toward a new release (or scrap it, you never know).
Please let us know your thoughts.
Thanks!
What if your app-db isn’t just a map of maps? Is there a way to generalize :inputs
and :path
to be functions?
I was curious about parameterized paths as well. I thought that may come up for the types of scenarios I’m often encountering.
I’m also trying to understand how async effects can interleave with this sort of logic. Or maybe even just effects in general. I noticed one example showed an event handler getting the output of a flow via a new fn. I’m fairly sure this isn’t making the event handler actually reactive to the flow output changing right?
Hey @U04ESH6949M, :inputs
should work fine that way (with some modification). Not sure about :path
, yet. Maybe there could be separate :get
and :set
keys for that purpose.
> this isn’t making the event handler actually reactive to the flow output changing right?
Correct. Nothing "reacts" to flows. That call to flow-output
is based on the value of app-db from the previous event. Flows just change value right after every event (but, technically, before the effects are triggered).
Some feedback on the docs, in the order of appearance:
• A typo: "the an :output
function"
• The bullet lists aren't rendered properly on the web page (compare to how GitHub renders them)
• rf/sub
is alpha, that should be mentioned
• Other typos: :live:?
and :live
both should be :live?
• "And independently of all this, :output is lazy. It only runs when :inputs have changed value." - it's not "lazy", it's "reactive". "Lazy" is something else entirely. Perhaps the colloquial meaning of the word is used, but I think that's just confusing.
• "The point is, you decide when the signal lives or dies, not React." - eh, when does React decide anything? Unmounting a view is my decision, evicting a subscription from the cache is re-frame's decision, Reagent is also there, but not React.
• Typo: "colleages"
• "or trigger the :reg-flow
effect" - oh, this is interesting. So it gives people a chance to royally screw up the ordering. :D Unless :reg-flow
is run even before :db
. Same for :clear-flow
• (defn controls [] ... [:a ...] ...)
- I'd replace those :a
with :button
• The code block in "It's a balloon prank planner app." doesn't look right. Subscription handlers are missing the second argument (not a big deal for CLJS, big deal for CLJ), the ::kitchen-volume
sub handler has wrong arguments, signal subs could use :<-
instead of the more explicit syntax
• "That means, to get a usable value for num-balloons-to-fill-kitchen
, we have to duplicate the business logic that we wrote into our subscription" - or use a global interceptor to do exactly what Flows do. :) At least on the level of a single flow graph. "[...] but at what cost?" - not much, actually. Not saying that flows aren't a worthy feature, but IME most of scenarios where flows would be applicable can also be solved with a global interceptor
• "rendering also causes state changes" - it doesn't, if "state" means "app-db". Otherwise, "state" means pretty much everything, and then you can't make that statement be untrue no matter how many new features are added
• "Once Reagent, Re-frame and React begin to share the concern of reactive dataflow, they can race, or play chicken. I'll react if you do! Can't run me if I unmount you first! Can't unmount me if I run you first!" - I don't get this part at all. If you don't dispatch events in lifecycle functions or subscription handlers, how can there be any race?
• "Something is looping in on itself here" - yes, but only in terms of concepts, not in terms of data/state. Unless you do something wonky, you can't make a sub implicitly depend on itself. With that in mind, that statement isn't that useful
• "If views are fully determined by the value of app-db
, when why have signals be determined by views? Why not simply have app-db
determine everything?" - because it's both simpler and easier for the vast majority of the subs (again - IME), even layer-3 ones. I don't need to create a whole whopping flow that's cached in app-db if all I need is a sorted view of some collection in app-db.
• "A flow's lifecycle is a pure function of app-db
(technically, app-db
and :inputs
, but :inputs
themselves are pure functions of app-db
, whether they be paths or other flows)." - there's an important discussion here. Given that you can replace a flow (and it's even somewhat encouraged given that it's documented and there are effects for it) and flows themselves are stored outside of app-db, it's not all that pure anymore, and it's also troublesome. Right now, I can send the whole app-db
to e.g. Sentry when an error happens and be able to restore the whole state of the app (I don't store non-serializable data in app-db and I don't change subscriptions dynamically). With flows being stored outside of app-db and encouraging flow changes via effects, that capability goes out the window, at least in general. Now the state of the app is split into two - app-db and flows.
Regarding :get
/`:get` for :inputs
and :path
. IMO it would be better to have:
• :input
which can be a map (where each value is a path or a function) or a function
• :output
which can be a vector path or a function
• Some other key like :derive
that would store what the current version has in :output
- the actual function that does the work
Extra :get
/`:set` keys would make things more complicated than they need to be.
Also, flows seem to be adding a proper rules engine to re-frame. I'm not well versed in the topic myself, but I feel I should ask just in case - has the prior art been considered? Would it make more sense instead of creating custom rules engine to add a bridge between re-frame and an existing rules engine?
Thanks for the super detailed critique! I fixed most of the small stuff.
> it gives people a chance to royally screw up the ordering. :D Unless :reg-flow
is run even before :db
. Same for :clear-flow
Good catch, I think. I'm not sure what screw-up you're imagining. But these actually are https://github.com/day8/re-frame/blob/c70bcd2efc39badabe29ed0ed77dff4a8f41f9be/src/re_frame/alpha.cljc#L427 which happen before :db
.
> replace those :a
with :button
Yeah, I'm still fiddling with that. :button
by default causes a page scroll, so I'm not sure the best way to avoid that without putting obscure interop code into our example.
> With flows being stored outside of app-db and encouraging flow changes via effects, that capability goes out the window,
We considered storing them in app-db, but didn't think of such a coherent justification. Now I'm thinking it's the way to go.
> add a bridge between re-frame and an existing rules engine?
Something to consider. We are mostly going off of our own inspiration, stemming from my earlier attempt at fixing subscriptions (i.e. rf/sub). That said, I'm a fan of odoyle-rules and I do see the similarities. We still have it in mind to build a new re-frame, which would share the characteristics of a rules engine, a relational db and a hierarchical state machine.
> :button
by default causes a page scroll
It definitely doesn't. Unless you have some weird JS running in the background doing something. Just a plain button doesn't scroll anything, and I just double-checked to confirm my own sanity. :)
> We considered storing them in app-db, but didn't think of such a coherent justification. Now I'm thinking it's the way to go.
Unclear, since flows rely on functions. But to be honest, I'm not sure what the best solution here is.
It would be possible to achieve all that I have now with Sentry and stuff by maintaining a rigid registry of flows and only allowing switching between those flows while relying on flow aliases instead of IDs for flows to be able to reference each other. But it doesn't sound fun.
weird JS running in the background doing somethingYeah I'm not sure, maybe it's something about how SCI imports reagent. But I found a better workaround for now. > Unclear, since flows rely on functions Would storing the fn body as edn solve the issue?
Alas, no. You can't store functions as EDN in general. Even if I can do it with my functions, doesn't mean that some library that introduces new flows will be able to do it. Storing only flow IDs can solve that, but then there has to be only one possible flow per ID.
I have a minor quibble here:
event -> app-db -> signals -> view -> event
∟-> signal graph -> signals -> view
what is that L
-shaped character that does not appear to be an L? A right angle? I suppose it is signifying the loop that is being described between views and signals, but if so, perhaps a diagram would be appropriate here. Otherwise, very thorough overview, looks like a cool feature!> This tutorial introduces a feature called Flows
, a trailing part of step 3 - call it step 3.b. re-frame's
tagline is "derived values, flowing" and, well, Flows
helps data to flow.
> A flow
calculates a derived value "automatically".
> When one part of the application state changes, another part is recalculated automatically. More concretely, when one or more paths within app-db
change value, then the value at another path is recalculated automatically.
I don't understand yet based on the description what problem this solves. I was hoping it would help more with chaining async side effects, but I don't think that's the case - i think it's more about "how do I use derived data in an effect?" idk
I too wanted to understand how this may interact with needing async steps. I use https://github.com/day8/re-frame-async-flow-fx right now for that type of thing. I understand the desire to not build all derived data into subs though. I’ve been down that path many times before and it always gets messy.
I’ve mostly been careful to make event handlers reuse fn’s etc that manage “state consistency” issues in the app-db. I think this new “flow” concept addresses that in hopefully a better way. I’m not sure how it’ll work when there need to be effects due to changes to app-db though as well (to calculate new outputs). I also note that the :on-change
interceptor has been there in re-frame & I’ve thought of using it many times before, but ultimately didn’t because it seemed too implicit and I think would just confuse us more in how the data flows and “where things are happening”.
This new “flow” does seem to be an improvement on that. It’s more explicit & accessible at least.
Do you guys have a use-case in mind? So far, I don't see any problems or solutions related to async-flow-fx. If an event was dispatched by async-flow-fx, the flows would behave as with any other event. For all intents and purposes, you can think of re-frame/flow
as an extra wrapper around the :db
effect. Like eugene said, it's like a rules engine in that when one part of app-db changes, another part updates.
I was asking for the use case that re-frame flows is for, actually, because it wasn't immediately apparent.
I think async-flow-fx was brought up because it has a similar name and I thought that it might be related (it is not)
Yeah I think we're going to rename that lib, actually. Since we want to release a v2.0 of it anyway.
I’m saying that I use async-flow for async things. This new flow is for non-async things. My curiosity right now in general, is that I’m wondering if I’ll end up hitting many cases where the new “flow” doesn’t work out well due to app-db path changes needing to trigger effects instead of only app-db updates to another path.
I understand that is not the goal of this new “flow”. I just am hopeful I’d be able to get enough utility out of it in “real world” situations where I wouldn’t be forced to just not use it due to need to wait for effects/async things to happen during a “flow” (which can’t happen).
I understand that if you are purely replacing sub-side signal graph usage, then this concern doesn’t apply since you wouldn’t be doing things like that there anyways.
The new Flows are not async, you wouldn't have to wait for anything - neither in subs nor in/after events.
It isn’t that I’m asking for them to be async. I’m wondering if they’ll truly solve problems in event handlers having to have tangled concerns for keeping app-db paths in sync.
I can't think of a reason why they wouldn't solve those kinds of problems. But then again, you can already solve those problems just fine with a global interceptor. It's just significantly more involved and harder to extend.
I already have largely moved away with complex subscription derivations and instead just manage it on the app-db side in event handlers. So I’m assuming “flows” will make that smoother for me to do.
We’ve stayed away from solving it with an interceptor. It sounds good in theory, but it ends up being quite implicit and confusing when it comes to debugging things or understand control-flow and dependencies. That’s at least what we’ve found in a few multi-person projects that have been long-lived.
It won't be less implicit with a flow, since it's basically a special-special interceptor. Perhaps some support from re-frame-10x could be added, dunno.
I’m optimistic on this new “flow” concept because it does speak to what we already learned about staying away from complex subscription-based derived values. My comments here have been more hopeful that I don’t run into limitations with it in typical event/effect sequences we encounter to where I can’t use it much.
Yeah, I’m happy that the new flow is more explicit than a “regular” interceptor slipped in there. This is a positive to me upon reading about it.
That's the opposite of what I've said. :) In terms of data changes in app-db, flows are exactly as implicit as a global interceptor. Some flow somewhere, perhaps even in some library, could change some value in your database without you having any idea about it.
> the use case that re-frame flows is for It is kind of subtle, or at least open-ended. I think our doc is decent at explaining how flows work, and how the example code operates, but it's been tricky to pin down the exact purpose. That message is spread throughout the different sections. Subscriptions let you create a graph of incremental computation. The problem is, you https://day8.github.io/re-frame/Flows/#reactivity, and you https://day8.github.io/re-frame/Flows/#caching. So we intend for flows to separate those concerns, providing incremental computation purely via the re-frame idiom.
I did misread what you said haha. I think it may still be an issue of implicitness, but I think it’s better than eg. on-change
interceptor.
@U02J388JDEG What do you think on having a support for flows in re-frame-10x? Feels like it should be possible to add.
@U0LK1552A please be sure to post any code you write to test out your assumptions. I'm less familiar with async-flow-fx, and I think a demo project would be super useful in proving out flows.
Are flows available in any deployed re-frame artifact or is it one of these “just use it from git” things?
Hey @U0LK1552A, re-frame.alpha
is now included in https://clojars.org/re-frame
My app shows a collection of widgets, each with data and attributes and state, so it seems to use flows I'd have to write one flow which loops over all widgets to see what needs updating, and do that any time anything changes in any widget. Unlike the subscription graph which first extracts all the info for the widget in question and doesn't recalculate widgets which didn't have input changes. Or would the expectation be that I dynamically create and register a separate flow for each widget, and remember to unregister it when removing the widget?
Flows don't replace subscriptions.
If you need some piece of derived data available right from app-db and that data can't be retrieved with a simple get-in
(usually because you want to use it in an event) - you'd use a flow to calculate and store the relevant data and then use subscriptions to retrieve that data.
If all you need is just extracting some data from some place, you'd use a regular subscription without any flows.
But each widget has some derived data which is currently being calculated by the subscription graph, and if I need that derived value in an event launched by the widget component, my two choices are to write a function duplicating what the subscription graph does, or to subscribe to the derived value in the view function and pass it to the event
This has caused me to go back and look at re-frame-utils
which scared me off the first time I looked at it because I didn't understand the performance warning, but now that I do understand things better I might give it a try
Right, and flows give you a third choice. If you have a sub that has, say, two input signals from plain subs, and runs some computation on those two values, you'd have a corresponding flow that does the same thing. And then that sub would simply extract the value that the flow has stored in the app-db. So it's a correspondence between complex subs and flows, not between views and flows.
I have a lot of subscriptions that look like this:
(re-frame/reg-sub ::derived-value
(fn [[_ widget-id]]
[(subscribe [::data widget-id])
(subscribe [::state widget-id])])
(fn [[data state] _]
(calculate-value data state)))
Ah, right. I imagine that for parameterized subs, you'd either have a flow per sub (and then, as you said, you'd iterate over all active widgets) or a flow per sub+value (you'd reg/unreg flows dynamically with :ref-flow
/`:clear-flow` when the corresponding widget is added/removed).
@U02J388JDEG Is my interpretation of the docs correct here?
@U04ESH6949M I think this is a pretty good case where flows give you what you need, and subscriptions don't.
(defn widget? [data] (:on? data))
(rf/reg-sub ::data :-> ::data)
(def widget [{:keys [on? id] :as data}]
[:button {:on-click #(rf/dispatch [::widget-clicked id])} (str id)])
(defn weight [fruit-type {:keys [count]}]
(let [std-weights {::apple 25 ::banana 50}
single-weight (std-weights fruit-type)]
(* single-weight count)))
(defn fruit-weight-flow [id]
{:id (keyword (str "fruit-weight-" id))
:inputs {:data [::data id]
:state [::state id]}
:output (fn [_ {:keys [data]}] (weight data))
:path [::data id :weight]
:live? (fn [_ {:keys [data]}] (widget? data))})
(rf/reg-flow (fruit-weight-flow ::apple))
(rf/reg-flow (fruit-weight-flow ::banana))
(defn widgets [ids]
(into [:<>]
(for [[k v] @(rf/sub ::data)
:when (widget? v)]
[widget v])))
(rf/reg-fx ::print println)
(rf/reg-event-fx
::widget-clicked
(fn [_ [_ id]]
(let [weight (rf/flow-output (fruit-weight-flow id))]
{:fx [[::print weight]]})))
(rf/reg-event-db
::init
(fn [_ _]
{::data {::apple {:count 2 :on? true} ::banana {:count 5 :on? false} ::state {::apple 2}}}))
Sorry this isn't perfectly coherent, but these are some things you can do with flows. We're still looking out for the reasonable design patterns to emerge.A subscription is live or dead depending on whether a subscribing component is mounted. In contrast, a flow is live or dead based on :live?
.
If you want the flow to be alive when the component is mounted, just use the same predicate for both :live?
and the component hiccup.
@U02J388JDEG Can the flow be used as a state machine? Is it going in that direction?
This neatly resolves overuse of injecting subs into event handlers.
One small suggestion:
reg-flow
should take the :id
as first argument.
This makes it a lot easier for IDEs to pick up a reference between the flow definition and its uses.
Cursive does a wonderful job with reg-sub
and reg-event-fx
: I can jump from a dispatch or subscribe to its “definition” with a single click.
Thanks @U02J388JDEG, are there plans to add other features for state machines?
I find state machines really good tool for decoupling UI and making it more clear
> reg-flow
should take the :id
as first argument.
They already do, actually. I'll add that to the docs.
The web page has this example:
(rf/reg-flow kitchen-area-flow-specification)
which is why i was wonderingI can say we're thinking hard about formal state machines, but not necessarily for flows, or even re-frame for that matter.
{:id ::state-assertion
:live? {:inputs {:x [:path :to :x]
:y [:path :to :y]}
:fn (fn [_ {:keys [x y]}] (and x y))}
:init (fn [db path] (assoc-in db path true))
:output (fn [value _] value)
:path [:assertions :x-and-y?]}
I have been wondering if you could build a state machine out of things like this. Basically the :x-and-y? key would be true when the assertion holds, and not present when it doesn't. You might be able to maintain a set at the path location, too (rather than a map). And we might consider making :output
be optional - this particular :output
fn is just a way of making it a no-op.I ended up working on a similar idea a while back https://github.com/domino-clj/domino I find this approach works well, one thing that might be worth considering would be to think in terms of graphs of related paths and cascading events. For example, if inputs produce a change at some path, and that path itself is an input for another flow, there's an implicit dependency there, since the change will cascade down. It would be good to consider rules for cascading updates explicitly, and also to think about a way to see all the paths that would be updated when a value changes. It's very useful to be able to answer the question of what are all the functions that will run when a value changes in the re-frame db.
> What if your app-db isn’t just a map of maps? Is there a way to generalize :inputs
and :path
to be functions?
@U04ESH6949M I looked into this, but for now I think it's a premature abstraction. We prefer to call re-frame a reference implementation, trusting the wide world of forks and libs to provide the custom solutions. For better or worse, re-frame is path-oriented programming.
That said, I think flows stand to improve on the https://day8.github.io/re-frame/Flows/#paths. We get an isomorphism between path and identity which we never had with subscriptions.
And we do have a fully general dataflow engine in mind, maybe we'll build it one day.