-
-
Notifications
You must be signed in to change notification settings - Fork 715
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
Composing "complex" components #264
Comments
@mcortesi this is a good, well-framed question. It is related to #137, but comes at it from a different angle. I think you've outlined the main options. You could also use Reagent cursors for encapsulating the state, but that won't work with encapsulating events. Cursors are likely to bring new problems, and I wouldn't recommend them. This general problem would also help people make reusable re-frame components. At the moment this isn't really possible to do in an elegant way. @stumitchell do you have anything to add here? I think you've been thinking about this a little? |
Hmm, components are currently, not well supported, we are looking into ways of doing this, and have suggestions where there is only one component rendered at a time. But that is not what this question is asking. Mostly, I would recommend local state and using pure reagent (not re-frame). |
@mcortesi I was trying to answer the very same questions, going back and forth. Finally I ended up implementing what Elm does, on top of re-frame (although it uses only a small subset of what re-frame offers). It might not be the recommended way, but it composes really well, I'm quite happy with it. Here is a sneak peek, the counter example: https://gist.github.com/vbedegi/b7292af981020d128d48dd2218468746 |
I think it's a difficult issue to solve within re-frame. I work on daily basis with js redux, and it's a similar problem. The key thing in my opinion is events and subscriptions are global. If you have a components you need to dinamically scope (route) events and subscriptions so that they refer to different parts of the global state. In Elm, messages (events) and the model, are not globally defined, instead each component defines it's messages and it's model, and the the parent component is responsible of scoping (routing) everything so that the child component doesn't know about the global structure. This is done by parametrizing the input of the component, and encapsulating the output of them (the msg). Something like @vbedegi shows, tries to do the same thing. The option that @stumitchell suggests, is what normally people suggest in the redux community. Don't use re-frame for that. Just have an intelligent component, with internal state, and actions, and parametrize everything you need to plug it in with your events/state. I think this can work for a component like a Modal, but sometimes it would be nice to have a better support. |
Hi there. I'm not sure if I got it right, but I solve this problem like this:
E.g.: Subs (def counter-id :counter-1)
(subscribe [:counter counter-id]) Events (dispatch [:counter/increment counter-id]) And DB looks like this: {:counter {:counter-1 {:value 2}
:counter-2 {:value 3}}} |
Hi there! I'm a little late to this discussion, please excuse me if you've discussed this elsewhere. However, I think I'm failing to see the problem. If you have a library that defines a component, events, and subscriptions that all go together and are well namespaced, it should work out okay. Example:
Note that I'm scoping everything in the DB to a sub map with a namespaced keyword. Does this not do what you want? You can have multiple counters. You can view them from multiple components. |
Yes this is similar to what we are thinking, but it would be nice to have a solution where the |
Hi all! It's been a while. Initially, i would say it's not what i would like @ericnormand. The case for me, is that you as a component maker need to be aware of the global prefix of your state; and also probably need to handle "garbage collecting" state when the component is destroyed/unmounted. What's nice about elm model, is that the component doesn't know it's being embebbed, nor it can distinguish between being embebbed or being the root component. For "him" there is no other state other than his. So, it's a nice abstraction. I doesn't need to care about what happen when it's ummounted or destroyed. The problem, i think, is that this abstraction is in conflict with the notion of global subscriptions & events. In a way, one would like that for the component, events&subscriptions were global, while for the system they are just local for the component. |
Hey @mcortesi! I think I understand better now. Besides conceptual elegance, what is the actual problem you're trying to solve?
Why does it matter that you need to be aware of the global prefix of your state?
Garbage collection seems like a non-issue for me. If one component adds to the global state (through events), it doesn't mean that it should be removed when the component is unmounted. Other things might still want the value and an equivalent component could be remounted to the same value.
I'm not that familiar with the new Elm Architecture. Does embedding refer to being part of a larger component or having its state saved somewhere?
I don't understand this sentence. I must be missing something. For me, state is either local to the component or global to the entire system. If it's local, nobody else has access to it. If it's global, everybody has access to it. What does Elm have that is different from local and global? In the end, it sounds like Elm is different enough that things won't translate well. I wonder if they're having similar discussions about implementing stuff that's possible in Re-frame. :) |
Anyone known of an example project implementing the ideas in @LukasRychtecky's solution? Is this still a recommend way of building re-frame "complex" components? |
I have a similar, but a more complex problem. The additional complexity lies in the fact that apart from the data path, I also need to change some aspects of event handlers. 95% of all functionality is the same between all types of entities, except for the aforementioned data paths, RPC endpoints names and data validation interceptors. And that makes me wonder what would you do in my place. Currently, I'm passing |
Not sure if this helps at all but here's what I do — add a unique identifier to the first argument of the component fn and put the local state in the global Re-Frame app state: (ns whatever.something
(:require [re-frame.core :as re]
[reagent.core :as reagent]
[reagent.debug :refer [dev?]]
[reagent.impl.component :refer [react-class?]]
[reagent.interop :refer-macros [$ $!]]))
(re/reg-event-db ::gc-local-state
(fn [db [_ ident]] (update db :local-state dissoc ident)))
(defn with-local-state [component-f]
(when (dev?) (assert (fn? component-f)))
(-> (fn [& args]
(let [ident (cljs.core/random-uuid) ; `(gensym)` might be sufficient and faster but you get the point
component-ret (apply component-f ident args)]
(when (dev?) (assert (or (fn? component-ret) (react-class? component-ret))))
(if (react-class? component-ret)
(let [orig-component-will-unmount
(-> component-ret ($ :prototype) ($ :componentWillUnmount))]
(doto component-ret
(-> ($ :prototype)
($! :componentWillUnmount
(fn componentWillUnmount []
(this-as c
(when-not (nil? orig-component-will-unmount)
(.call orig-component-will-unmount c))
(re/dispatch-sync [::gc-local-state ident])))))))
(reagent/create-class
{:render component-ret
:component-will-unmount
(fn [_] (re/dispatch-sync [::gc-local-state ident]))}))))
(with-meta (meta component-f))
(doto ($! :name (.-name component-f))))) Usage: (re/reg-sub ::local-reactive-text
(fn [db [_ ident]]
(get-in db [:local-state ident :some-data])))
(re/reg-event-db ::set-local-reactive-text
(fn [db [_ ident text]]
(assoc-in db [:local-state ident :some-data] text)))
(def some-component
(with-local-state
(fn some-component [ident default-text]
(let [reactive-text (re/subscribe [::local-reactive-text ident])]
(fn []
[:div {:on-mouse-up #(re/dispatch [::set-local-reactive-text ident "reactive text has been set"])}
(or @reactive-text default-text)])))))
(defn other-component []
(fn []
[[some-component "some default text 1"]
[some-component "some default text 2"]])) |
I have implemented the following to bridge reagent cursors and re-frame (getter/setter) events. Can be combined with an
|
Here's a draft piece of documentation I'm interested in comments. What am I missing? Towards the end it references an upcoming re-frame feature called |
Another draft, substantially different to the last draft. I'm interested in comments and feedback |
Maybe a bit off-topic, but returning a function with same signature when binding subscribe (like in this example https://github.com/day8/re-frame/blob/master/docs/Navigation.md#what-about-navigation) is no more true? |
@LukasRychtecky Please take support issues to the Clojurians Slack, #re-frame channel. This is not the place. |
In the interests of moving forwards on the current re-frame sprint, I'm going to close this issue. The new re-frame website, appearing in a couple of days, will contain the page referenced by me above, and I think that resolves this issue. |
That doesn’t solve it. It will work for only two levels. Nest any deeper and you’ll quickly see problems. I think |
BranchScope is a useful shortcut for passing So, yes, it is useful for avoiding the need for "prop drilling", which is why we are going to add it, but I don't believe it will "solve" anything. I see BranchScope as complementary. |
For the record the docs page is: |
Hi! I'm new to re-frame, and i can't find what's the proper/recommended way of working with complex components.
By complex, I mean a component that besides a rendering function needs state and events. For example, here is and example from the Elm tutorial. We have a "Widget" that has a state (the counter value) and 2 buttons: increment & decrement. So, in essence i need to hold the counter value in my state, and be able to dispatch an increment and decrement event.
I have some possible answers in my head, that are similar to what i would do in js redux, but none seems to be as good as what elm proposes.
Elm is "lifting" the render function and the events, so they don't need to know about their tree path, they function as all the state tree and event namespace where their own.
The text was updated successfully, but these errors were encountered: