Open jaredly opened 8 years ago
Yes, I been contemplating this move from a framework to library for a while. It is coming.
In your terms, init-re-frame-state
should return a frame
. And then you dispatch
and subscribe
by passing in a frame.
Except, that means you have to pass frame
down through the entire function call tree which is arduous. Really arduous. There's something completely delicious and simple about the use of global dispatch
and subscribe
, even though it is clearly evil in some ways.
Anyway, a solution is coming.
I should also point you to @darwin's pure frame fork: https://github.com/binaryage/pure-frame and his pull request #107
We could use React's context
to pass things down the React, which is how relay, redux, etc. do it.
That is exactly the plan. :-)
Let me know if there's any way I can help -- I've done a lot of react
@jaredly You've done some work on react-devtools, right? Well, if you have free hands for some fun work... This might be interesting for you. Maybe you could help me with a fresh devtools fork. First goal is to bring CLJS REPL to client-side seamlessly integrated with devtools javascript console[1]. But there are more ideas to provide great devtools enhancements for cljs development.
@darwin kinda related ... at one point we developed a proof of concept debugger ... which allowed you to set breakpoints in devtools and then execute clojurescript code in the context of that breakpoint.
The proof of concept worked. But it was all a bit rough. But we've never had time to get back to it and improve it.
If anyone wants to hack on it, I';d happily make it public.
@mike-thompson-day8 sounds cool, I think I will be able to support that quite easily. Javascript code generated by figwheel can be executed in the context of current breakpoint if I send it to devtools and let devtools execute it as if it was entered into console directly. I could have a look at your solution anyways. Thanks.
@darwin if you can do that (execute in context of current breakpoint) then you are already there. No need for our heavier process. We have to fire the app up in Electron and then perform a sleight of hand to have two processes talking to the VM debugger (ours and the real devtools). If you can do it all on the inside of devtools then that's certainly easier.
@jaredly Hey thanks. I wasn't aware that Redux used Context in this way. On shallow reading, I can now see now they have the notion of a Provider
. Which leads to something like the old Container/Component pattern but (I assume) with Context thrown in.
I'm just dwelling on how that might look in a Reagent/re-frame/clojurescript context.
Hmm. Redux sure looks and acts like re-frame (and Elm, of course). But no mention of re-frame in the inspiration section. Middleware? subscribe? Etc? Huurrupphh - I want my 15 mins of github glory (15 characters of glory?). Oh well, moving on.
@mike-thompson-day8 or convergent evolution? :) clojurescript has had a reeealy low profile in the javascript community so far (om notwithstanding), whereas elm gets talked about a lot.
@jaredly re-frame was published about 7 months before redux, so there's not much "coevolutionary" about the two. And stuff like middleware is not a concept from Elm. And the redux readme has a certain comment and reference which is almost word for word out of the re-frame README (and it is sufficiently obscure that it could have only come from that one place). The Elm Architecture and re-frame were written up about the same time (late 2014).
I'd been sailing along enjoying the convenience of re-frame's global registrations until today when I discovered devcards. Scoping stub handler registrations to single "cards" for isolating and testing components seems pretty critical. I guess I can proceed with the hope that a single stub handler per event type per page of cards will be sufficient, but this is pretty restrictive. (Or just make separate pages I guess.)
Glad to hear something is in the works! Even a namespaced version of what we have now via some kind of context macro might work(?) e.g. (with-reframe-ns "cardfoo" (re-frame/register-handler :bar-handler ...)
Thanks for re-frame! Redux may have some of the same ideas (and a suspicious literal translation of its name), but it still doesn't have CLJS. =P
@mike-thompson-day8 what was the comment on the redux README that appeared plagiarized? Redux was what got me interested in FP and Clojure in the first place, and I was pumped when I came across re-frame to find that you had all the things I liked about Redux and more.
Also, would love to hear best practices for using re-frame with devcards. Anyone have ideas of how to show a limited subset of the db as the app state?
It's been a year since this issue has been opened and there's been lots of cool stuff coming out of re-frame, congrats to everyone being involved π
I'm wondering what the "official stance" on this issue is and if there are any plans around the general issue. While many want to use Devcards I for one would like to be able to use re-frame's event loop but without the Reagent atom. pure-frame
hasn't been updated in a while and as fast as re-frame moves that's very understandable :)
I think one way forward could be trying to refactor some parts of the code base in a backwards compatible manner while separating or making it easier to separate global state. Would the re-frame team be interested in such contributions?
@martinklepsch
Yes, I'm keen to see re-frame be a library, rather than a framework, but I'm mindful that all design decisions have pros and cons. Iβd like to be sure we see real benefits.
Part 1:
frame
re-frame.core
re-frame.core
functions in terms of some default instance of this deftype. So Part 1 is fairly easy. But part 2 requires a bit more thought.
In Part 2, when there are multiple instances of a frame
at once (one for each devcard?). You can't simply (subscribe [:items])
any more. All the code must (subscribe f1 [:items])
where f1
is a frame, or the id of a frame. The same with dispatch
, it must be called with a frame
or frame id
.
Now, to make a frame
available, within a component, we'll have to be using React's context
feature, or some Reagent equivalent. Some process which allows the frame to be "made available" through all the layers of child components, without the overhead of explicitly passing it down.
Aside: something to be careful of: contexts
are only available at component render time. So any on-click
handlers which do a dispatch
, will be called after the render is finished, and would have to close over the context
because, by the time they are called, the render is well over.
Anyway, this brings us back to registration. Where should reg-event-db
store the association? Perhaps into a frame? I don't think so. Perhaps into a Registrar
(map)? Maybe better. And then, when you create a frame
, you must supply one or more Registrar which contain all the event handlers, subscription handlers, etc? Same with reg-fx
and reg-cofx
?
I'm a bit attracted to the idea of frames
and Registrar
being identified by id
(keywords). Then the instance is identified by data. But that then requires a global register of such things, and we are back to a global. All the same, there is something βrightβ about using data (ids), rather than instances (of a frame). More thought needed.
The decision regarding Part 2, will feedback to Part 1.
Usecases to guide us:
Anyway, just initial thoughts.
Great to hear, thanks for the update and elaboration @mike-thompson-day8.
I think what you outlined as Part 1 sounds great β and I think it would make sense to implement that even before we know exactly what Part 2 looks like for a few reasons:
frame
deftype and a singleton we can (and should) recreate the API in re-frame.core
. I.e. it can all be done in a backwards compatible way.frame
deftype makes it much easier for other people to explore actual usage and solutions to Part 2.frame
type could be marked as experimental.) I'm naturally optimistic and don't see why the deftype would break anything but who knows.On the relationship between a frame
and a Registrar
: These seem to be always 1 to 1 so each frame
has one Registrar
. I don't see why you would have multiple registries as this would theoretically enable multiple handlers/subs/etc for the same id(?). With that in mind it seems logical that each frame
comes with a single Registrar
.
Storing all frames (and implicitly their registrar) in some place might be handy sometimes but I'm not sure it's hard enough to justify adding state to the framework. (There could always be re-frame.test-utils
or similar facilities to create this state on-demand for particular use cases.)
Just for reference I think these are the bits of state that should be part of a frame
:
re-frame.registrar/registry
for cofx
event
fx
and sub
re-frame.router/event-queue
FSM for processing eventsre-frame.db/app-db
(i.e. some app-db)re-frame.subs/query->reaction
subscriptions cacheI think I will try to implement smaller chunks of what you outlined as Part 1 and then we can see how to proceed.
Aside:
@smahood points out to me (via another medium) that I'm using words/terms in a confusing way. The correct set of words to use are:
registrar
- the function/ns responsible for registering things (https://www.merriam-webster.com/dictionary/registrar)registry
- the place where things are stored (https://www.merriam-webster.com/dictionary/registry)register
- the action of recording a thing (https://www.merriam-webster.com/dictionary/register)So, where above I was talking about Registrar
, I should have been talking about a registry
, etc.
@martinklepsch
I note the following regarding the possible separation of a Registry
(wrongly termed Registrar by me above) from a frame
... consider the re-frame-undo
library. It uses reg-event-db
and reg-sub
. How should it work when you want to add the undo capability to given frame
(but not another) ?
I can see two possibilities:
Register
instance. And then, in an app, a programmer would include this Register instance when creating a frame
(they may provide multiple Registers). This would require us to have separate frame
and Register
.register-my-stuff
function which an app programmer would call with a frame as an argument. The function would "inject" necessary registrations into the frame
And how to do this in a backwards compatible way.
Other thoughts? Preferences?
My current understanding is along the lines of "each frame has a separate registry" and frames would expose a method of registering stuff (with their respective, embedded registry). This would allow adding re-frame-undo
like this:
(undo/register-undo! frame)
I hope I fully understand the question... What the above would require is a reg-event-db
(&c) that takes a frame as argument. In re-frame.core
we could provide variants of these functions with a frame already provided to allow users not to worry about the global state, e.g.:
(def reg-event-db (partial frame/reg-event-db the-global-frame))
Hope that illustrates my thoughts on the use case outlined above.
To add to this list "Just for reference I think these are the bits of state that should be part of a frame:"
trace-cbs
atom. We'd want that to be encapsulated in the frame, and somehow pass the frame to the tracing macros. I mention this more for completeness, not that it needs to be solved right away.
Not sure if this is useful or not, but I've been thinking about using the initialization step that loads your app-data (as in https://github.com/Day8/re-frame/blob/master/docs/Loading-Initial-Data.md#getting-data-into-app-db) as a distinct effect, and have that be the place where your frame is defined. There are almost certainly some use cases where this won't work and I have no idea if it's technically feasible, but if we can take advantage of the existing namespace tree (such that children NS are using the frame defined in their calling NS), then the backwards compatibility comes essentially for free for existing programs.
Past that, I think if we can utilize interceptors or cofx (perhaps expanding the implementation but keeping the mental model consistent) then that seems to be an ideal way to do this - part of the context is the frame that will be used. It would mean extending them to work with subs and such, but it seems like this kind of dependency injection is what they are built for.
My 2c and thoughts.
I am still unsure this should go in here as while devcards is very good to have in your workflow, many maybe are not using it. I liked pure-frame and I wish it was a library actually.
Totally random ideas: Could a key in the db for each devcards component be an alternative? Or an interceptor to save/reset/restore parts of the DB that are needed by each devcards page? I guess the other thing that is need is to bootstrap the page but again interceptors could be handy.
I am just afraid that such a big change might complicate things in aframework that is disarmingly easy to grasp. This is definitely good to analyze deeply here in written prose (no Slack I mean) so that the brainstorming process is visible.
Apart from that, good job as usual with 0.9.x
π
@arichiardi I understand your caution. I feel it too. We write big applications using the current approach and, frankly, we find it works very nicely. I like the current simplicity (in both in library code, and program use). But, I'm open to working out how we tweak things in the direction of a library. But only once I get the overall picture right and see that it is worth it.
By "worth it", I'm expecting to be guided by big improvements in these usecases:
Any other usecases I should consider? Is this the right way of assessing? (functional snobbery around globals is understood but is not a consideration :-))
@martinklepsch you have gone for "option 2". But I'm inclined more towards "option 1". I'd like a data oriented design. A Register
is just a map. I'd like to be able to deep merge
them, and do other data oriented kinds of manipulations. Data - that's the way we roll :-) But that's just an initial reaction, and all options remain on the table.
@mike-thompson-day8 I absolutely understand and share your desire to have a design built around data. data > functions > macros
is my jam π. Just to describe my understanding of "Option 1" (a.k.a. "global registry") in a bit more detail:
The Registry is a piece of state that holds all handlers potentially spanning multiple Frames. All Frames would have to be registered through that piece of state. Right now a registry's structure looks like this:
{kind {id handler-fn}}
If I understand you correctly "Option 1" would change this by introducing another level:
{frame {kind {id handler-fn}}}
Assuming this is correct let me make the following comments. (Using a numbered list so it's easier to refer back to individual items, not it imply importance or any other meaning.)
While we can deep-merge
the global registry data above we can also do data manipulation things with separately existing registries, e.g.
(deep-merge (:kind->id->handler (:registry frame1))
(:kind->id->handler (:registry frame2)))
In this case you'll need references to the respective registries / frames but that doesn't seem like a significant issue to me.
When I used the-global-frame
in a snippet above I was really referring to a piece of data that describes one instance of a frame. It could be a record looking like this:
{:registry {:kinds #{,,,} :kind->id->handler #cljs.core.Atom{,,,}}
:event-queue (router/->EventQueue :idle interop/empty-queue {} registry)
:app-db re-frame.core/app-db ; (or something else)
:subscriptions-cache #cljs.core.Atom{,,,}}
As you can see it's all data under the hood.
I talk about assumptions and unanticipated use cases above and really I can't think of anything particular that wouldn't work with a global registry. That said someone will find a funky way to break it and I'd rather have a malleable (or wieldy as Luke VanderHart put it) system than a system that irreversibly commits to something that may limit my possibilities down the line.
If you've read all this, you probably deserve a fun video, don't you?
Regarding use cases I would like to add: Everything in re-frame except for subscriptions is not tied to Reagent in a specific way. While I don't expect Re-frame to support other React wrappers any time soon it might be a good thing to keep in mind. I really like Re-frame's event model, the coeffects, effects and interceptors and being able to use that elsewhere (even with some extra fiddling) would be pretty cool.
About the last point, I guess it is particularly true if we ever want to use it as reactive library for backend stuff.
Hey quick update from Day8 team on this
Next steps forward:
context
feature. (this is a probably a blocker on evaluating use cases effectively)Given that, we can't really look at merging in https://github.com/martinklepsch/re-frame/pull/2 into re-frame until we see example code on how it affects use cases. I can understand the desire to get some movement on this, but we don't want to pre-commit ourselves to one implementation before we see how it affects user code.
In summary, we feel this is worth investigating further and after a refreshing holiday break, we'll be back to look at this with fresh eyes. I realise that's probably not what you're wanting to hear. If anyone is wanting to progress this further in the meantime, then working on providing React's context
feature in Reagent would probably be a good step.
@danielcompton, @martinklepsch Hi!
Reagent (or a temporary fork) needs to have support added for React's context feature. (this is a probably a blocker on evaluating use cases effectively)
Reagent support context feature. Example for working with react context and "Higher-Order Component":
(ns quester.util.url-helpers
(:require [reagent.core :as r]))
(defn provider [& _]
(let [helpers (atom {})]
(r/create-class
{:displayName
"UrlHelpersProvider"
:getChildContext
(fn [] #js{:urlHelpers @helpers})
:childContextTypes
#js{:urlHelpers js/React.PropTypes.any.isRequired}
:reagent-render
(fn [request-for & children]
(let [url-for (fn [& args]
(let [req (apply request-for args)]
(assert (= :get (:request-method req)))
(assert (= #{:request-method :uri} (-> req keys set)))
(:uri req)))]
(reset! helpers {:request-for request-for
:url-for url-for})
(into [:div] children)))})))
(defn wrapper [component]
"Higher-Order Component"
(r/create-class
{:displayName
"UrlHelpersWrapper"
:contextTypes
#js{:urlHelpers js/React.PropTypes.any.isRequired}
:reagent-render
(fn [& args]
(let [this (r/current-component)
url-helpers (.. this -context -urlHelpers)]
(into [component url-helpers] args)))}))
It's like react-redux Provider and connect.
@darkleaf Hey, thanks for outlining an example of context usage here, definitely helps getting an idea about how this could progress. The main problem I see with using context is that it effectively requires quite a bit of boilerplate to components even when they do only basic subscribing.
One way around that could be to
re-frame-provider
component) and def
-like macro that handles setting contextTypes
and making the relevant functions available in the component body. I'm not a fan of new def-like macros usually but β assuming we want to use context / eliminate global state β maybe it's the only way? Rum's defc
macro and mixins might be interesting to look at in that regard. It's relatively open/composable.
@martinklepsch
New macro is unnecessary.
You must use wrapper
. It add value from context as first component arg (into [component url-helpers] args)))}))
I've been investigating re-writing an Om app using re-frame, and I've landed here because (I think) I need a subset of this functionality. It's not a use case that's been mentioned yet, so there may be a better way of doing it - I'm coming from a few years using Om, so it may be clouding my understanding!
Currently, all pages of the site (including user-authenticated ones) are able to render client or server side. On the client, global state is fine for this; on the server (node.js), it requires building a separate state per-request and feeding it into render-to-string
as an argument. In Om, the function looks like this:
(defn render-to-string
"Takes a state atom and returns the HTML for that state."
[state-atom component]
(->> state-atom
(om/root-cursor)
(om/build component)
(dom/render-to-str)))
Is there a way to achieve something similar with re-frame, bearing in mind that monitoring for changes and re-rendering isn't needed here, or would it need to wait until the entire state is decoupled?
@gtebbutt if you are investigating the port to re-frame
be aware that given the nature of the event system re-frame
implements, server side rendering is not as straightforward as in Om.next. People have attempted this with moderate success in the past and maybe you are already aware of this, just wanted to point it out.
Thanks @arichiardi, that's useful to know. I'm looking at the options right now, so any info is helpful - whichever one works best for the project, it's good to keep up with the current landscape.
Since the existing server API provides a fully constructed map of the initial state, it's starting to look as though the event system could potentially be sidestepped entirely on the server; basically, the render function from above becomes:
(defn render-to-string
"Takes a state atom and returns the HTML for that state."
[state component]
(with-redefs [re-frame.db/app-db (ratom state)]
(reagent.dom.server/render-to-string component)))
It should be perfectly safe, given the JS concurrency model, but the use of with-redefs
still scares me.
Hi guys, I created a fork of re-frame to explore some of the ideas detailed in this discussion.
https://github.com/chpill/re-frankenstein/
A live example of what it can do: https://chpill.github.io/todos-re-frankenstein/
The readme should give some insight about the approach but please open issues if things are not clear (or just wrong!).
I feel we can really make a version of re-frame that could work without global-state and be used with Rum (or other view layers!). Let me know what you think!
Hello. I made a separate namespace to host a local state version of re-frame v10.2 some months ago. Basically, one creates a state which holds mostly all containers of re-frame (app-db, kind->id->handler, etc.). Then, inside the app, all calls to reg-sub, reg-event-X, subscribe, dispatch, will use the created state as first parameter.
The namespace is called re-frame-lib
.
(defonce state
(-> (new-state)
(reg-sub :db (fn [db _] db)))
...
(reg-event-db .....)))
;; later ...
(dispatch state [:event-x])
(subscribe state [:db])
It appears to work well. I can use devcards with 2 re-frame applications on the same namespace/file. It may be useful for someone.
Any new updates on this given the new React Context API? https://reactjs.org/docs/context.html
re-frame is a really nice framework to use. Thank you much for providing it.
I too would love to see re-frame turned into a library (as has been done by a few of the now outdated forks) and to provide support for not having the app-db
be a global variable.
Let me start off by saying thanks for the time, thought and effort of those who have contributed to re-frame and this thread! Below I proposes changes that break from a lot of work already done. I hope it's taken as it's intended, which is to be constructive.
We should reconsider the solution of "lib / siloed app / separate states / db's / frames". I had the same initial inclination towards making re-frame a lib with siloed states. That being said, a core concept (and IMO a "superpower") of re-frame, or similarly redux, is the single global state where different parts of the system only pay attention to their domain. I believe the lib solution unnecessarily subverts that core principle.
Instead we should continue to embrace the power of a single state and encourage thinking in terms of, what I call, the "instance pattern", where you have a set of events & subscriptions (you can call these "components", "modules", "frames", whatever... I call them "components") that operate on an instance under their domain (be it a whole app or an item) by receiving an absolute db path to where the concrete data resides. To support this we wouldn't need to refactor the core of re-frame but we would need to agree on a consistent pattern of passing path context to both events & subscriptions, similarly to @mike-thompson-day8's proposed part 2 frame solution. There would also need to be support for threading the context through subscriptions. Lastly, we'd probably want to build up a few more general utilities / conveniences for both events and subscriptions to improve the ergonomics of working with context paths.
One immediate case I can think of where the "instance pattern" succeeds over a lib is when you have two apps / devcards that manipulate the browser url. Unless you want to give up that functionality, you'll need to merge it and at that point you really have one app with two instances that only look like their own apps. The fact that you have two apps on the same page is itself a strong indicator that you'll have interop so why encourage a pattern that effectively shuts the door on the option?
There is a need for the "instance pattern" outside of the more dramatic devcards example. See Composing "complex" components or consider the simple case of "Items" where you have events & subscriptions (a component) that operates on items as well as individual items. Currently we tend to do an ad hoc solution of passing an item id as an argument (dispatch [:update-item 1])
. Under the hood of the events you resolve that id 1
to some path like [:items 1]
. That's all fine until you want to have two instances of items. You then run into a cascading effect where you'll likely pass a second id and need to rewrite your events / subs to take it into account (dispatch [:update-item :items-a 1])
resolving to [:items-1 1]
. This is still a small enough case that it's doable but on closer inspection it's a recursive problem which can be solve by path composition and in the end scale up to a component that is itself a standalone app. In other words: components all the way down!
(defrecord InstanceContext [])
to hold the context.
(def instance-context (map->InstanceContext {::component-specific-key [:path :to :component :instance]}))
(dispatch [:update-item instance-context 1])
(subscribe [:items instance-context])
:<=
instead of :<-
.
@daxborges my current thoughts on this can be found in the EP-002: https://github.com/Day8/re-frame/blob/master/docs/EPs/002-ReframeInstances.md
@mike-thompson-day8 @daxborges I just want to jump back in here and add the server-side rendering situation to both of your use cases. Not that I think these solutions would be a problem in that context, just that it seems to me the likeliest use case where there will be many different app-db
s (one per user) and thus probably makes sense to consider at design time.
It's arguably somewhat simpler (as the state probably only needs to be set once), but it is also a situation where the instance count may be high and it's critically important that data doesn't leak between instances.
Thanks @mike-thompson-day8. I've summarized the comparison between your thoughts Frames
and mine Instance Pattern
. I've also put together an in depth comparison if you're interested.
deftype
. In other words the data is an instance of a deftype
and the Component is the deftype
.Since re-frame is getting some renewed attention I wanted to point out that we've been quite succesfully using a fork based on @martinklepsch's frames PR at Nextjournal, which we're calling freerange. We're still working on and improving this, but are already using this happily on two separate projects.
The big driver for us was being able to have devcards with their own isolated "frame". We've found the frames approach combines really nicely with React context.
Thanks @plexus π
@mike-thompson-day8 has actually already pointed out freerange to me and we've had a look at the source code to inform some future work that will be coming on introducing contexts to re-frame. It won't be exactly the same, but its certainly helped our thinking so thank you!
Both multiple re-frame instances on a page and reusable re-frame 'components' will be built on the context infrastructure.
I am also maintaining a re-frame
fork in the same line of freerange
. However, I am the only maintainer and freerange seems to have more people involved. Also, the source code looks great.
Has a plan distilled for re-frame instances that has maintainers blessing? The idea is 5 years old at this point and there are a few forks. I'm happy to do some work if there is an agreed path. Generally the approach seems to be this: https://github.com/day8/re-frame/discussions/664
Thanks for writing re-frame! It's super awesome.
It would be nice to have the option of not using global state (the event queue, the
key->fn
handler functions atom,undo/redo-list
,id->fn
event handlers atom, globaldb/app-db
).I'm imagining a
(init-reframe-state)
that returns a map with all of the relevant bits. Then all of the public functions that usually just use the global state could take that state map as a first argument. e.g.(dispatch my-state [:event])
,(register-handler my-state ...)
etc. If you don't pass in your custom state map, then they would default to the current behavior.Thoughts?