You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a context provider changes, we scan the tree for matching consumers
and mark them as dirty so that we know they have pending work. This
prevents us from bailing out if, say, an intermediate wrapper
is memoized.
Currently, we propagate these changes eagerly, at the provider.
However, in many cases, we would have ended up visiting the consumer
nodes anyway, as part of the normal render traversal, because there's
no memoized node in between that bails out.
We can save CPU cycles by propagating changes only when we hit a
memoized component — so, instead of propagating eagerly at the
provider, we propagate lazily if or when something bails out.
Another neat optimization is that if multiple context providers change
simultaneously, we don't need to propagate all of them; we can stop
propagating as soon as one of them matches a deep consumer. This works
even though the providers have consumers in different parts of the tree,
because we'll pick up the propagation algorithm again during the next
nested bailout.
Most of our bailout logic is centralized in
`bailoutOnAlreadyFinishedWork`, so this ended up being not that
difficult to implement correctly.
There are some exceptions: Suspense and Offscreen. Those are special
because they sometimes defer the rendering of their children to a
completely separate render cycle. In those cases, we must take extra
care to propagate *all* the context changes, not just the first one.
I'm pleasantly surprised at how little I needed to change in this
initial implementation. I was worried I'd have to use the reconciler
fork, but I ended up being able to wrap all my changes in a regular
feature flag. So, we could run an experiment in parallel to our
other ones.
I do consider this a risky rollout overall because of the potential for
subtle semantic deviations. However, the model is simple enough that I
don't expect us to have trouble fixing regressions if or when they
arise during internal dogfooding.
---
This is largely based on [RFC #118](reactjs/rfcs#118),
by @gnoff. I did deviate in some of the implementation details, though.
The main one is how I chose to track context changes. Instead of storing
a dirty flag on the stack, I added a `memoizedValue` field to the
context dependency object. Then, to check if something has changed, the
consumer compares the new context value to the old (memoized) one.
This is necessary because of Suspense and Offscreen — those components
defer work from one render into a later one. When the subtree continues
rendering, the stack from the previous render is no longer available.
But the memoized values on the dependencies list are. (Refer to the
previous commit where I implemented this as its own atomic change.)
This requires a bit more work when a consumer bails out, but nothing
considerable, and there are ways we could optimize it even further.
Concpeutally, this model is really appealing, since it matches how our
other features "reactively" detect changes — `useMemo`, `useEffect`,
`getDerivedStateFromProps`, the built-in cache, and so on.
I also intentionally dropped support for
`unstable_calculateChangedBits`. We're planning to remove this API
anyway before the next major release, in favor of context selectors.
It's an unstable feature that we never advertised; I don't think it's
seen much adoption.
Co-Authored-By: Josh Story <jcs.gnoff@gmail.com>
0 commit comments