New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global State #137

Open
jaredly opened this Issue Dec 9, 2015 · 38 comments

Comments

Projects
None yet
@jaredly

jaredly commented Dec 9, 2015

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, global db/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?

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 9, 2015

Contributor

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.

Contributor

mike-thompson-day8 commented Dec 9, 2015

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.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 9, 2015

Contributor

I should also point you to @darwin's pure frame fork: https://github.com/binaryage/pure-frame and his pull request #107

Contributor

mike-thompson-day8 commented Dec 9, 2015

I should also point you to @darwin's pure frame fork: https://github.com/binaryage/pure-frame and his pull request #107

@jaredly

This comment has been minimized.

Show comment
Hide comment
@jaredly

jaredly Dec 9, 2015

We could use React's context to pass things down the React, which is how relay, redux, etc. do it.

jaredly commented Dec 9, 2015

We could use React's context to pass things down the React, which is how relay, redux, etc. do it.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 10, 2015

Contributor

That is exactly the plan. :-)

Contributor

mike-thompson-day8 commented Dec 10, 2015

That is exactly the plan. :-)

@jaredly

This comment has been minimized.

Show comment
Hide comment
@jaredly

jaredly Dec 10, 2015

Let me know if there's any way I can help -- I've done a lot of react

jaredly commented Dec 10, 2015

Let me know if there's any way I can help -- I've done a lot of react

@darwin

This comment has been minimized.

Show comment
Hide comment
@darwin

darwin Dec 10, 2015

@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.

bhauman/lein-figwheel#309

darwin commented Dec 10, 2015

@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.

bhauman/lein-figwheel#309

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 11, 2015

Contributor

@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.

Contributor

mike-thompson-day8 commented Dec 11, 2015

@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.

@darwin

This comment has been minimized.

Show comment
Hide comment
@darwin

darwin Dec 11, 2015

@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 commented Dec 11, 2015

@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.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 11, 2015

Contributor

@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.

Contributor

mike-thompson-day8 commented Dec 11, 2015

@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.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 11, 2015

Contributor

@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.

Contributor

mike-thompson-day8 commented Dec 11, 2015

@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.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 11, 2015

Contributor

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.

Contributor

mike-thompson-day8 commented Dec 11, 2015

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.

@jaredly

This comment has been minimized.

Show comment
Hide comment
@jaredly

jaredly Dec 11, 2015

@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 commented Dec 11, 2015

@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.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 11, 2015

Contributor

@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).

Contributor

mike-thompson-day8 commented Dec 11, 2015

@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).

@spieden

This comment has been minimized.

Show comment
Hide comment
@spieden

spieden Jan 3, 2016

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

spieden commented Jan 3, 2016

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

@Conaws

This comment has been minimized.

Show comment
Hide comment
@Conaws

Conaws Jan 4, 2016

@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.

Conaws commented Jan 4, 2016

@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.

@Conaws

This comment has been minimized.

Show comment
Hide comment
@Conaws

Conaws Jan 4, 2016

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?

Conaws commented Jan 4, 2016

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?

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Dec 18, 2016

Contributor

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?

Contributor

martinklepsch commented Dec 18, 2016

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?

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 19, 2016

Contributor

@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:

  1. Scoop up all the vars and put them in a deftype called say frame
  2. Give this deftype the same method API currently offered by re-frame.core
  3. rewrite 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:

  • devcards
  • testing
  • imaginary case where the programmer wants two apps on the same page.

Anyway, just initial thoughts.

Contributor

mike-thompson-day8 commented Dec 19, 2016

@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:

  1. Scoop up all the vars and put them in a deftype called say frame
  2. Give this deftype the same method API currently offered by re-frame.core
  3. rewrite 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:

  • devcards
  • testing
  • imaginary case where the programmer wants two apps on the same page.

Anyway, just initial thoughts.

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Dec 19, 2016

Contributor

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:

  • Using the 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.
  • Providing the frame deftype makes it much easier for other people to explore actual usage and solutions to Part 2.
  • Because the official API doesn't change we can always revert if serious issues arise. (Using the 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 events
  • re-frame.db/app-db (i.e. some app-db)
  • re-frame.subs/query->reaction subscriptions cache

I think I will try to implement smaller chunks of what you outlined as Part 1 and then we can see how to proceed.

Contributor

martinklepsch commented Dec 19, 2016

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:

  • Using the 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.
  • Providing the frame deftype makes it much easier for other people to explore actual usage and solutions to Part 2.
  • Because the official API doesn't change we can always revert if serious issues arise. (Using the 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 events
  • re-frame.db/app-db (i.e. some app-db)
  • re-frame.subs/query->reaction subscriptions cache

I think I will try to implement smaller chunks of what you outlined as Part 1 and then we can see how to proceed.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 19, 2016

Contributor

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:

So, where above I was talking about Registrar, I should have been talking about a registry, etc.

Contributor

mike-thompson-day8 commented Dec 19, 2016

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:

So, where above I was talking about Registrar, I should have been talking about a registry, etc.

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 19, 2016

Contributor

@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:

  1. The library adds all its registrations to a 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.
  2. The library would provide a 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?

Contributor

mike-thompson-day8 commented Dec 19, 2016

@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:

  1. The library adds all its registrations to a 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.
  2. The library would provide a 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?

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Dec 19, 2016

Contributor

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.

Contributor

martinklepsch commented Dec 19, 2016

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.

@danielcompton

This comment has been minimized.

Show comment
Hide comment
@danielcompton

danielcompton Dec 19, 2016

Contributor

To add to this list "Just for reference I think these are the bits of state that should be part of a frame:"

  • re-frame.trace has a 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.

Contributor

danielcompton commented Dec 19, 2016

To add to this list "Just for reference I think these are the bits of state that should be part of a frame:"

  • re-frame.trace has a 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.

@smahood

This comment has been minimized.

Show comment
Hide comment
@smahood

smahood Dec 19, 2016

Collaborator

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.

Collaborator

smahood commented Dec 19, 2016

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.

@arichiardi

This comment has been minimized.

Show comment
Hide comment
@arichiardi

arichiardi Dec 19, 2016

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 commented Dec 19, 2016

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 😀

@mike-thompson-day8

This comment has been minimized.

Show comment
Hide comment
@mike-thompson-day8

mike-thompson-day8 Dec 20, 2016

Contributor

@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:

  • devcards (multiple, distinct, isolated instances on the same page)
  • testing (create an instance, test, and then throw it away)
  • two apps on the same page (similar requirements to devcards)

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.

Contributor

mike-thompson-day8 commented Dec 20, 2016

@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:

  • devcards (multiple, distinct, isolated instances on the same page)
  • testing (create an instance, test, and then throw it away)
  • two apps on the same page (similar requirements to devcards)

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.

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Dec 20, 2016

Contributor

@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.)

  1. Making global state support the use case of multiple frames (which hasn't been possible because of global state before) seems like fighting state with more, further-reaching state.

  2. Committing to a construct like the global registry requires anticipation of all possible future use cases. If only one future use case does not work properly with the global registry we are back to square one. There needs to be an escape hatch/lower level construct.

  3. 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.

  4. If we still want a global piece of state to administrate/monitor/inspect all frames that can certainly be done — I just think it should be a thin layer of sugar in the core namespace instead of a deep-reaching assumption about how people will use multiple frames.

  5. 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?

Contributor

martinklepsch commented Dec 20, 2016

@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.)

  1. Making global state support the use case of multiple frames (which hasn't been possible because of global state before) seems like fighting state with more, further-reaching state.

  2. Committing to a construct like the global registry requires anticipation of all possible future use cases. If only one future use case does not work properly with the global registry we are back to square one. There needs to be an escape hatch/lower level construct.

  3. 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.

  4. If we still want a global piece of state to administrate/monitor/inspect all frames that can certainly be done — I just think it should be a thin layer of sugar in the core namespace instead of a deep-reaching assumption about how people will use multiple frames.

  5. 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?

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Dec 20, 2016

Contributor

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.

Contributor

martinklepsch commented Dec 20, 2016

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.

@arichiardi

This comment has been minimized.

Show comment
Hide comment
@arichiardi

arichiardi Dec 20, 2016

About the last point, I guess it is particularly true if we ever want to use it as reactive library for backend stuff.

arichiardi commented Dec 20, 2016

About the last point, I guess it is particularly true if we ever want to use it as reactive library for backend stuff.

@danielcompton

This comment has been minimized.

Show comment
Hide comment
@danielcompton

danielcompton Dec 20, 2016

Contributor

Hey quick update from Day8 team on this

  1. We've put in a ton of effort on 0.9 in docs and code, especially Mike on the docs, and we'd like a bit of a break from re-frame.
  2. We've got Christmas coming up and then we're all away on holiday for various times until February.
  3. The core issue for us is with moving away from global state is to work on ergonomics and use cases first, and then work backwards to the implementation. When Darwin did his work on pure-frame, he found that threading the frame context around everywhere was tricky. In practice, it can be a bit difficult to discover the ergonomics without doing an implementation, but we want the discussion to be driven by concrete use cases first.

Next steps forward:

  1. Mike, me, and the rest of the Day8 team are going to be taking a break from any serious work on re-frame until sometime in February.
  2. 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)
  3. We want to evaluate the different approaches for removing global state by looking at the use cases we care about and seeing how the code looks and feels to develop with, e.g.
    • Traditional re-frame apps
    • Testing
    • Devcards
    • Running multiple apps on the same page

Given that, we can't really look at merging in martinklepsch#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.

Contributor

danielcompton commented Dec 20, 2016

Hey quick update from Day8 team on this

  1. We've put in a ton of effort on 0.9 in docs and code, especially Mike on the docs, and we'd like a bit of a break from re-frame.
  2. We've got Christmas coming up and then we're all away on holiday for various times until February.
  3. The core issue for us is with moving away from global state is to work on ergonomics and use cases first, and then work backwards to the implementation. When Darwin did his work on pure-frame, he found that threading the frame context around everywhere was tricky. In practice, it can be a bit difficult to discover the ergonomics without doing an implementation, but we want the discussion to be driven by concrete use cases first.

Next steps forward:

  1. Mike, me, and the rest of the Day8 team are going to be taking a break from any serious work on re-frame until sometime in February.
  2. 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)
  3. We want to evaluate the different approaches for removing global state by looking at the use cases we care about and seeing how the code looks and feels to develop with, e.g.
    • Traditional re-frame apps
    • Testing
    • Devcards
    • Running multiple apps on the same page

Given that, we can't really look at merging in martinklepsch#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.

@darkleaf

This comment has been minimized.

Show comment
Hide comment
@darkleaf

darkleaf Jan 10, 2017

@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)))}))

Example for provider.

Example for wrapper.

It's like react-redux Provider and connect.

darkleaf commented Jan 10, 2017

@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)))}))

Example for provider.

Example for wrapper.

It's like react-redux Provider and connect.

@martinklepsch

This comment has been minimized.

Show comment
Hide comment
@martinklepsch

martinklepsch Mar 1, 2017

Contributor

@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

  • expose something through React context (e.g. a re-frame-provider component) and
  • establishing a new 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.

Contributor

martinklepsch commented Mar 1, 2017

@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

  • expose something through React context (e.g. a re-frame-provider component) and
  • establishing a new 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.

@darkleaf

This comment has been minimized.

Show comment
Hide comment
@darkleaf

darkleaf Mar 1, 2017

@martinklepsch

New macro is unnecessary.
You must use wrapper. It add value from context as first component arg (into [component url-helpers] args)))}))

darkleaf commented Mar 1, 2017

@martinklepsch

New macro is unnecessary.
You must use wrapper. It add value from context as first component arg (into [component url-helpers] args)))}))

@gtebbutt

This comment has been minimized.

Show comment
Hide comment
@gtebbutt

gtebbutt Mar 11, 2017

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 commented Mar 11, 2017

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?

@arichiardi

This comment has been minimized.

Show comment
Hide comment
@arichiardi

arichiardi Mar 11, 2017

@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.

arichiardi commented Mar 11, 2017

@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.

@gtebbutt

This comment has been minimized.

Show comment
Hide comment
@gtebbutt

gtebbutt Mar 11, 2017

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.

gtebbutt commented Mar 11, 2017

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.

@chpill

This comment has been minimized.

Show comment
Hide comment
@chpill

chpill May 17, 2017

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!

chpill commented May 17, 2017

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!

@jfigueroama

This comment has been minimized.

Show comment
Hide comment
@jfigueroama

jfigueroama Feb 23, 2018

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.

https://github.com/jfigueroama/re-frame

jfigueroama commented Feb 23, 2018

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.

https://github.com/jfigueroama/re-frame

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment