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

Composing "complex" components #264

Closed
mcortesi opened this issue Nov 7, 2016 · 21 comments
Closed

Composing "complex" components #264

mcortesi opened this issue Nov 7, 2016 · 21 comments

Comments

@mcortesi
Copy link

mcortesi commented Nov 7, 2016

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.

  • How can i have 2 instances of this Counter Widget?
  • How would i route the increment/decrement events so that i target a specific widget?
  • How would i scope the render of each of them, so to get the correct counter value?

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.

  1. Use internal state (don't use the global)
  2. Pass the value and event handlers to the Counter Widget as parameters
  3. Make the counter and event handler aware of it's the 'path' in the state tree

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.

@mcortesi mcortesi changed the title How do i compose "complex" components? Composing "complex" components Nov 7, 2016
@danielcompton
Copy link
Contributor

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

@stumitchell
Copy link
Contributor

stumitchell commented Nov 7, 2016

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

@vbedegi
Copy link

vbedegi commented Nov 8, 2016

@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

@mcortesi
Copy link
Author

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.

@LukasRychtecky
Copy link

Hi there. I'm not sure if I got it right, but I solve this problem like this:

  • Every component (I mean more instances of same the component) has it's own unique ID (keyword what ever).
  • A component subscribes for the current ID and dispatches events with the ID.

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

@ericnormand
Copy link

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:

(ns my.components.counter
  (:require [re-frame.core :as rf]))

(rf/reg-event-db
  :counter/increment
  (fn [db [_ counter-id]]
    (update-in db [::counters counter-id] (fnil inc 0))))

(rf/reg-sub
  :counter/value
  (fn [db [_ counter-id]]
    (get-in db [::counters counter-id] 0)))

(defn counter-view [counter-id]
  [:span @(rf/subscribe [:counter/value counter-id])])

(defn counter-inc-button [counter-id]
  [:button {:on-click #(rf/dispatch [:counter/increment counter-id])} "+1"])

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.

@stumitchell
Copy link
Contributor

Yes this is similar to what we are thinking, but it would be nice to have a solution where the counter-id is automatically generated.

@mcortesi
Copy link
Author

mcortesi commented Aug 7, 2017

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.

@ericnormand
Copy link

Hey @mcortesi! I think I understand better now.

Besides conceptual elegance, what is the actual problem you're trying to solve?

you as a component maker need to be aware of the global prefix of your state

Why does it matter that you need to be aware of the global prefix of your state?

need to handle "garbage collecting" state when the component is destroyed/unmounted.

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.

What's nice about elm model, is that the component doesn't know it's being embebbed

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?

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.

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

@Ramblurr
Copy link

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?

@p-himik
Copy link
Contributor

p-himik commented Oct 22, 2017

I have a similar, but a more complex problem.
My application uses multiple similar dialogs for different entities - there's a dialog that shows a table of all items stored by a defined path in app-db, and there'a dialog that allows you to add new or change an existing item.

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.
While the data paths and RPC endpoints can be made parameters that are passed everywhere, data validation interceptors cannot. Well, they can, but I think it would be really ugly for a number of reasons.

And that makes me wonder what would you do in my place. Currently, I'm passing item-kind keyword everywhere and use a bunch of multimethods that resolve on this keyword to particular data paths, RPC endpoints, and validation interceptors. Another approach would be to wrap all common code into a huge macro and use it for each item kind for which you need this kind of dialogs. Yet another one would be to pass item-kind-description map instead of the item-kind keyword that would contain all relevant information.
What are your thoughts?

@alexandergunnarson
Copy link

alexandergunnarson commented Feb 11, 2018

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"]]))

@johanatan
Copy link

johanatan commented Apr 17, 2020

I have implemented the following to bridge reagent cursors and re-frame (getter/setter) events. Can be combined with an ident-managing system such as @alexandergunnarson has mentioned for truly independent local state (which would cut down on the boilerplate on the re-frame side significantly).

(ns whatever.rfcursor
  (:require
   [reagent.ratom :as ratom]
   [re-frame.core :as rf]))

;; provides a bridge between re-frame events and (legacy) ratom manipulating code.

(defn rfcursor
  ([read write] (rfcursor read identity write))
  ([read rxfm write]
   (reify
     IAtom
     ratom/IReactiveAtom

     IEquiv
     (-equiv [_ ^clj other]
       (and (= read (.-read other))
            (= write (.-write other))))

     IDeref
     (-deref [this]
       (rxfm @(rf/subscribe read)))

     IReset
     (-reset! [this new-value]
       (rf/dispatch (conj write new-value))
       new-value)

     ISwap
     (-swap! [a f] (-reset! a (f @a)))
     (-swap! [a f x] (-reset! a (f @a x)))
     (-swap! [a f x y] (-reset! a (f @a x y)))
     (-swap! [a f x y more] (-reset! a (apply f @a x y more)))

     IPrintWithWriter
     (-pr-writer [_ w opts]
       (-write w "#object[whatever.rfcursor ")
       (#'cljs.core/pr-writer read w opts)
       (#'cljs.core/pr-writer rxfm w opts)
       (#'cljs.core/pr-writer write w opts)
       (-write w "]"))

     IWatchable
     (-notify-watches [this old new] (#'reagent.ratom/notify-w this old new))
     (-add-watch [this key f]        (#'reagent.ratom/add-w this key f))
     (-remove-watch [this key]       (#'reagent.ratom/remove-w this key))

     IHash
     (-hash [_] (hash [read write])))))

@mike-thompson-day8
Copy link
Contributor

Here's a draft piece of documentation
https://gist.github.com/mike-thompson-day8/37e85576a3b6441ed7e8be7c5457cac1

I'm interested in comments. What am I missing?

Towards the end it references an upcoming re-frame feature called BranchScope which, at this stage, you can think of as a small variation on React's Context

@mike-thompson-day8
Copy link
Contributor

mike-thompson-day8 commented May 13, 2020

Another draft, substantially different to the last draft.
https://gist.github.com/mike-thompson-day8/dad5b66c8cd74082326dad6ce331128d

I'm interested in comments and feedback

@LukasRychtecky
Copy link

LukasRychtecky commented May 14, 2020

Another draft, substantially different to the last draft.
https://gist.github.com/mike-thompson-day8/dad5b66c8cd74082326dad6ce331128d

I continue to be 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?

@mike-thompson-day8
Copy link
Contributor

@LukasRychtecky Please take support issues to the Clojurians Slack, #re-frame channel. This is not the place.

@mike-thompson-day8
Copy link
Contributor

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.

@jleonard-r7
Copy link

That doesn’t solve it. It will work for only two levels. Nest any deeper and you’ll quickly see problems.

I think BranchScope sounds like a much better solution.

@mike-thompson-day8
Copy link
Contributor

mike-thompson-day8 commented May 15, 2020

BranchScope is a useful shortcut for passing id (etc.) down through layers. It is a convenience.

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.

@mike-thompson-day8
Copy link
Contributor

For the record the docs page is:
https://day8.github.io/re-frame-wip/reusable-components/

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