There was a comment that it's not safe to walk the unmounted fiber tree
which I'm not sure is correct or not but we need to walk the instance
tree to be able to clean up virtual instances anyway. We already walk
the instance tree to clean up "remaining instances".
This is also simpler because we don't need to special case Suspense
boundaries. We simply clean up whatever branch we had before.
The ultimate goal is to also walk the instance tree for updates so we
don't need a fiber to instance map.
## Summary
As promised on https://github.com/facebook/react/pull/29627, this
creates a unit test for the `findNodeHandle` error that prevents
developers from calling it within render methods.
## How did you test this change?
```
$ yarn test ReactFabric-test.internal.js
```
Stacked on #30625 and #30657.
This ensures that we only create instances during the commit
reconciliation and that we don't create unnecessary instances for things
that are filtered or not mounted. This ensures that we also can rely on
the reconciliation to do all the clean up. Now everything is created and
deleted as a pair in the same pass.
Previously we were including unfiltered components in the owner stack
which probably doesn't make sense since you're intending to filter them
everywhere presumably. However, it also means that those links were
broken since you can't link into owners that don't exist in the parent
tree.
The main complication is the component filters. It relied on not
unmounting the old instances. I had to update some tests that asserted
on ids that are now shifted.
For warnings/errors tracking I now restore them back into the pending
set when they unmount. Basically it puts them back into their
"pre-commit" state. That way when they remount they’re still there.
For restoring the current selection I use the tracked path mechanism
instead of relying on the id being unchanged. This is better anyway
because if you filter out the currently selected item it's better to
select the nearest match instead of just losing the selection.
Same principle as #30555. We shouldn't be throttling the UI to make it
feel less snappy. Instead, we should use back-pressure to handle it.
Normally the browser handles it automatically with frame aligned events.
E.g. if the thread can't keep up with sync updates it doesn't send each
event but the next one. E.g. pointermove or resize.
However, it is possible that we end up queuing too many events if the
frontend can't keep up but the solution to this is the same as mentioned
in #30555. I.e. to track the last message and only send after we get a
response.
I still keep the throttle to persist the selection since that affects
the disk usage and doesn't have direct UX effects.
The main motivation for this change though is that lodash throttle
doesn't rely on timers but Date.now which makes it incompatible with
most jest helpers which means I can't write tests against these
functions properly.
This no longer uses the handleCommitFiberUnmount hook to track unmounts.
Instead, we can just unmount the DevToolsInstances that we didn't reuse.
This doesn't account for cleaning up instances that were unnecessarily
created when they weren't in the tree. I have a separate follow up for
that.
This also removes the queuing of untracking. This was added in #21523
but I don't believe it has been needed for a while because the mentioned
flushPendingErrorsAndWarningsAfterDelay hasn't called untrackFiberID for
a while so the race condition doesn't exist. It's hard to tell though
because from the issues there weren't really any repros submitted.
This is the beginning of a refactor of the DevTools Fiber backend. The
new approach is basically that we listen to each commit from Fiber and
traverse the tree - building up a filtered shadow tree. Then we send
diffs based on that tree and perform our own operations against that
instead of using Fibers as the source of truth.
Fiber diffs Elements -> Fibers. The backend diffs Fibers ->
DevToolsInstances as a side-effect it sends deltas to the front end.
This makes the algorithm resilient to a different Fiber implementation
that doesn't use pairs of Fibers (alternates) but instead stateless new
clones each time. In that world we can't treat Fibers as instances. They
can hold onto instances but they're not necessarily 1:1 themselves.
The same thing also applies to Server Components that don't have their
own instances.
The algorithm is more or less the same as React's reconciliation in
ReactChildFiber itself. However, we do a mutable update of the tree as
we go. We also cheat a bit here in the first version in that we still
have fiberToFiberInstance map and alternate which makes reorders easier.
Further down we could do the reorders by adding the previous set to a
temporary map like ChildFiber does but only if they're not already in
order.
This first bit is just about making sure that we produce correct trees.
We have fairly good test coverage already of that already.
In the next few follow ups I'll start simplifying the rest of the logic
by taking advantage of the new tree.
<!--
Thanks for submitting a pull request!
We appreciate you spending the time to work on these changes. Please
provide enough information so that others can review your pull request.
The three fields below are mandatory.
Before submitting a pull request, please make sure the following is
done:
1. Fork [the repository](https://github.com/facebook/react) and create
your branch from `main`.
2. Run `yarn` in the repository root.
3. If you've fixed a bug or added code that should be tested, add tests!
4. Ensure the test suite passes (`yarn test`). Tip: `yarn test --watch
TestName` is helpful in development.
5. Run `yarn test --prod` to test in the production environment. It
supports the same options as `yarn test`.
6. If you need a debugger, run `yarn test --debug --watch TestName`,
open `chrome://inspect`, and press "Inspect".
7. Format your code with
[prettier](https://github.com/prettier/prettier) (`yarn prettier`).
8. Make sure your code lints (`yarn lint`). Tip: `yarn linc` to only
check changed files.
9. Run the [Flow](https://flowtype.org/) type checks (`yarn flow`).
10. If you haven't already, complete the CLA.
Learn more about contributing:
https://reactjs.org/docs/how-to-contribute.html
-->
## Summary
Just fixing some copy-paste typos.
## How did you test this change?
Untested.
Follow up from #30584.
You can already select a singleton or hoistable (that's not a resource)
in the browser elements panel and it'll select the corresponding node in
the RDT Components panel. That works because it uses the same mechanism
as event dispatching and those need to be able to receive events.
However, you can't select a resource. Because that's conceptually one to
many.
This keeps track of which fiber is acquiring which resource so we can
find all the corresponding instances.
E.g. now you can select the `<link rel="stylesheet">` in the Flight
fixture in the Element panel and then the component that rendered it in
the Components panel will be selected.
If we had a concept multi-selection we could potentially select all of
them. This similar to how a Server Component can be rendered in more
than one place and if we want to select all matching ones. It's kind of
weird though and both cases are edge cases.
Notably imperative preloads do have elements that don't have any
corresponding component but that's ok. So they'll just select `<head>`.
Maybe in dev we could track the owners of those.
This is just for clarity at first.
Before:
- mountFiberRecursively accepts a set of children and flag that says
whether to just do one
- updateFiberRecursively accepts a fiber and loops over its children
- unmountFiberChildrenRecursively accepts a fiber and loops over its
children
After:
- mountFiberRecursively accepts a Fiber and calls
mountChildrenRecursively
- updateFiberRecursively accepts a Fiber and calls
updateChildrenRecursively
- unmountFiberRecursively accepts a Fiber and calls
unmountChildrenRecursively
- mountChildrenRecursively accepts a set of children and loops over each
one
- updateChildrenRecursively accepts a set of children and loops over
each one
- unmountChildrenRecursively accepts a set of children and loops over
each one
So now there's one place where things happens for the single item and
one place where we do the loop.
Basically the new Float types needs to be supported. Resources are a bit
special because they're a DOM specific type but we can expect any other
implementation using resources to provide and instance on this field if
needed.
There's a slightly related case for the reverse lookup. You can already
select a singleton or hoistable (that's not a resource) in the browser
elements panel and it'll select the corresponding node in the RDT
Components panel. That works because it uses the same mechanism as event
dispatching and those need to be able to receive events.
However, you can't select a resource. Because that's conceptually one to
many. We could in principle just search the tree for the first one or
keep a map of currently mounted resources and just pick the first fiber
that created it. So that you can select a resource and see what created
it. Particularly useful when there's only one Fiber which is most of the
time.
---------
Co-authored-by: Ruslan Lesiutin <rdlesyutin@gmail.com>
Adding `__IS_NATIVE__` global, which will be used for forking backend
implementation. Will only be set to `true` for `react-devtools-core`
package, which is used by `react-native`.
Ideally, we should name it `react-devtools-native`, and keep
`react-devtools-core` as host-agnostic.
With this change, the next release of `react-devtools-core` should
append component stack as Error object, not as string, and should add
`(<anonymous>)` suffix to component stack frames.
Persistent renderers used the `Update` effect flag to check if a subtree
needs to be cloned. In some cases, that causes extra renders, such as
when a layout effect is triggered which only has an effect on the JS
side, but doesn't update the host components.
It's been a bit tricky to find the right places where this needs to be
set and I'm not 100% sure I got all the cases even though the tests
passed.
[`react-window` disables `pointerEvents` while scrolling meaning you
can't click anything while
scrolling.](https://github.com/bvaughn/react-window/issues/128).
This means that the first click when you stop the scroll with inertial
scrolling doesn't get registered. This is suuuper annoying. This might
make sense when you click to stop on a more intentional UI but it
doesn't makes sense in a list like this because we eagerly click things
even on mousedown.
This PR just override that to re-enable pointer events.
Supposedly this is done for performance but that might be outdated
knowledge. I haven't observed any difference so far.
If we discover that it's a perf problem, there's another technique we
can use where we call `ownerDocument.elementFromPoint(e.pageX, e.pageY)`
and then dispatch the event against that element. But let's try the
simplest approach first?
There's two problems. The biggest one is that it turns out that Chrome
is throttling looping timers that we're using both while polling and for
batching bridge traffic. This means that bridge traffic a lot of the
time just slows down to 1 second at a time. No wonder it feels sluggish.
The only solution is to not use timers for this.
Even when it doesn't like in Firefox the batching into 100ms still feels
too sluggish.
The fix I use is to batch using a microtask instead so we can still
batch multiple commands sent in a single event but we never artificially
slow down an interaction.
I don't think we've reevaluated this for a long time since this was in
the initial commit of DevTools to this repo. If it causes other issues
we can follow up on those.
We really shouldn't use timers for debouncing and such. In fact, React
itself recommends against it because we have a better technique with
scheduling in Concurrent Mode. The correct way to implement this in the
bridge is using a form of back-pressure where we don't keep sending
messages until we get a message back and only send the last one that
matters. E.g. when moving the cursor over a the elements tab we
shouldn't let the backend one-by-one move the DOM node to each one we
have ever passed. We should just move to the last one we're currently
hovering over. But this can't be done at the bridge layer since it
doesn't know if it's a last-one-wins or imperative operation where each
one needs to be sent. It needs to be done higher. I'm not currently
seeing any perf problems with this new approach but I'm curious on React
Native or some thing. RN might need the back-pressure approach. That can
be a follow up if we ever find a test case.
Finally, the other problem is that we use a Suspense boundary around the
Element Inspection. Suspense boundaries are for things that are expected
to take a long time to load. This shows a loading state immediately. To
avoid flashing when it ends up being fast, React throttles the reveal to
200ms. This means that we take a minimum of 200ms to show the props. The
way to show fast async data in React is using a Transition (either using
startTransition or useDeferredValue). This lets the old value remaining
in place while we're loading the next one.
We already implement this using `inspectedElementID` which is the async
one. It would be more idiomatic to implement this with useDeferredValue
rather than the reducer we have now but same principle. We were just
using the wrong ID in a few places so when it synchronously updated they
suspended. So I just made them use the inspectedElementID instead.
Then I can simply remove the Suspense boundary. Now the selection
updates in the tree view synchronously and the sidebar lags a frame or
two but it feels instant. It doesn't flash to white between which is
key.
When aborting with a postpone value in Fizz if any tasks are still
pending in the root while prerendering the prerender will fatally error.
This is different from postponing imperatively in a root task and really
the semantics should be the same. This change updates React to treat an
abort with a postpone value as a postponed root rather than a fatal
error.
This just tracks the `.parent` field properly and uses DevToolsInstances
in more places that used to use IDs or Fibers.
I also use this new parent path when looking up a DevToolsInstance from
a DOM node. This should ideally be simple because the `.parent` field
represents only the unfiltered parents and include any virtual parents.
So we should be able to just get one from nearest Fiber that has one.
However, because we don't currently always clean up the map of
DevToolsInstances (e.g. updateComponentFilters doesn't recursively clean
out everything) it can leave matches hanging that shouldn't be there. So
we need to run the shouldFilterFiber filter to ignore those.
Another interesting implication is that without a FiberInstance we don't
have a way to get to a VirtualInstance from a HostComponent. Which means
that even filtered Fibers need to have a FiberInstance if they have a
VirtualInstance parent. Even if we don't actually mount them into the
front-end.
There's a special case that happens when we replay logs on the client
because this doesn't happen within the context of any particular
rendered component. So we need to reimplement things that would normally
be handled by a full client like Fiber.
The implementation of `getOwnerStackByComponentInfoInDev` is the
simplest version since it doesn't have any client components in it so I
move it to `shared/`. It's only used by Flight but both `react-server/`
and `react-client/` packages. The ReactComponentInfo type is also more
generic than just Flight anyway.
In a follow up I still need to implement this in React DevTools when
native tasks are not available so that it appends it to the console.
Stacked on #30494 and #30491.
This is setting us up to be able to track Server Components. This is now
split into a FiberInstance (Client Component) and a VirtualInstance
(Server Component). We're not actually creating any VirtualInstances yet
though this is just the data structures.
Server Components and potentially other compiled away or runtime
optimized away (e.g. calling through a function without creating an
intermediate Fiber) don't have a stateful instance. They just represent
the latest data. It's kind of like a React Element.
However, in the DevTools UI we need them to be stateful partly just so
that you can select and refer to them separately. E.g. the same Server
Component output rendered into different slots on the client should
still have two different representations in the DevTools. Also if the
same child Fibers update in place because the Server Component refreshed
we shouldn't lose the selection if you've selected a Server Component.
I'll implement this by creating Virtual Instances that only exist for
the purpose of the DevTools UI and so it'll be implemented in the
DevTools.
We could just make a Map from `id` to `Fiber | ReactComponentInfo` but
that requires a branching without a consistent hidden class. We also
need some more states on there. We also have some other Maps that tracks
extra states like that of component stacks, errors and warnings.
Constantly resizing and adding/removing from a Map isn't exactly fast.
It's faster to have a single Map with an object in it than one Map per
object. However, having extra fields that are usually just `null` can
instead mean more memory gets used. Since only a very small fraction of
instances will have errors/warnings or having initialized its component
stack, it made sense to store that in a separate Map that is usually
just empty.
However, with the addition of particularly the `parent` field and the
ability to do a fast hidden-class safe branching on the `kind` field I
think it's probably worth actually allocating an extra first class
object per Instance to store DevTools state into. That's why I converted
from just storing `Fiber` -> `id` to storing `Fiber` ->
`DevToolsInstance` which then keeps the warnings/errors/componentStack
as extra fields that are usually `null`. That is a lot of objects though
since it's one per Fiber pair basically.
We use static dependency injection. We shouldn't use this dynamic
dependency injection we do for DevTools internals. There's also meta
programming like spreading and stuff that isn't needed.
This moves the config from `injectIntoDevTools` to the FiberConfig so it
can be statically resolved.
Closure Compiler has some trouble generating optimal code for this
anyway so ideally we'd refactor this further but at least this is better
and saves a few bytes and avoids some code paths (when minified).
Stacked on #30491.
When going from DOM Node to select a component or highlight a component
we find the nearest mounted ancestor. However, when multiple renderers
are nested there can be multiple ancestors. The original fix#24665 did
this by taking the inner renderer if it was an exact match but if it
wasn't it just took the first renderer.
Instead, we can track the inner most node we've found so far. Then get
the ID from that node (which will be fast since it's now a perfect
match). This is a better match.
However, the main reason I'm doing this is because the old mechanism
leaked the `Fiber` type outside the `RendererInterface` which is
supposed to abstract all of that. With the new algorithm this doesn't
leak.
I've tested this with a new build against the repro in the old issue
#24539 and it seems to work.
Stacked on #30490.
This is in the same spirit but to clarify the difference between what is
React Native vs part of any generic Host. We used to use "Native" to
mean three different concepts. Now "Native" just means React Native.
E.g. from the frontend's perspective the Host can be
Highlighted/Inspected. However, that in turn can then be implemented as
either direct DOM manipulation or commands to React Native. So frontend
-> backend is "Host" but backend -> React Native is "Native" while
backend -> DOM is "Web".
Rename NativeElementsPanel to BuiltinElementsPanel. This isn't a React
Native panel but one part of the surrounding DevTools. We refer to Host
more as the thing running React itself. I.e. where the backend lives.
The runtime you're inspecting. The DevTools itself needs a third term.
So I went with "Builtin".
This is not used by DevTools since it has its own implementation of it.
This function is getting removed since `findDOMNode` is getting removed
so we shouldn't keep around extra bytes unnecessarily.
There is also `findHostInstancesForRefresh` which should really be
implemented on the `react-refresh` side. Not using an injection but
that's a heavier lift and only affects `__DEV__`.
There is currently a mismatch in how the persistent mode JS API and the
Fabric native code interpret `completeRoot`.
This is a short-lived experiment to see the effect of moving the Fabric
`completeRoot` call from `finalizeContainerChildren` to
`replaceContainerChildren` which in some cases does not get called.
Currently if you abort a Fizz render during rendering the render will
not complete correctly because there are inconsistencies with task
counting. This change updates the abort implementation to allow you to
abort from within a render itself. We already landed a similar change
for Flight in #29764
I need to start clarifying where things are really actually Fibers and
where they're not since I'm adding Server Components as a separate type
of component instance which is not backed by a Fiber.
Nothing in the front end should really know anything about what kind of
renderer implementation we're inspecting and indeed it's already not
always a "Fiber" in the legacy renderer.
We typically refer to this as a "Component Instance" but the front end
currently refers to it as an Element as it historically grew from the
browser DevTools Elements tab.
I also moved the renderer.js implementation into the `backend/fiber`
folder. These are at the same level as `backend/legacy`. This clarifies
that anything outside of this folder ideally shouldn't refer to a
"Fiber".
console.js and profilingHooks.js unfortunately use Fibers a lot which
needs further refactoring. The profiler frontend also uses the term
alot.
When a Fizz render is closing but not yet closed it's possible that
pinged tasks can spawn more work. The point of the closing state is to
allow time to start piping/reading the underlying stream but
semantically the render is finished at that point so work should no
longer happen.
When prerendering it can be convenient to abort the prerender while
rendering. However if any Suspense fallbacks have not yet rendered
before the abort happens the fallback itself will error and cause the
nearest parent Suspense boundary to render a fallback instead.
Prerenders are by definition not time critical so the prioritization of
children over fallbacks which makes sense for render isn't similarly
motivated for prerender. Given this, this change updates fallback
rendering during a prerender to attempt the fallback before attempting
children.
**This API is not intended to ship. This is a temporary unstable hook
for internal performance profiling.**
This PR exposes `unstable_useContextWithBailout`, which takes a compare
function in addition to Context. The comparison function is run to
determine if Context propagation and render should bail out earlier.
`unstable_useContextWithBailout` returns the full Context value, same as
`useContext`.
We can profile this API against `useContext` to better measure the cost
of Context value updates and gather more data around propagation and
render performance.
The bailout logic and test cases are based on
https://github.com/facebook/react/pull/20646
Additionally, this implementation allows multiple values to be compared
in one hook by returning a tuple to avoid requiring additional Context
consumer hooks.
In https://github.com/facebook/react/pull/23316 we fixed a bug where
onload events were missed if they happened too early. This update adds
support for srcset to retrigger the load event. Firefox unfortunately
does not trigger a load even when you assign srcset so this won't work
in every browser when you use srcset without src however it does close a
gap in chrome at least
This is a major nit but this avoids an extra stack frame when we're
replaying logs.
Normally the `printToConsole` frame doesn't show up because it'd be
ignore listed.
<img width="421" alt="Screenshot 2024-07-25 at 11 49 39 AM"
src="https://github.com/user-attachments/assets/81334c2f-e19e-476a-871e-c4db9dee294e">
When you expand to show ignore listed frames a ton of other frames show
up.
<img width="516" alt="Screenshot 2024-07-25 at 11 49 47 AM"
src="https://github.com/user-attachments/assets/2ab8bdfb-464c-408d-9176-ee2fabc114b6">
The annoying thing about this frame is that it's at the top of the stack
where as typically framework stuff ends up at the bottom and something
you can ignore. The user space stack comes first.
With this fix there's no longer any `printToConsole` frame.
<img width="590" alt="Screenshot 2024-07-25 at 12 09 09 PM"
src="https://github.com/user-attachments/assets/b8365d53-31f3-43df-abce-172d608d3c9c">
Am I wiling to eat the added complexity and slightly slower performance
for this nit? Definitely.
Following https://github.com/facebook/react/pull/30436
Concurrent by default strategy has been unshipped. Here we clean up the
`allowConcurrentByDefault` path and related logic/tests.
For now, this keeps the `concurrentUpdatesByDefaultOverride` argument in
`createContainer` and `createHydrationContainer` and ignores the value
to prevent more breaking changes to `react-reconciler` in the RC stage.
This enables configuring the name of the requested environment.
When we currently use createTask, we start with a `"use server"`
annotation. This option basically configures that string.
I now also deal with the case when switching environments along the
owner path. If you go from `"Third Party"` to `"Server"` to `"Client"`,
it'll have a task named `"use third party"` at the root, then `"use
server"` and then finally `"use client"`.
We don't really have the concept of a Server Component making a request
during render to then create another Server Component. Really the inner
one should conceptually have the first one as its owner in that case. So
currently the inner one will always have a null owner. We could somehow
connect them in this server-to-server case.
We don't currently have a way to configure the `"use client"` option but
I figured maybe that could be inferred by the server environment that
the Flight Client is executed within.
Note: We did talk before about annotating each stack frame with the
environment. You can effectively do that manually when parsing
`rsc://React/{environment}/` from `captureOwnerStack`. However, we can't
do that natively. At least not without deeper integration. Because it's
the source map that's responsible for the actual function name of each
stack frame - not what we give it at runtime. So for the native stacks,
the task showing the change in environment is more practical.
This way you can use the environment to know where to look for the
source map in case you have multiple server environments.
This becomes part of the public protocol since it's part of what you'll
parse out of the `rsc://React/` prefixed URLs inside of
`captureOwnerStack`.
This lets you customize the filter, for example allowing node_modules or
filter out additional functions that you don't want to include when
sending the stack to the client.
Notably this doesn't filter out Server Components out of the parent
stack. Those are just like a view of the tree by name. Not virtual stack
frames.
When a model references a deduped object of a blocked element that has
subsequently been turned into a lazy element, we need to wait for the
lazy element's chunk to resolve before resolving the reference.
Without the fix, the new test failed with the following runtime error:
```
TypeError: Cannot read properties of undefined (reading 'children')
1003 | let value = chunk.value;
1004 | for (let i = 1; i < path.length; i++) {
> 1005 | value = value[path[i]];
| ^
1006 | }
1007 | const chunkValue = map(response, value);
1008 | if (__DEV__ && chunk._debugInfo) {
at getOutlinedModel (packages/react-client/src/ReactFlightClient.js:1005:26)
```
The bug was uncovered after updating React in Next.js in
https://github.com/vercel/next.js/pull/66711.
We still filter them before passing from server to client in Flight
Server but when presenting a native stack, we don't need to filter them.
That's left to ignore listing in the presentation.
The stacks are pretty clean regardless thanks to the bottom stack
frames.
We can also unify the owner stack formatters into one shared module
since Fizz/Flight/Fiber all do the same thing. DevTools currently does
the same thing but is forked so it can support multiple versions.
Concurrent by default has been unshipped! Let's clean it up.
Here we remove `forceConcurrentByDefaultForTesting`, which allows us to
run tests against both concurrent strategies. In the next PR, we'll
remove the actual concurrent by default code path.