-
Notifications
You must be signed in to change notification settings - Fork 56
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
Unexpected change in behaviour beween React 17 and 18 #219
Comments
A classic "Diamond" problem. Only a few state managers solved it so far. Vanilla context API is free from it, because "propagation" is synchronised with "rendering", however subscription based This problem was raised to React a couple of times, and the "official" answer (link pending) is that you should not worry how much time you component got rendered, you only need to worry on how much time result of it's work was "commited" because that is expensive operation, and the rest is not. This still raises a question on how to synchronise some useEffects and run them only when rendering is "stable". I've even written an article about this 4 years ago (DejaVu, state synchronisation part) However, let's try to do a leap of fate - here is a fix for batching, and batching is the moment expected to change with R18, so multiple events would be executed together. Before the first one could be delayed, and that might be a reason for double rendering. It's just a few lines of code you can try to copy-paste into your local version of sweet-state. Please let me know if it helps
|
Indeed, the tricky part is that the Beta update is triggered during render. At that point RSS will schedule a new update, so it will be:
In the past, RSS had its own implementation of One possible workaround is to skip scheduling on some container update actions. Basically implementing a // beta store
const onUpdate =
() =>
({ setState }: StoreAPI, { input }: Props) => {
console.log('updating beta');
// ideally this can be implemented inside a nicer facade, like `flushSync(() => ...)`
defaults.batchUpdates = false;
setState({ ouput: input });
defaults.batchUpdates = true;
}; The downside is that R18 now complains: "Cannot update a component ( Thinking out loud there is another path that you could take, but requires more refactoring: stores now have a |
Thanks for the quick responses @theKashey @albertogasparin ! @theKashey I tried #218 and it doesn't actually seem to make a difference here. TBH, I don't have a good enough understanding of the internals to say anything more than that. @albertogasparin Right. I could imagine the @theKashey @albertogasparin - do you see any future for By the way, just in case it is helpful: If Thanks folks for your help, really appreciated. |
So one of the versions is to keep on shim? That could be an option?
Interesting. Lets look at this black box from aside:
Sounds like an optimisation quite related to the "Concurrent Spirit". A potential solution of the problem, in case if @obweger found a silver bullet, is to use single global store for managing subscriptions. Hello Redux and friends.
The end result will trigger more code on any random update, but cause the same number of React component updates and more importantly guarantee (we dont have any proofs yet) correctness. With less following re-renders it might even work faster by doing more things. Thoughts? |
Alternate approach - is there any need to do any optimisations on our side with React 18 auto batching?
Meaning:
|
@theKashey I think that was my hope with R18, but from my early tests it is not as aggressive as current RSS one. And that is also what BTW, I've done some more digging and seems like that the root cause is the conversion of the container from class to functional component (v2.7.0+). As part of that work I had to find a different solution for when triggering the store updates in case of a prop change. Before I was using This is a bit of a pickle... I could convert back to class component, could add a class wrapper (hello HOC) to handle this specific update case, or could say this is a React problem and @obweger has to deal with it 😅 |
Officially - the result of current render will be discarded and the new one will start. So "should be fine", but look like it's not.
|
So there is one moment we've missed a little.
Callstack leads to this line - react-sweet-state/src/components/container.js Line 140 in 0384e28
As a result of such update results of a render are getting discarded and rendered "again". This is why you are experiencing the "continuous" flow of data - it just stops on every bump. This creates a possible solution, that I would not be comfortable to implement:
Ie the goal is to first update container, then values inside it. Using given primitives, where Container can be given any arguments this is quite challenging task with every Container becoming a gatekeeper for all communications. In this terms we have two options left:
Look like this is the correction for R18 we are looking for. I was playing with #218 for quite a while, and pretty sure we dont need batching/schedule at all with React 18 autobatching.
|
That was my initial hope too. Maybe it's still the case for batching, but not for scheduling. RSS currently "holds" updates for longer (almost like startTransition) and so removing that too causes additional re-renders in places where there are close state updates.
Can you write a POC? As we will be already in render phase, so |
"Right now" RSS schedules only the first update flushing all others synchronously 😢 . And as a fix to that we can remove scheduler at all. Thanks to R18 scheduling any updates for us.
According to the code it will return value from
So if
Will point to the "current state", and it probably will - then all consumers will be in sync |
It does not work because
React is "smart" about what should re-render, so in case of prop-less children it avoids any work. So |
Right, so as "children" is not changed, and it is not changed as it's defined a level above, update propagation is stopped.
Another moment to clarify is a behavior difference between
It complains because we Look like it's ok to call This lures us to https://github.com/facebook/react/blob/c5b9375767e2c4102d7e5559d383523736f1c902/packages/react-reconciler/src/ReactFiberHooks.js#L3157 and And it seems like an answer to the question - it will just "mark" affected component to drop it's render result and re-render with new data, aka "restart". In case of external update it will just mark it for further render. So, what if we try to use What if.... |
I've played around with this for another while. Here's an update:
The latter limitation is a big bummer in case of large apps, because we are trading off a single blocking render (via So I'm currently unsure... My preference would likely be ditch the custom scheduling, ditch More info in the wild: |
According to reactwg/react-18#86 this is what was planned
This is also the major difference between
PS: Original react issue mentions the absense of
So here we are - a bunch of small stored suffering from something Redux solved a whole ago, and look like #146 is more about "Redux" way of solving the problem - moving everything outside. But still... There are 3 possible ways of working:
Switching to |
Team,
I have a setup where I'm processing data in a sequence / nesting of Sweet State containers, where the result (i.e., state) of one container is passed into the next container as a
prop
. In terms of data processing, this "sequence" is more of a directed graph, looking like e.g. this:... meaning that the output of
Alpha
is used both inBeta
, where it causes an update toGamma
and ultimatelyDelta
, and also inDelta
directly.In React 17, as well as with
defaults.batchUpdates = false
, this works "synchronously" (for the lack of a better word), meaning that whenAlpha
updates,Delta
updates only once, with the updatedAlpha
value and the updatedGamma
value.In React 18, the same setup causes
Alpha
to update twice, first with the updatedAlpha
value and the oldGamma
value, and then with both values updated.See this reproduced here: https://codesandbox.io/p/devbox/sweet-state-batching-issue-2kpdyx?file=%2Fsrc%2FApp.tsx%3A14%2C34. Open the console, and hit "Update". (The problem doesn't seem to happen on initial render.)
Mille grazie!
The text was updated successfully, but these errors were encountered: