DevTools now 'untrack' Fibers (cleans up the ID-to-Fiber mapping) after a slight delay in order to support a Fast Refresh edge case:
1. Component type is updated and Fast Refresh schedules an update+remount.
2. flushPendingErrorsAndWarningsAfterDelay() runs, sees the old Fiber is no longer mounted (it's been disconnected by Fast Refresh), and calls untrackFiberID() to clear it from the Map.
3. React flushes pending passive effects before it runs the next render, which logs an error or warning, which causes a new ID to be generated for this Fiber.
4. DevTools now tries to unmount the old Component with the new ID.
The underlying problem here is the premature clearing of the Fiber ID, but DevTools has no way to detect that a given Fiber has been scheduled for Fast Refresh. (The '_debugNeedsRemount' flag won't necessarily be set.)
The best we can do is to delay untracking by a small amount, and give React time to process the Fast Refresh delay.
Works around the corrupted Store state by detecting a broken Fast Refresh remount and forcefully dropping the root and re-mounting the entire tree. This prevents Fibers from getting duplicated in the Store (and in the Components tree). The benefit of this approach is that it doesn't rely on an update or change in behavior to Fast Refresh. (This workaround is pretty dirty, but since it's a DEV-only code path, it's probably okay.)
Note that this change doesn't fix all of the reported issues (see #21442 (comment)) but it does fix some of them.
This commit also slightly refactors the way DevTools assigns and manages unique IDs for Fibers in the backend by removing the indirection of a "primary Fiber" and instead mapping both the primary and alternate.
It also removes the previous cache-on-read behavior of getFiberID and splits the method into three separate functions for different use cases:
* getOrGenerateFiberID – Like the previous function, this method returns an ID or generates and caches a new one if the Fiber hasn't been seen before.
* getFiberIDUnsafe – This function returns an ID if one has already been generated or null if not. (It can be used to e.g. log a message about a Fiber without potentially causing it to leak.)
* getFiberIDThrows – This function returns an ID if one has already been generated or it throws. (It can be used to guarantee expected behavior rather than to silently cause a leak.)
Previously, DevTools recursed over both children and siblings during mount. This caused potential stack overflows when there were a lot of children (e.g. a list containing many items).
Given the following example component tree:
A
B C D
E F
G
A method that recurses for every child and sibling leads to a max depth of 6:
A
A -> B
A -> B -> E
A -> B -> C
A -> B -> C -> D
A -> B -> C -> D -> F
A -> B -> C -> D -> F -> G
The stack gets deeper as the tree gets either deeper or wider.
A method that recurses for every child and iterates over siblings leads to a max depth of 4:
A
A -> B
A -> B -> E
A -> C
A -> D
A -> D -> F
A -> D -> F -> G
The stack gets deeper as the tree gets deeper but is resilient to wide trees (e.g. lists containing many items).
I recently added UI for the Profiler's commit and post-commit durations to the DevTools, but I made two pretty silly oversights:
1. I used the commit hook (called after mutation+layout effects) to read both the layout and passive effect durations. This is silly because passive effects may not have flushed yet git at this point.
2. I didn't reset the values on the HostRoot node, so they accumulated with each commit.
This commitR addresses both issues:
1. First it adds a new DevTools hook, onPostCommitRoot*, to be called after passive effects get flushed. This gives DevTools the opportunity to read passive effect durations (if the build of React being profiled supports it).
2. Second the work loop resets these durations (on the HostRoot) after calling the post-commit hook so address the accumulation problem.
I've also added a unit test to guard against this regressing in the future.
* Doing this in flushPassiveEffectsImpl seemed simplest, since there are so many places we flush passive effects. Is there any potential problem with this though?
* Remove redundant initial of isArray (#21163)
* Reapply prettier
* Type the isArray function with refinement support
This ensures that an argument gets refined just like it does if isArray is
used directly.
I'm not sure how to express with just a direct reference so I added a
function wrapper and confirmed that this does get inlined properly by
closure compiler.
* A few more
* Rename unit test to internal
This is not testing a bundle.
Co-authored-by: Behnam Mohammadi <itten@live.com>
* Bump version number
* Remove Scheduler indirection
I originally kept the React PriorityLevel and Scheduler PriorityLevel
types separate in case there was a versioning mismatch between the two
modules. However, it looks like we're going to keep the Scheduler module
private in the short to medium term, and longer term the public
interface will match postTask. So, I've removed the extra indirection
(the switch statements that convert between the two types).
* DevTools flushes updated passive warning/error info after delay
Previously this information was not flushed until the next commit, but this provides a worse user experience if the next commit is really delayed. Instead, the backend now flushes only the warning/error counts after a delay. As a safety, if there are already any pending operations in the queue, we bail.
Co-authored-by: eps1lon <silbermann.sebastian@gmail.com>
* Restore inspect-element bridge optimizations
When the new Suspense cache was integrated (so that startTransition could be used) I removed a couple of optimizations between the backend and frontend that reduced bridge traffic when e.g. dehydrated paths were inspected for elements that had not rendered since previously inspected. This commit re-adds those optimizations as well as an additional test with a bug fix that I noticed while reading the backend code.
There are two remaining TODO items as of this commit:
- Make inspected element edits and deletes also use transition API
- Don't over-eagerly refresh the cache in our ping-for-updates handler
I will addres both in subsequent commits.
* Poll for update only refreshes cache when there's an update
* Added inline comment
The memoized state of effect hooks is only invalidated when deps change. Deps are compared between the previous effect and the current effect. This can be problematic if one commit consists of an update that has changed deps followed by an update that has equal deps. That commit will lead to memoizedState containing the changed deps even though we committed with unchanged deps.
The n+1 update will therefore run an effect because we compare the updated deps with the deps with which we never actually committed.
To prevent this we now invalidate memoizedState on every updateEffectImpl call so that memoizedStat.deps always points to the latest deps.
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).
* Fixed invalid DevTools work tags
Work tags changed recently (PR #13902) but we didn't bump React versions. This meant that DevTools has valid work tags only for master (and FB www sync) but invalid work tags for the latest open source releases. To fix this, I incremneted React's version in Git (without an actual release) and added a new fork to the work tags detection branch.
This commit also adds tags for the experimental Scope and Fundamental APIs to DevTools so component names will at least display correctly. Technically these new APIs were first introduced to experimental builds ~16.9 but I didn't add a new branch to the work tags fork because I don't they're used commonly. I've just added them to the 17+ branches.
* Removed FundamentalComponent from DevTools tag defs
* 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
* Improve DevTools editing interface
This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods.
Specifically, this commit includes the following changes:
* Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys.
* Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods.
* Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths.
* Refactor 'inspected element' UI components to improve readability.
* Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.)
It also contains the following code cleanup:
* Additional unit tests have been added for modifying values as well as renaming or deleting paths.
* Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.)
* I also took this as an opportunity to refactor some of the existing code in a few places:
* Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks.
* The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition.
This commit also adds temporary support for override* commands with mismatched backend/frontend versions:
* Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use.
* Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.
* Add autofix to cross-fork lint rule
* replace-fork: Replaces old fork contents with new
For each file in the new fork, copies the contents into the
corresponding file of the old fork, replacing what was already there.
In contrast to merge-fork, which performs a three-way merge.
* Replace old fork contents with new fork
First I ran `yarn replace-fork`.
Then I ran `yarn lint` with autofix enabled. There's currently no way to
do that from the command line (we should fix that), so I had to edit the
lint script file.
* Manual fix-ups
Removes dead branches, removes prefixes from internal fields. Stuff
like that.
* Fix DevTools tests
DevTools tests only run against the old fork, which is why I didn't
catch these earlier.
There is one test that is still failing. I'm fairly certain it's related
to the layout of the Suspense fiber: we no longer conditionally wrap the
primary children. They are always wrapped in an extra fiber.
Since this has been running in www for weeks without major issues, I'll
defer fixing the remaining test to a follow up.
This commit adds a new tab to the Settings modal: Debugging
This new tab has the append component stacks feature and a new one: break on warn
This new feature adds a debugger statement into the console override
* Start MVP for showing inspected element key
* Add key in other places
* Add key from backend
* Remove unnecessary hydrateHelper call
* Hide copy button when no label
* Move above props
* Revert changes to InspectedElementTree.js
* Move key to left of component name
* Updated CSS
Co-authored-by: Brian Vaughn <brian.david.vaughn@gmail.com>
* Add LanePriority type
React's internal scheduler has more priority levels than the external
Scheduler package. Let's use React as the source of truth for tracking
the priority of updates so we have more control. We'll still fall back
to Scheduler in the default case. In the future, we should consider
removing `runWithPriority` from Scheduler and replacing the valid use
cases with React-specific APIs.
This commit adds a new type, called a LanePriority to disambiguate from
the Scheduler one.
("Lane" refers to another type that I'm planning. It roughly translates
to "thread." Each lane will have a priority associated with it.)
I'm not actually using the lane anywhere, yet. Only setting stuff up.
* Remove expiration times train model
In the old reconciler, expiration times are computed by applying an
offset to the current system time. This has the effect of increasing
the priority of updates as time progresses. Because we also use
expiration times as a kind of "thread" identifier, it turns out this
is quite limiting because we can only flush work sequentially along
the timeline.
The new model will use a bitmask to represent parallel threads that
can be worked on in any combination and in any order.
In this commit, expiration times and the linear timeline are still in
place, but they are no longer based on a timestamp. Effectively, they
are constants based on their priority level.
* Stop using ExpirationTime to represent timestamps
Follow up to the previous commit. This converts the remaining places
where we were using the ExpirationTime type to represent a timestamp,
like Suspense timeouts.
* Fork Dependencies and PendingInteractionMap types
These contain expiration times
* Make ExpirationTime an opaque type
ExpirationTime is currently just an alias for the `number` type, for a
few reasons. One is that it predates Flow's opaque type feature. Another
is that making it opaque means we have to move all our comparisons and
number math to the ExpirationTime module, and use utility functions
everywhere else.
However, this is actually what we want in the new system, because the
Lanes type that will replace ExpirationTime is a bitmask with a
particular layout, and performing operations on it will involve more
than just number comparisions and artihmetic. I don't want this logic to
spread ad hoc around the whole codebase.
The utility functions get inlined by Closure so it doesn't matter
performance-wise.
I automated most of the changes with JSCodeshift, with only a few manual
tweaks to stuff like imports. My goal was to port the logic exactly to
prevent subtle mistakes, without trying to simplify anything in the
process. I'll likely need to audit many of these sites again when I
replace them with the new type, though, especially the ones
in ReactFiberRoot.
I added the codemods I used to the `scripts` directory. I won't merge
these to master. I'll remove them in a subsequent commit. I'm only
committing them here so they show up in the PR for future reference.
I had a lot of trouble getting Flow to pass. Somehow it was not
inferring the correct type of the constants exported from the
ExpirationTime module, despite being annotated correctly.
I tried converting them them to constructor functions — `NoWork`
becomes `NoWork()` — and that made it work. I used that to unblock me,
and fixed all the other type errors. Once there were no more type
errors, I tried converting the constructors back to constants. Started
getting errors again.
Then I added a type constraint everywhere a constant was referenced.
That fixed it. I also figured out that you only have to add a constraint
when the constant is passed to another function, even if the function is
annotated. So this indicates to me that it's probably a Flow bug. I'll
file an issue with Flow.
* Delete temporary codemods used in previous commit
I only added these to the previous commit so that I can easily run it
again when rebasing. When the stack is squashed, it will be as if they
never existed.
* DevTools console override handles new component stack format
DevTools does not attempt to mimic the default browser console format for its component stacks but it does properly detect the new format for Chrome, Firefox, and Safari.
Some of our internal reconciler types have leaked into other packages.
Usually, these types are treated as opaque; we don't read and write
to its fields. This is good.
However, the type is often passed back to a reconciler method. For
example, React DOM creates a FiberRoot with `createContainer`, then
passes that root to `updateContainer`. It doesn't do anything with the
root except pass it through, but because `updateContainer` expects a
full FiberRoot, React DOM is still coupled to all its fields.
I don't know if there's an idiomatic way to handle this in Flow. Opaque
types are simlar, but those only work within a single file. AFAIK,
there's no way to use a package as the boundary for opaqueness.
The immediate problem this presents is that the reconciler refactor will
involve changes to our internal data structures. I don't want to have to
fork every single package that happens to pass through a Fiber or
FiberRoot, or access any one of its fields. So my current plan is to
share the same Flow type across both forks. The shared type will be a
superset of each implementation's type, e.g. Fiber will have both an
`expirationTime` field and a `lanes` field. The implementations will
diverge, but not the types.
To do this, I lifted the type definitions into a separate module.
* Enable prefer-const rule
Stylistically I don't like this but Closure Compiler takes advantage of
this information.
* Auto-fix lints
* Manually fix the remaining callsites
* Update Flow to 0.84
* Fix violations
* Use inexact object syntax in files from fbsource
* Fix warning extraction to use a modern parser
* Codemod inexact objects to new syntax
* Tighten types that can be exact
* Revert unintentional formatting changes from codemod
Previously, DevTools filtered empty commits on the backend, while profiling, through the use of a bailout heuristic that React currently happens to use. This approach was brittle and may have exacerbated the long-standing Profiler bug #16446.
This PR removes that heuristic and adds as a post-processing filtering pass instead. This removes the coupling between DevTools and a React internal implementation detail that may change.
I believe DevTools has two choices of criteria for this filtering:
* Filter commits that have no actual duration metadata.
* Filter commits that have no recorded operations (no mutations to the tree, no changed tree base durations).
I chose the first option, filtering by commits that have no reported metadata. It will miss an edge case, e.g. , but we would have nothing meaningful to show in the Profiler for those cases anyway. (This particular edge case is why one of the snapshots changed with this commit.)
The second option, filtering by recorded operations, could potentially miss a more important edge case: where a component *did* render, but its didn't change. (In that event, there would be no operations to send.)