Another thread about react and state management: (this time about performance) https://twitter.com/buildsghost/status/1255756139033051138
he's been on a roll lately
I had no clue who he is, seems like he is approaching React with a fresh mind? After 9 years of doing custom UIs in Objective-C / Mac, I feel there’s a lot of interesting ideas to bring to web dev. Not all of them practical, sure, but still interesting 😄
I know he used to work at facebook, he's participated a lot in the ecosystem surrounding React (yarn/babel/flow).
I stopped following him a year back because he was just incredibly toxic on twitter (and got banned for it once or twice), but seems he's trying to build his brand back
which I'm here for, he clearly has valuable things to share
An idea which I’m bouncing around my head lately -> a lot of the hand-wringing about global vs local state that I see around, is because global state is, a) well, global and potentially accessible from everywhere, and b) has its own special API (redux/re-frame/whatever). Local state on the other hand is clearly scoped and unceremonial (use a ratom/useState/setState) -> but the moment you want to share it with other components, you either need to send it to the global store or use a Context or prop-drilling.
The (half-baked) idea is: what if you could have multiple stores that share the same API (via say a protocol) and could be named? The API should be lean enough to be able to replace a hook, but expandable enough to handle more advanced things (subscriptions, cursors, whatever). Then the names would carry richer semantic information -> e.g. the global-store
is clearly global whereas the submission-form-store
would clearly be relevant only in your submission store.
I need to sit down and actually code a skeleton of this to see if it makes any practical sense (there’s some details to be figured out with Context and hooks etc), just thinking aloud here.
Hmmm. For me, I like local state because it's anonymous. If I decide to mount the login form twice in the page, there's no weirdness.
I was more thinking like a login form probably needs to coordinate some local state with components deep down the hierarchy, right? Where you need to go into context or prop drilling.
I was thinking - what if local state looked like global in the API?
Hmmm. Sounds like you're describing context to me, so I'm not following.
Context is a means to pass “things” down the hierarchy without needing to thread them through components, right? A kind of dynamic scope. I’m more concerned about the thing you pass down, and how do you update it.
Now I'm only thinking about:
(let [submission-form-store (react/useState {})]
(some-child submission-form-store))
Which is probably also not right 😁 Is the key aspect that instead of (react/useState)
it's something which can have cursors/minimal watchers applied?Something like that - it might use hooks under the hood but you wouldn’t care. I need to do some code to see.
Interested to see what you invent.
@orestis it would be cool if they went into a global pool, then you could do things like (swap! (:submission-form-store @pool) inc)
Nice idea - kind of my thinking is to have namespace local names too. Needs more thinking.
I guess I'm just ripping off https://github.com/riverford/objection there 😁
I think jamie’s opinion in the tweet above is spot on with mine
use specific tools for specific behavior (forms, routing/navigation, data fetching, animation) but don’t try and align with a one size fits all approach
I do wonder how much I'll care once I have fast refresh setup tbh :)
but poking atoms is more fun at the REPL
But I'm not sure his points on performance are correct. He says poking A, can cause unrelated B to update slowly (or maybe N things). But that's an easily solved problem. You can create the same dilemma in React by hoisting your state.
yes I think there are reasons that people like global state
and those reasons should be addressed, but maybe with less global state 😉
like, what if you could query for a component’s state and manipulate it at the REPL?
Redux doesn't use global state though, it's not exactly local state either.
Redux is better than re-frame here. It scopes the store to the component tree at least.
what if you had a way of getting a pic of a DAG of the way that your individual stores compose? etc.
his point about Redux needing to be redesigned to participate in React’s scheduler is also spot on, and where new tools can really shine
how amazing would it be to have a ClojureScript STM-like API to build on top of Reacts scheduler to avoid choking the page?
if your transaction takes longer than x ms (based on priority) then pause it to take care of higher priority updates, resume it later after things quiet down
I'm not sure you could do the transactions thing without a go-macro-like code rewriter to convert into a state machine
I think you would just use something like alter
ref-set!
etc.
at the low level
Do you mean a long render, or a long state change? :thinking_face:
a state change
React’s state are basically like STM refs already
they don’t have “fiber-local” values but they get scheduled and committed / rolled back just like STM
one of the problems Jamie was talking about was that Redux is this synchronous store that can’t participate in React’s scheduling
so even though your local state updates gets scheduled according to priority of the event that triggered it, Redux updates happen immediately, always, no matter what, which can exacerbate performance problems
his point is that React is smart. using it’s scheduler, it can isolate your performance problems to only the component that is slow, because it can deprioritize slow updates and let the rest of the component tree update while it waits for things to quiet down
ideally, if you are going to use an external store, your calculations to update the store would also participate in this scheduling so that it could be prioritize accordingly
but programming many external stores like this will twist your mind the same way that any other coordination of async updates to mutable variables will
That would require the ability to pause any given body of work (e.g. a long JSON.parse...) or to predict which updates would be slow.
How does React know what's slow & to give up?
yeah you can only go so fine grained due to JS
React yields after executing each component’s render to see if it’s taking too long
And if it is, it just ... waits to be slow later?
yep
How does it decide what to do next I guess?
Seems like it's missing the priority part of the puzzle
it has a queue of work
I pasted a link above which I’m basically just repeating, I don’t really know how much of this works in detail
https://clojurians.slack.com/archives/C012GLC2SAZ/p1588204080221800
AFAICT it keeps a prioritize queue of work, and after rendering each component it then checks to make sure it’s not taking too long given its priority.
if there are other higher priority updates on the queue, and it’s taking too long, it pauses rendering and puts it back on the queue and takes the higher priority work
for an STM-like thing, I’m not sure yet what the yield point would be… It would be good if you could yield in the middle of a transaction to see if there’s a higher priority item to process instead, but that’s not obvious how to do since you don’t have an obvious “and now I can pause and resume later”.
I guess you could yield every alter
or ref-set
, but that would make for a very funky API
OK, so we're not talking about component A vs B, we're talking about effects go last, state updates go first & go together, that kind of thing.
I’m not sure exactly what you’re referencing yet. yes we’re not scheduling component A vs component B necessarily
React is scheduling renders of the whole tree
so if you’ve got a websocket slamming you with split-second stock data, meanwhile your user is banging on the keyboard
Yeah. I'd misinterpreted the prioritization really. I thought it would be like "increment is fast, let those through, decrement is slow ignore those for a bit"
React is going to schedule a render of the tree according to the state update for every one of those events
and now it can prioritize the renders that are related to the keyboard banging and deprioritize the websocket state updates
Yeah. I don't see redux necessarily struggling to participate in that necessarily though :thinking_face:
What we're really talking about is global prioritization. So if you can hook the scheduler, the websocket handler can schedule the redux events to be lower in priority.
Seems like a poor man's version of this is to use a counter & setTimeout hooked up to global keyup events or something. Don't dispatch websockets when the keyup event counter/s is more than 3 or something.
yeah I think that you can get a simple strategy going pretty quickly: • make state updates async • use scheduler to prioritize and run them you at least are better than current state of the art (redux/re-frame)
but getting this really dialed in comes back to that yield point question
it still kinda sucks because you really don’t have any way of pausing or halting a slow update
there’s no natural point to check to see if there’s higher priority work to be done
Doesn't sound like react does that in the gist you linked, but I'm guessing that'll change in concurrent mode.
But that's exactly how facebook do async with php, they make a state machine and after each "state", they check if there's other work to do.
> If the deadlines are not exceeded, the renderer will render and commit them one-by-one, stopping whenever it is going to exceed its frame time budget which it receives from `scheduler`. This can happen even in the middle of processing a single batch. There is a cooperative yield point after processing each individual fiber.
yeah, each fiber. But they're restricted by the size of a fiber.
To get smaller you have to split it up
yeah but that’s way better than a whole tree!
that was my point… there’s no obvious point in the middle of a Redux reducer or re-frame event to do that to me yet
I guess what you’re saying is that is where the state machine comes in, basically you end up wrapping each computation step within the reducer in an await
that sucks
yeah, that's all that core.async/go
, await, clr, etc. do under the hood.