react

"mulling over state management stuff"
Aron 2020-05-06T00:31:29.375500Z

So, I tried to build this, and it works, but it's slow. If anyone wants to play around with it, I would be much grateful šŸ™‚ https://github.com/ashnur/sierpinski-cljs-fiber-demo

Aron 2020-05-06T08:12:02.376700Z

so, the legacy stuff being slow is expected, but I am guessing the experimental one is slow because I made some mistake while converting it to cljs

Aron 2020-05-06T08:12:25.377100Z

since the original written 3 years ago works fine

orestis 2020-05-06T09:59:09.378Z

I know this isnā€™t groundbreaking, and itā€™s not Concurrent Mode friendly, but how does that API look like? https://gist.github.com/orestis/546923dd508d7de3e629fef35ad26066

orestis 2020-05-06T10:00:16.379Z

The gist of it (ha!) is that given a usual atom, you can make a useStore hook that subscribes to that atom via a selector function, which re-renders your components when that value changes.

orestis 2020-05-06T10:04:11.380700Z

So thereā€™s three layers, a plain atom,, a simple ā€œstoreā€ which handles watching the atom, keeping track of subscribers, and a useStore hook that connects that store to react.

orestis 2020-05-06T10:07:38.382900Z

The useStore hook should migrate to useMutableSource eventually. Updating the atom is completely orthogonal, can be just a swap! or a more advanced event-driven approach. You can have multiple stores, pass them via context, keep them globalā€¦

Aron 2020-05-06T10:17:24.383300Z

unless I see the sierpinski triangles fast, it's not good enough šŸ˜„

Aron 2020-05-06T10:17:50.383900Z

the surface details of an API rarely bother me, I did 7 years of sitebuild, I learned over a hundred template languages, it's not like you can do worse

Aron 2020-05-06T10:17:52.384100Z

šŸ˜„

Aron 2020-05-06T10:20:32.385100Z

@orestis more seriously, wouldn't it make more sense to do this optimization at read time? that is, wherever I get my references from, should give the same references for the same values.

orestis 2020-05-06T10:41:38.385600Z

Not sure which optimization you refer to?

Aron 2020-05-06T10:45:43.386300Z

literally the only place the word appears in the gist? šŸ™‚

orestis 2020-05-06T10:46:39.387Z

Oh that code was copied-pasted -> thatā€™s just a little thing to make sure you donā€™t unsub and resub due to useEffect seeing different values in the dependencies.

Aron 2020-05-06T10:47:35.387300Z

my point exactly, what if you forget to add this "little thing"?

orestis 2020-05-06T10:49:19.388300Z

As the library author, youā€™ve done a bad job šŸ˜„ as a user, you wouldnā€™t need to know about it. But I donā€™t understand what the alternative youā€™re proposing is.

Aron 2020-05-06T11:15:38.394Z

my bad, I got confused, I thought all of this was userland

orestis 2020-05-06T12:00:20.395400Z

Ahh makes sense. No, the point was to wrap the React details under an API that looks kinda like reframe but aligning with new React.

2020-05-06T12:19:35.401500Z

Regarding Dan's Reddit response above. He writes "Suspense is designed around the need for a cache because we think it is aĀ better user experienceĀ to, for example, be able to press Back and immediately see the previous screen. Instead of waiting for components to re-fetch because you put the data in the components themselves." So are the React core team moving away from component-local state and advocate a global state that can be queried? Much like what we've had in various cljs libraries for a long time, albeit without Concurrent Mode. It feels to me like React is moving exactly the opposite direction than I am.

Aron 2020-05-06T13:13:18.402700Z

what's the nices react ui component library you have ever seen? šŸ˜„

orestis 2020-05-06T13:13:39.403100Z

Seems they will be more opinionated than in the past, I saw references in the react codebase about also adding a data fetching functionality. Not sure what to make of it yet, without any official announcement. I hope based on past behaviour that all of this would be opt-in and low-level, if you want to bring in your own opinions.

Aron 2020-05-06T13:16:07.403300Z

As I talked about this before, local state I believe is an anti-pattern for View data. Client side Applications have 2 other kinds of state beyond View data that do not suffer from this limitation and can be put in local state. I know that Dan Abramov previously has written stuff on twitter akin to "local state is fine, nothing wrong with it", so I wouldn't think they want to move away from it. On the other hand, how do you describe this if not a push towards global shared state: https://reactjs.org/docs/lifting-state-up.html

Aron 2020-05-06T13:16:56.404300Z

Some of legacy React API can only be correct if it's also slow and blocking, people who rely on this will either not use React or rewrite their components, this is just physics šŸ˜‰

2020-05-06T13:18:46.404800Z

I wonder if their upcoming data fetching functionality has something to do with CM and caching...

Aron 2020-05-06T13:19:36.405500Z

afaik, historically they were implemented separately by different people

Aron 2020-05-06T13:20:47.406700Z

I would say that CM is the algebraic thing that is the logical upgrade to React legacy, and Suspense is just another variation to manage state internally in react which is going to have the same problems as all the other attempts they tried had.

2020-05-06T13:23:04.407900Z

What kind of solutions do you see for this state management? Considering you see local state as an anti-pattern, and CM+Suspense will have problems.

Aron 2020-05-06T14:05:16.416Z

CM is independent of Suspense local state is antipattern - let me add a correction/qualification: it is perfectly fine when you don't expect to maintain the component long term and having something done quickly is important. Otherwise, as you can see in the React docs linked, it's expected for this local state management to get shared over time

Aron 2020-05-06T14:15:51.427100Z

The reason it's antipattern is that unlike most other state, your View state is read independently by at least one person and you need to adhere to strong consistency rules there, you can't just be eventually consistent in a UI, imagine adding a product in the basket but the total only eventually gets updated. The difficulty lies in the basic nature of information that you don't quite know ahead of time what state will be used with what other state together to create some effect. Maybe two totally stand-alone components with nice encapsulated states suddenly need to share data because some business imperatives require a UI effect that explicitly depends on said data? Now you either rewrite both of them together, or Lift the state, which is just a different name for rewriting šŸ™‚. I just want to have a solution where I never ever ever have to change or move state management logic if it's already doing what it is supposed to do. I want a linearly complex development process at most.

Aron 2020-05-06T14:26:04.427600Z

btw, I still think it's impressive https://youtu.be/z-6JC0_cOns?t=1107 just that it should be separate

lilactown 2020-05-06T14:52:08.428400Z

@smt I think that Dan is saying that for data fetching, caching is something that you want to do

lilactown 2020-05-06T14:54:16.429600Z

I think that the React team has realized that there are different kinds of state

lilactown 2020-05-06T14:55:14.430600Z

UI state: should be as local as possible. if state should be recreated when a component unmounts, it belongs in local state

Aron 2020-05-06T14:55:15.430700Z

Yeah, but they want to solve it, I am vary about this.

Aron 2020-05-06T14:55:21.431Z

It should be separate

lilactown 2020-05-06T14:56:33.432500Z

caching is different because it's a cross cutting concern, many components might fetch the same data on the screen at the same time, or unmount and then another component mounts that fetches the same data - not a good experience if you're always doing a network request

Aron 2020-05-06T14:56:59.432700Z

Your state never changes based on rendering alone, this is my whole point. šŸ™‚ So the idea that some state might be re-created when a component unmounts sounds like I am tying state to render. I expect my state to drive my views.

lilactown 2020-05-06T14:57:42.432900Z

you're connecting state to the component that owns it

lilactown 2020-05-06T14:58:39.433800Z

for UI state, the performance and behavior is way better when it's local state. for caching, the performance and behavior is terrible when it's local state - so we need another solution. an external store!

lilactown 2020-05-06T14:59:53.435100Z

AFAICT the React team is working on a de facto solution for this external store for caching. I am fairly certain that they are creating this as an a la carte solution, because they're also working with Relay and Next.js which are implementing their own special purpose caching stores

Aron 2020-05-06T15:03:51.435300Z

but this assumes ownership by the state, two way data binding

Aron 2020-05-06T15:07:16.435500Z

it's two way data binding, and how do you solve the Lifting state issue? you just do what the docs says? doesn't that contradict the whole idea of ownership? do we really want to maintain what piece of code owns what dynamic piece of memory? seems very inverted to me.

Aron 2020-05-06T15:08:06.435700Z

this remains to be demonstrated, in principle there is no difference

lilactown 2020-05-06T15:08:48.435900Z

I have a 50k line re-frame app that most of the performance problems are fixed by migrating to local state šŸ™‚

Aron 2020-05-06T15:09:43.436100Z

1. that's your app, not a general rule that applies to all react apps 2. not really a demonstration, how do you know that I couldn't do the same performance benefits without local states? just because that it's "a" solution, doesn't mean it's "the only" solution

lilactown 2020-05-06T15:10:22.436300Z

you can recreate local state inside of a global store, but you're doing a bunch of programming that isn't necessary

lilactown 2020-05-06T15:10:47.436500Z

just like you can store everything inside of global variables, but it's often better to use local variables

lilactown 2020-05-06T15:12:17.436700Z

a piece of state being "local" is an additional semantic on top of state. React gives that semantic to you basically for free. it's a lot of work to build it yourself, and it turns out the semantic can be applied a lot of the time

lilactown 2020-05-06T15:13:57.436900Z

yeah, I just do what the docs say

lilactown 2020-05-06T15:14:56.437100Z

at any given moment in time, UI state should be owned by the component that coordinates it. memory ownership is something that happens anyway, but React just makes it more explicit

lilactown 2020-05-06T15:15:03.437300Z

Rust makes it even MORE explicit šŸ˜„

Aron 2020-05-06T15:20:43.437500Z

> isn't necessary again, my whole point, that I repeat, but I get no reaction, is that this is something you can't know ahead of time.

Aron 2020-05-06T15:21:16.437700Z

and it's not about local vs global variables, it's about access, you can still hide it from components that don't need it

lilactown 2020-05-06T15:21:17.437900Z

I think you could use useSubscription for now

Aron 2020-05-06T15:21:59.438100Z

so, how do you share state between separated react apps on the same webpage?

Aron 2020-05-06T15:22:32.438300Z

or how do you compose rich components with local states that were written for months by different people and now you should "lift" their state but don't have 4 months to rewrite

lilactown 2020-05-06T15:22:48.438500Z

I think that you can have a pretty reasonable heuristic for what should be local vs. global

Aron 2020-05-06T15:22:49.438700Z

you will have to wrap

lilactown 2020-05-06T15:23:00.438900Z

I think we're still learning, but that doesn't mean it's unknowable

Aron 2020-05-06T15:23:03.439100Z

breaking encapsulation, and not even following the docs

Aron 2020-05-06T15:23:45.439500Z

"pretty reasonable" is not good enough, especially not since it's incredible to imagine anyone actually taking responsibility in such a manner/

lilactown 2020-05-06T15:24:46.440300Z

@orestis the only potential problem I see in your store protocol is that it expects state updates to be synchronous

lilactown 2020-05-06T15:26:38.441800Z

(-trigger-subs store old new) and (-get-value selector) assumes that state is synchronously coherent and can be changed immediately

lilactown 2020-05-06T15:26:44.442100Z

that's pretty rigid IME

orestis 2020-05-06T15:55:19.444700Z

@lilactown whatā€™s the alternative? At some point the state changes and consumers need to be updated. The only way I can think of is to wrap the state change itself with the intent (via scheduler?)

orestis 2020-05-06T15:56:31.446100Z

My escape hatch is the ability to have multiple stores, with different schedule priorities.

orestis 2020-05-06T15:59:08.448300Z

Iā€™ll be running this on experimental release with concurrent mode soon, tweaking as I go along. Iā€™ll try to recreate the original JS Iceland demos.

lilactown 2020-05-06T16:41:50.453Z

yeah, I think a more reasonable API would be something like:

(defprotocol IStore
  (-send [store compute-fn])
  (-get-value [store selector])
  (-subscribe [store selector on-change]))
now instead of synchronously setting state to be the new one, you hand the store a function to compute the next state

lilactown 2020-05-06T16:42:31.453600Z

this way a store can batch and schedule changes

Aron 2020-05-06T16:43:05.454500Z

you can even share a reducer and an effector functions that like redux take the state and update it

Aron 2020-05-06T16:43:28.455400Z

i mean by components

lilactown 2020-05-06T16:49:33.458800Z

a super naive version can just be:

(deftype AtomStore [backing]
  (-send [_ compute-fn]
    (swap! backing compute-fn))
  (-get-value [this]
    @backing)
  (-subscribe [this selector on-change]
    (let [watch-key (gensym)]
      (add-watch backing watch-key
        (fn [_ _ old new]
          (let [selector-new (selector new)]
            (when (not= (selector old) selector-new))
              (on-change selector-new))))))))

orestis 2020-05-06T17:08:40.459300Z

Ah, but thatā€™s a different concern ā€” Iā€™m trying to decompose the problem.

orestis 2020-05-06T17:09:25.459600Z

(defprotocol IDispatch
  (dispatch [this event])
  (register-event [this event handler]))

(deftype AtomDispatcher [backing handlers]
  IDispatch
  (dispatch [this event]
    (doseq [h (get @handlers event)]
      (swap! backing h)))

  (register-event [this event handler]
    (swap! handlers update event (fnil conj []) handler)))


(defn new-dispatcher [backing]
  (AtomDispatcher. backing (atom {})))


(defonce backing-store (atom {:counter 0}))
(defonce store (lstore/new-store backing-store))
(defonce events (new-dispatcher backing-store))

(defonce register-events!
  (memoize
   (fn []
     (println "registering events")
     (register-event events :inc #(update % :counter inc))
     (register-event events :dec #(update % :counter dec)))))

(register-events!)

orestis 2020-05-06T17:09:44.460100Z

I have this so far (a riff on re-frameā€™s approach, but with less moving parts and no interceptors)

orestis 2020-05-06T17:10:13.460600Z

Using the same backing store (an atom) you can a) subscribe to it and b) mutate it via different approaches.

orestis 2020-05-06T17:10:34.461Z

It will always bottom out to swap! though.

orestis 2020-05-06T17:11:08.461300Z

(defn Button []
  (react/createElement "button" #js {:onClick (fn [e]
                                                (js/setTimeout
                                                 (fn []
                                                   (batch (fn []
                                                            (dispatch events :inc)
                                                            (dispatch events :dec)
                                                            (dispatch events :inc))))
                                                 100))}
                       "Increase"))

orestis 2020-05-06T17:11:27.461600Z

Where batch is

(defn batch [f]
  (react-dom/unstable_batchedUpdates f))

lilactown 2020-05-06T17:14:38.462500Z

I donā€™t really see the point of splitting those lines at the level of the store

lilactown 2020-05-06T17:14:45.462800Z

maybe Iā€™m not following completely

lilactown 2020-05-06T17:19:24.466300Z

like I think that dispatching to a global router, and having those events fan out to different stores, would get really confusing

lilactown 2020-05-06T17:19:43.466700Z

I would rather dispatch an event to a specific store, that I know will initiate a change in that store

orestis 2020-05-06T17:42:41.468Z

Yes -> one atom is connected at one store (for React subscriptions) and one ā€œrouterā€ (for building a better abstraction).

orestis 2020-05-06T18:04:33.471300Z

So my whole thinking here is that Iā€™m trying to see if there are small piece of the puzzle that can be implemented well, then depending on each appā€™s needs you would pick and choose. Based on both the Clojure philosophy of composability and the specifics of state/values/identity (atoms, agents, etc). Perhaps this will go nowhere of course šŸ™‚

lilactown 2020-05-06T18:56:11.471500Z

yeah I think thatā€™s valuable

lilactown 2020-05-06T18:56:53.472200Z

the protocol I posted above is what I currently think the lowest common denominator w.r.t. a public API that a hook would use

lilactown 2020-05-06T18:59:23.473400Z

the perspective Iā€™m coming from, is that I would like to use something like reagentā€™s reaction/javelinā€™s cells, but that can participate in scheduling with React

lilactown 2020-05-06T19:08:19.477800Z

if you have all of these global refs sitting around, being mutated and composed together, you need to ensure that they are updated in sync. so mutations need to be controlled very tightly

2020-05-06T19:29:56.481300Z

This back button business is one of the biggest bummers of SPAs. Back in the good old days we got it for free from the browser, with scroll position restoration and all. Now it's not that easy to even figure out when the page has fully rendered (to restore scroll after that).

dominicm 2020-05-06T19:32:32.481600Z

I miss scroll restore. Seems like something that could be done using the history api.

2020-05-06T19:34:22.483400Z

Yeah, you can store state via the history api that's not visible in the url. But still, figuring out when to restore and all the nuances isn't usually easy.

2020-05-06T19:35:59.483900Z

Well-designed component hierarchy helps in that too.