Skip to content
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 · 48 comments
Open

Global State #137

jaredly opened this issue Dec 9, 2015 · 48 comments

Comments

@jaredly
Copy link

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
Copy link
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.

@mike-thompson-day8
Copy link
Contributor

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

@jaredly
Copy link
Author

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
Copy link
Contributor

That is exactly the plan. :-)

@jaredly
Copy link
Author

jaredly commented Dec 10, 2015

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

@darwin
Copy link

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
Copy link
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.

@darwin
Copy link

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
Copy link
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.

@mike-thompson-day8
Copy link
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.

@mike-thompson-day8
Copy link
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.

@jaredly
Copy link
Author

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
Copy link
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).

@spieden
Copy link

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
Copy link

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
Copy link

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
Copy link
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?

@mike-thompson-day8
Copy link
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.

@martinklepsch
Copy link
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.

@mike-thompson-day8
Copy link
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
Copy link
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
Copy link
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.

@danielcompton
Copy link
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
Copy link
Contributor

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
Copy link

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
Copy link
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
Copy link
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
Copy link
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.

@arichiardi
Copy link

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

@danielcompton
Copy link
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.

@darkleaf
Copy link

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
Copy link
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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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
Copy link

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

@dijonkitchen
Copy link
Contributor

Any new updates on this given the new React Context API? https://reactjs.org/docs/context.html

@charlesg3
Copy link

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.

@daxborges
Copy link

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.

Proposal

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.

Use Cases

App / Devcards

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?

Complex Components "Instances"

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!

Proposed Solution (Rough outline)

  • Add a record (defrecord InstanceContext []) to hold the context.
    • Easy to check existence.
    • You can pass multiple paths - maybe even other forms of context like an id? (not sure how I feel about this).
      • Useful if you have subs or events from one component used in another component.
      • You can assoc and dissoc paths.
  • Pass instances of that record as the second item in the event / subscription vector.
    (def instance-context (map->InstanceContext {::component-specific-key [:path :to :component :instance]}))
    (dispatch [:update-item instance-context 1])
    (subscribe [:items instance-context])
    
    • Passing the context as an argument like @mike-thompson-day8's proposed in part 2 frame solution is also an option but I like that putting it in the vector requires no changes to re-frame core (I expect this to be a point of debate and look forward to hearing other takes on it).
  • Ability to thread instance-context through subscriptions. Sugar could be :<= instead of :<-.
    • Probably want the ability to whitelist context keys in order to keep subs efficient.
  • Other conveniences:
    • Interceptors to put context into coeffects and remove from event vector.
    • Utilities for path manipulation.
    • Using React context to pass the instance context along (As proposed already for frames).

Closing notes

  • I have huge respect for the work already done on this topic. I propose this paradigm shift constructively, apologies if it came off as anything but.
  • It should go without saying, please poke holes in these ideas!
  • This solution could still probably be made to look like the lib solution however, I appreciate re-frame for its willingness to take an opinionated stance. I may, of course, be biased here.
  • Separation of concerns:
    • Yes, this solution makes it easier to accidentally manipulate portions of the db outside of the intended domain.
      • We deal with this in our apps already (great power -> great responsibility).
      • The lib solution would only reduce the problem, not eliminate it.
  • I have some of this fleshed out in a private repo. I plan to make it public once I have the core functionality done. Ping me if you really want an early look.
  • Thank you to those of you who have taken the time to read this!

@mike-thompson-day8
Copy link
Contributor

@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

@gtebbutt
Copy link

gtebbutt commented Apr 1, 2019

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

@daxborges
Copy link

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.

Problems Solved

Paradigm

  • Frames
    • Instances of event handlers, subscription handlers, etc paired with their data / state, much like an OOP Class.
  • Instance Pattern
    • Groupings of event handlers, subscription handlers, etc (a Component) registered only once.
      • Components operate on instances of data much like deftype. In other words the data is an instance of a deftype and the Component is the deftype.

Pros

  • Frames
    • Aims to be a tradition lib.
      • Arguably easier to grok.
    • Higher level of "safety" due to siloed state.
    • Only requires refactoring of views in existing apps to support app instances.
      • I'm not sure if this is true for event handlers that call dispatch / dispatch-n.
  • Instance Pattern
    • Solves multiple problems.
    • Encourages reusability and composition.
    • Keeps the door open for app instance integration down the road.
      • Recognizes that apps will probably share singletons of the environment "Browser url"
    • Can use a primitive that works with React context.
    • Requires minimal changes to re-frame core.

Cons

  • Frames
    • Silos app instances, closing the door on future integration.
    • Doesn't address shared environment singletons.
    • Has complexity between Frames, Registrars & Hot reloading which needs further investigation.
    • Requires considerable changes to re-frame core.
  • Instance Pattern
    • Less intuitive (Not easy, but maybe more simple).
    • Less "safe".
      • Depends on namespaced keys and proper context.
      • I have some rough thoughts on how this can possibly be improved.
    • Requires refactoring of event & subscription handlers as well as views for existing apps.
      • Maybe not event & subscriptions... more exploration is necessary, here's a start.

@plexus
Copy link
Contributor

plexus commented May 5, 2020

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.

@superstructor
Copy link
Contributor

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.

@josefigueroa-nedap
Copy link

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.

@clyfe
Copy link

clyfe commented Jan 4, 2021

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: #664

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests