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
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
since the original written 3 years ago works fine
https://claudiopro.github.io/react-fiber-vs-stack-demo/fiber.html
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
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.
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.
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ā¦
unless I see the sierpinski triangles fast, it's not good enough š
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
š
@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.
Not sure which optimization you refer to?
https://gist.github.com/orestis/546923dd508d7de3e629fef35ad26066#file-hooks_store-cljs-L45
literally the only place the word appears in the gist? š
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.
my point exactly, what if you forget to add this "little thing"?
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.
my bad, I got confused, I thought all of this was userland
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.
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.
what's the nices react ui component library you have ever seen? š
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.
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
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 š
I wonder if their upcoming data fetching functionality has something to do with CM and caching...
afaik, historically they were implemented separately by different people
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.
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.
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
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.
btw, I still think it's impressive https://youtu.be/z-6JC0_cOns?t=1107 just that it should be separate
@smt I think that Dan is saying that for data fetching, caching is something that you want to do
I think that the React team has realized that there are different kinds of state
UI state: should be as local as possible. if state should be recreated when a component unmounts, it belongs in local state
Yeah, but they want to solve it, I am vary about this.
It should be separate
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
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.
you're connecting state to the component that owns it
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!
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
but this assumes ownership by the state, two way data binding
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.
this remains to be demonstrated, in principle there is no difference
I have a 50k line re-frame app that most of the performance problems are fixed by migrating to local state š
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
you can recreate local state inside of a global store, but you're doing a bunch of programming that isn't necessary
just like you can store everything inside of global variables, but it's often better to use local variables
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
yeah, I just do what the docs say
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
Rust makes it even MORE explicit š
> 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.
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
I think you could use useSubscription
for now
so, how do you share state between separated react apps on the same webpage?
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
I think that you can have a pretty reasonable heuristic for what should be local vs. global
you will have to wrap
I think we're still learning, but that doesn't mean it's unknowable
breaking encapsulation, and not even following the docs
"pretty reasonable" is not good enough, especially not since it's incredible to imagine anyone actually taking responsibility in such a manner/
@orestis the only potential problem I see in your store protocol is that it expects state updates to be synchronous
(-trigger-subs store old new)
and (-get-value selector)
assumes that state is synchronously coherent and can be changed immediately
that's pretty rigid IME
@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?)
My escape hatch is the ability to have multiple stores, with different schedule priorities.
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.
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 statethis way a store can batch and schedule changes
you can even share a reducer and an effector functions that like redux take the state and update it
i mean by components
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))))))))
Ah, but thatās a different concern ā Iām trying to decompose the problem.
(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!)
I have this so far (a riff on re-frameās approach, but with less moving parts and no interceptors)
Using the same backing store (an atom) you can a) subscribe to it and b) mutate it via different approaches.
It will always bottom out to swap!
though.
(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"))
Where batch
is
(defn batch [f]
(react-dom/unstable_batchedUpdates f))
I donāt really see the point of splitting those lines at the level of the store
maybe Iām not following completely
like I think that dispatching to a global router, and having those events fan out to different stores, would get really confusing
I would rather dispatch an event to a specific store, that I know will initiate a change in that store
Yes -> one atom is connected at one store (for React subscriptions) and one ārouterā (for building a better abstraction).
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 š
yeah I think thatās valuable
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
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
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
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).
I miss scroll restore. Seems like something that could be done using the history api.
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.
Well-designed component hierarchy helps in that too.