Recommended way to have local state in a shadow-grove component

Hi all. I’m looking at the doc about components, in which the following dummy component is listed:

;; shouldn't have global state, but convenient for demo purposes
(defonce bad-global-atom (atom {:foo 0 :bar 0}))

(defc ui-foo []
  (bind {:keys [foo bar] :as data}
    (watch bad-global-atom))

  (bind baz
    (compute-expensive foo))
  
  (render
    (<< [:div {:on-click [::inc! :foo]} foo]
        [:div {:on-click [::inc! :bar]} bar]
        [:div baz])
  
  (event ::inc! [env e which]
    (swap! bad-global-atom update which inc))))

And my question is: agreeing that it’s not good practice to have a global atom like this; what then would be a version of this dummy component that contains its own state?

First of all a big disclaimer: I consider Local State an anti pattern that should be avoided. Your normalized DB is supposed to the only source of truth. I realize that is something a little more complicated than just using local state. I’m still thinking about cleaner ways to handle this.

That being said, you just move the atom creation into the component.

(defc ui-foo []
  (bind local-state-ref
    (atom {:foo 0 :bar 0}))

  (bind {:keys [foo bar] :as data}
    (watch local-state-ref))

  (bind baz
    (compute-expensive foo))
  
  (render
    (<< [:div {:on-click {:e ::inc! :which :foo}} foo]
        [:div {:on-click {:e ::inc! :which :bar}} bar]
        [:div baz])
  
  (event ::inc! [env {:keys [which]} e]
    (swap! local-state-ref update which inc))))

I find myself agreeing with considering local state to be an anti-pattern, but I don’t know why. Perhaps this is nothing, but I am comparing event-handling and data-handling and wondering if they can be treated very similarly. I can create a component that handles a particular error, and if it doesn’t handle it, the containing component can, and if it doesn’t, eventually the event “bubbles up” to the “registered” event handler. I like this approach to handling events, because every component can have its own jurisdiction. Doesn’t data follow a similar course, ideally? If I create a component that uses state to track its own animation, I feel that no other component cares about that, so I am leaning towards thinking that whatever state is needed to drive that animation ought to abide in that component, not in the app’s normalized db.

This is just a braindump; please feel free to disagree with or correct anything I am proposing.

Local State is definitely useful in many places. Thats one place where React Hooks excel, they compose better and you can have many small Local State instances all over the place. They just don’t perform that well and can’t really do partial/incremental updates. Everything has to run and you have to manually take care of re-running as little as possible with the help of all the other hooks.

So, for grove I wanted to explore an alternative. Less composable but full incremental updates with no manual tracking. Not yet sure if its actually better. I can just add a react hooks style implementation at any point without changing the framework. Could even make both available, need more experience building stuff with it first.

I’m still exploring all of this and collecting feedback, so it would help if you explain what you intend to do with the local state. In my own apps I’ve been putting everything into the localized db and thats working ok. Definitely more verbose than just some local state though.

Tracking animation stuff and things that don’t really belong into the DB (ie. anything DOM related) is really the space of custom attributes and custom implementations. That often means direct DOM interop. Its all a bit abstract and not something that has a formal API (yet). You are in rough terrain with this stuff. :stuck_out_tongue: