Tracked Fibers are called "updaters" and are exposed to DevTools via a 'memoizedUpdaters' property on the ReactFiberRoot. The implementation of this feature follows a vaguely similar approach as interaction tracing, but does not require reference counting since there is no subscriptions API.
This change is in support of a new DevTools Profiler feature that shows which Fiber(s) scheduled the selected commit in the Profiler.
All changes have been gated behind a new feature flag, 'enableUpdaterTracking', which is enabled for Profiling builds by default. We also only track updaters when DevTools has been detected, to avoid doing unnecessary work.
This commit contains a proposed change to layout effect semantics within Suspense subtrees: If a component mounts within a Suspense boundary and is later hidden (because of something else suspending) React will cleanup that component’s layout effects (including React-managed refs).
This change will hopefully fix existing bugs that occur because of things like reading layout in a hidden tree and will also enable a point at which to e.g. pause videos and hide user-managed portals. After the suspended boundary resolves, React will setup the component’s layout effects again (including React-managed refs).
The scenario described above is not common. The useTransition API should ensure that Suspense does not revert to its fallback state after being mounted.
Note that these changes are primarily written in terms of the (as of yet internal) Offscreen API as we intend to provide similar effects semantics within recently shown/hidden Offscreen trees in the future. (More to follow.)
(Note that all changes in this PR are behind a new feature flag, enableSuspenseLayoutEffectSemantics, which is disabled for now.)
This flag was meant to avoid flushing discrete updates unnecessarily,
if multiple discrete events were dispatched in response to the same
platform event.
But since we now flush all discrete events at the end of the task, in
a microtask, it no longer has any effect.
* Add feature flag: enableStrongMemoryCleanup
Add a feature flag that will test doing a recursive clean of an unmount
node. This will disconnect the fiber graph making leaks less severe.
* Detach sibling pointers in old child list
When a fiber is deleted, it's still part of the previous (alternate)
parent fiber's list of children. Because children are a linked list, an
earlier sibling that's still alive will be connected to the deleted
fiber via its alternate:
live fiber
--alternate--> previous live fiber
--sibling--> deleted fiber
We can't disconnect `alternate` on nodes that haven't been deleted
yet, but we can disconnect the `sibling` and `child` pointers.
Will use this feature flag to test the memory impact.
* Combine into single enum flag
I combined `enableStrongMemoryCleanup` and `enableDetachOldChildList`
into a single enum flag. The flag has three possible values. Each level
is a superset of the previous one and performs more aggressive clean up.
We will use this to compare the memory impact of each level.
* Add Flow type to new host config method
* Re-use existing recursive clean up path
We already have a recursive loop that visits every deleted fiber. We
can re-use that one for clean up instead of adding another one.
Co-authored-by: Andrew Clark <git@andrewclark.io>
In the lazy context implementation, not all context changes are
propagated from the provider, so we can't rely on the propagation alone
to mark the consumer as dirty. The consumer needs to compare to the
previous value, like we do for state and context.
I added a `memoizedValue` field to the context dependency type. Then in
the consumer, we iterate over the current dependencies to see if
something changed. We only do this iteration after props and state has
already bailed out, so it's a relatively uncommon path, except at the
root of a changed subtree. Alternatively, we could move these
comparisons into `readContext`, but that's a much hotter path, so I
think this is an appropriate trade off.
* The exported '<React.StrictMode>' tag remains the same and opts legacy subtrees into strict mode level one ('mode == StrictModeL1'). This mode enables DEV-only double rendering, double component lifecycles, string ref warnings, legacy context warnings, etc. The primary purpose of this mode is to help detected render phase side effects. No new behavior. Roots created with experimental 'createRoot' and 'createBlockingRoot' APIs will also (for now) continue to default to strict mode level 1.
In a subsequent commit I will add support for a 'level' attribute on the '<React.StrictMode>' tag (as well as a new option supported by ). This will be the way to opt into strict mode level 2 ('mode == StrictModeL2'). This mode will enable DEV-only double invoking of effects on initial mount. This will simulate future Offscreen API semantics for trees being mounted, then hidden, and then shown again. The primary purpose of this mode is to enable applications to prepare for compatibility with the new Offscreen API (more information to follow shortly).
For now, this commit changes no public facing behavior. The only mechanism for opting into strict mode level 2 is the pre-existing 'enableDoubleInvokingEffects' feature flag (only enabled within Facebook for now).
* Renamed strict mode constants
StrictModeL1 -> StrictLegacyMode and StrictModeL2 -> StrictEffectsMode
* Renamed tests
* Split strict effects mode into two flags
One flag ('enableStrictEffects') enables strict mode level 2. It is similar to 'debugRenderPhaseSideEffectsForStrictMode' which enables srtict mode level 1.
The second flag ('createRootStrictEffectsByDefault') controls the default strict mode level for 'createRoot' trees. For now, all 'createRoot' trees remain level 1 by default. We will experiment with level 2 within Facebook.
This is a prerequisite for adding a configurable option to 'createRoot' that enables choosing a different StrictMode level than the default.
* Add StrictMode 'unstable_level' prop and createRoot 'unstable_strictModeLevel' option
New StrictMode 'unstable_level' prop allows specifying which level of strict mode to use. If no level attribute is specified, StrictLegacyMode will be used to maintain backwards compatibility. Otherwise the following is true:
* Level 0 does nothing
* Level 1 selects StrictLegacyMode
* Level 2 selects StrictEffectsMode (which includes StrictLegacyMode)
Levels can be increased with nesting (0 -> 1 -> 2) but not decreased.
This commit also adds a new 'unstable_strictModeLevel' option to the createRoot and createBatchedRoot APIs. This option can be used to override default behavior to increase or decrease the StrictMode level of the root.
A subsequent commit will add additional DEV warnings:
* If a nested StrictMode tag attempts to explicitly decrease the level
* If a level attribute changes in an update
The only difference between default updates and transition updates is
that default updates do not support suspended refreshes — they will
instantly display a fallback.
Co-authored-by: Rick Hanlon <rickhanlonii@gmail.com>
* Add the feature flag
* Add a host config method
* Wire it up to the work loop
* Export constants for third-party renderers
* Document for third-party renderers
Adds a feature flag to tweak the internal heuristic used to "unsuspend"
lanes when a new update comes in.
A lane is "suspended" if we couldn't finish rendering it because it was
missing data, and we chose not to commit the fallback. (In this context,
"suspended" does not include updates that finished with a fallback.)
When we receive new data in the form of an update, we need to retry
rendering the suspended lanes, since the new data may have unblocked the
previously suspended work. For example, the new update could navigate
back to an already loaded route.
It's impractical to retry every combination of suspended lanes, so we
need some heuristic that decides which lanes to retry and in
which order.
The existing heuristic roughly approximates the old Expiration Times
model. It unsuspends all lower priority lanes, but leaves higher
priority lanes suspended.
Then when we start rendering, we choose the lanes that have the highest
LanePriority and render those -- and then we add to that all the lanes
that are highher priority.
If this sounds terribly confusing, it's because it barely makes sense.
(It made more sense in the Expiration Times world, I promise, but it
was still confusing.) I don't think it's worth me trying to explain the
old behavior too much because the point here is that we can replace it
with something simpler.
The new heurstic is to unsuspend all suspended lanes whenever there's
an update.
This is effectively what we already do except in a few very specific
edge cases, ever since we removed the delayed suspense feature from
everything that's not a refresh transition.
We can optimize this in the future to only unsuspend lanes that are
either 1) in the `lanes` or `subtreeLanes` of the node that was updated,
or 2) in the `lanes` of the return path of the node that was updated.
This would exclude lanes that are only located in unrelated sibling
trees. But, this optimization wouldn't be useful currently because we
assign the same transition lane to all transitions. It will become
relevant again once we start assigning arbitrary lanes to transitions
-- but that in turn requires us to implement entanglement of overlapping
transitions, one of our planned projects.
So to sum up: the goal here is to remove the weird edge cases and switch
to a simpler model, on top of which we can make more substantial
improvements.
I put it behind a flag so I can run an A/B test and confirm it doesn't
cause a regression.
A passive effect's cleanup function may throw after an unmount. Prior to this commit, such an error would be ignored. (React would not notify any error boundaries.)
After this commit, React will skip any unmounted boundaries and look for a still-mounted boundary. If one is found, it will call getDerivedStateFromError and/or componentDidCatch (depending on the type of boundary). Unmounted boundaries will be ignored, but as they have been unmounted– this seems appropriate.
DevTools was built with a fork of an early idea for how Suspense cache might work. This idea is incompatible with newer APIs like `useTransition` which unfortunately prevented me from making certain UX improvements. This PR swaps out the primary usage of this cache (there are a few) in favor of the newer `unstable_getCacheForType` and `unstable_useCacheRefresh` APIs. We can go back and update the others in follow up PRs.
### Messaging changes
I've refactored the way the frontend loads component props/state/etc to hopefully make it better match the Suspense+cache model. Doing this gave up some of the small optimizations I'd added but hopefully the actual performance impact of that is minor and the overall ergonomic improvements of working with the cache API make this worth it.
The backend no longer remembers inspected paths. Instead, the frontend sends them every time and the backend sends a response with those paths. I've also added a new "force" parameter that the frontend can use to tell the backend to send a response even if the component hasn't rendered since the last time it asked. (This is used to get data for newly inspected paths.)
_Initial inspection..._
```
front | | back
| -- "inspect" (id:1, paths:[], force:true) ---------> |
| <------------------------ "inspected" (full-data) -- |
```
_1 second passes with no updates..._
```
| -- "inspect" (id:1, paths:[], force:false) --------> |
| <------------------------ "inspected" (no-change) -- |
```
_User clicks to expand a path, aka hydrate..._
```
| -- "inspect" (id:1, paths:['foo'], force:true) ----> |
| <------------------------ "inspected" (full-data) -- |
```
_1 second passes during which there is an update..._
```
| -- "inspect" (id:1, paths:['foo'], force:false) ---> |
| <----------------- "inspectedElement" (full-data) -- |
```
### Clear errors/warnings transition
Previously this meant there would be a delay after clicking the "clear" button. The UX after this change is much improved.
### Hydrating paths transition
I also added a transition to hydration (expanding "dehyrated" paths).
### Better error boundaries
I also added a lower-level error boundary in case the new suspense operation ever failed. It provides a better "retry" mechanism (select a new element) so DevTools doesn't become entirely useful. Here I'm intentionally causing an error every time I select an element.
### Improved snapshot tests
I also migrated several of the existing snapshot tests to use inline snapshots and added a new serializer for dehydrated props. Inline snapshots are easier to verify and maintain and the new serializer means dehydrated props will be formatted in a way that makes sense rather than being empty (in external snapshots) or super verbose (default inline snapshot format).
* Remove react/unstable_cache
We're probably going to make it available via the dispatcher. Let's remove this for now.
* Add readContext() to the dispatcher
On the server, it will be per-request.
On the client, there will be some way to shadow it.
For now, I provide it on the server, and throw on the client.
* Use readContext() from react-fetch
This makes it work on the server (but not on the client until we implement it there.)
Updated the test to use Server Components. Now it passes.
* Fixture: Add fetch from a Server Component
* readCache -> getCacheForType<T>
* Add React.unstable_getCacheForType
* Add a feature flag
* Fix Flow
* Add react-suspense-test-utils and port tests
* Remove extra Map lookup
* Unroll async/await because build system
* Add some error coverage and retry
* Add unstable_getCacheForType to Flight entry
## Summary
We're experiencing some issues internally where the component stack is
getting into our way of fixing them as it causes the page to become
unresponsive. This adds a flag so that we can disable this feature as a
temporary workaround.
More internal context: https://fburl.com/go9yoklm
## Test Plan
I tried to default this flag to `__VARIANT__` but the variant tests
(`yarn test-www --variant`) started to fail across the board since a lot
of tests depend on the component tree, things like this:
https://user-images.githubusercontent.com/458591/100771192-6a1e1c00-33fe-11eb-9ab0-8ff46ba378a2.png
So, it seems to work :-)
Given that it's unhandy to update the hundreds of tests that are failing
I decided to hard code this to `false` like we already do for some other
options.
This callback accepts the no parameters (except for the current interactions). Users of this hook can inspect the call stack to access and log the source location of the component.
Background:
State updates that are scheduled in a layout effect (useLayoutEffect or componentDidMount / componentDidUpdate) get processed synchronously by React before it yields to the browser to paint. This is done so that components can adjust their layout (e.g. position and size a tooltip) without any visible shifting being seen by users. This type of update is often called a "nested update" or a "cascading update".
Because they delay paint, nested updates are considered expensive and should be avoided when possible. For example, effects that do not impact layout (e.g. adding event handlers, logging impressions) can be safely deferred to the passive effect phase by using useEffect instead.
This PR updates the Profiler API to explicitly flag nested updates so they can be monitored for and avoided when possible.
Implementation:
I considered a few approaches for this.
Add a new callback (e.g. onNestedUpdateScheduled) to the Profiler that gets called when a nested updates gets scheduled.
Add an additional boolean parameter to the end of existing callbacks (e.g. wasNestedUpdate).
Update the phase param to add an additional variant: "mount", "update", or "nested-update" (new).
I think the third option makes for the best API so that's what I've implemented in this PR.
Because the Profiler API is stable, this change will need to remain behind a feature flag until v18. I've turned the feature flag on for Facebook builds though after confirming that Web Speed does not currently make use of the phase parameter.
Quirks:
One quirk about the implementation I've chosen is that errors thrown during the layout phase are also reported as nested updates. I believe this is appropriate since these errors get processed synchronously and block paint. Errors thrown during render or from within passive effects are not affected by this change.
This reverts commits bcca5a6ca7 and ffb749c95e, although neither revert cleanly since methods have been moved between the work-loop and commit-work files. This commit is a mostly manual effort of undoing the changes.
* Remove Blocks
* Remove Flight Server Runtime
There's no need for this now that the JSResource is part of the bundler
protocol. Might need something for Webpack plugin specifically later.
* Devtools
* Move traversal logic to ReactFiberCommitWork
The current traversal logic is spread between ReactFiberWorkLoop and
ReactFiberCommitWork, and it's a bit awkward, especially when
refactoring. Idk the ideal module structure, so for now I'd rather keep
it all in one file.
* Traverse commit phase effects iteratively
We suspect that using the JS stack to traverse through the tree in the
commit phase is slower than traversing iteratively.
I've kept the recursive implementation behind a flag, both so we have
the option to run an experiment comparing the two, and so we can revert
it easily later if needed.
Reading or writing a ref value during render is only safe if you are implementing the lazy initialization pattern.
Other types of reading are unsafe as the ref is a mutable source.
Other types of writing are unsafe as they are effectively side effects.
This change also refactors useTransition to no longer use a ref hook, but instead manage its own (stable) hook state.
This PR double invokes effects in __DEV__ mode.
We are thinking about unmounting layout and/or passive effects for a hidden tree. To understand potential issues with this, we want to double invoke effects. This PR changes the behavior in DEV when an effect runs from create() to create() -> destroy() -> create(). The effect cleanup function will still be called before the effect runs in both dev and prod. (Note: This change is purely for research for now as it is likely to break real code.)
**Note: The change is fully behind a flag and does not affect any of the code on npm.**
* Failing test for #19608
* Attach Listeners Eagerly to Roots and Portal Containers
* Forbid createEventHandle with custom events
We can't support this without adding more complexity. It's not clear that this is even desirable, as none of our existing use cases need custom events. This API primarily exists as a deprecation strategy for Flare, so I don't think it is important to expand its support beyond what Flare replacement code currently needs. We can later revisit it with a better understanding of the eager/lazy tradeoff but for now let's remove the inconsistency.
* Reduce risk by changing condition only under the flag
Co-authored-by: koba04 <koba0004@gmail.com>
The behavior of error boundaries for passive effects that throw during cleanup was recently changed so that React ignores boundaries which are also unmounting in favor of still-mounted boundaries. This commit implements that same behavior for layout effects (useLayoutEffect, componentWillUnmount, and ref-detachment).
The new, skip-unmounting-boundaries behavior is behind a feature flag (`skipUnmountedBoundaries`).
These stacks improve the profiler data but they're expensive to generate and generating them can also cause runtime errors in larger applications (although an exact repro has been hard to nail down). Removing them for now. We can revisit adding them after this profiler has been integrated into the DevTools extension and we can generate them lazily.