Commit Graph

421 Commits

Author SHA1 Message Date
Sebastian Markbåge
eb89912ee5 Add expertimental optimisticKey behind a flag (#35162)
When dealing with optimistic state, a common problem is not knowing the
id of the thing we're waiting on. Items in lists need keys (and single
items should often have keys too to reset their state). As a result you
have to generate fake keys. It's a pain to manage those and when the
real item comes in, you often end up rendering that with a different
`key` which resets the state of the component tree. That in turns works
against the grain of React and a lot of negatives fall out of it.

This adds a special `optimisticKey` symbol that can be used in place of
a `string` key.

```js
import {optimisticKey} from 'react';
...
const [optimisticItems, setOptimisticItems] = useOptimistic([]);
const children = savedItems.concat(
  optimisticItems.map(item =>
    <Item key={optimisticKey} item={item} />
  )
);
return <div>{children}</div>;
```

The semantics of this `optimisticKey` is that the assumption is that the
newly saved item will be rendered in the same slot as the previous
optimistic items. State is transferred into whatever real key ends up in
the same slot.

This might lead to some incorrect transferring of state in some cases
where things don't end up lining up - but it's worth it for simplicity
in many cases since dealing with true matching of optimistic state is
often very complex for something that only lasts a blink of an eye.

If a new item matches a `key` elsewhere in the set, then that's favored
over reconciling against the old slot.

One quirk with the current algorithm is if the `savedItems` has items
removed, then the slots won't line up by index anymore and will be
skewed. We might be able to add something where the optimistic set is
always reconciled against the end. However, it's probably better to just
assume that the set will line up perfectly and otherwise it's just best
effort that can lead to weird artifacts.

An `optimisticKey` will match itself for updates to the same slot, but
it will not match any existing slot that is not an `optimisticKey`. So
it's not an `any`, which I originally called it, because it doesn't
match existing real keys against new optimistic keys. Only one
direction.
2025-11-18 16:29:18 -05:00
Sebastian "Sebbie" Silbermann
1ea46df8ba [DevTools] Batch updates when updating component filters (#35093) 2025-11-11 23:20:22 +01:00
Sebastian "Sebbie" Silbermann
8c15edd57c [DevTools] Send root unmount as a regular removal operation (#35107) 2025-11-11 23:08:54 +01:00
Sebastian "Sebbie" Silbermann
ce4054ebdd [DevTools] Measure when reconnecting Suspense (#35098) 2025-11-10 20:55:31 +01:00
Sebastian "Sebbie" Silbermann
5a9921b839 [DevTools] Apply Activity slice filter when double clicking Activity (#34908) 2025-11-08 18:09:44 +01:00
Sebastian "Sebbie" Silbermann
c9ddee7e36 [DevTools] Reset forced states when changing component filters (#34929) 2025-10-31 12:57:11 +01:00
Sebastian Markbåge
3a0ab8a7ee [DevTools] Synchronize Scroll Position Between Suspense Tab and Main Document (#34641)
It's annoying to have to try to find where it lines up with no hints.

This way when you hover over something it should be on screen.

The strategy I went with is that it scrolls to a percentage along the
scrollable axis but the two might not be exactly the same. Partially
because they have different aspect ratios but also because suspended
boundaries can shrink the document while the suspense tab needs to still
be able to show the boundaries that are currently invisible.
2025-10-29 21:49:35 -04:00
Sebastian Markbåge
0a5fb67ddf [DevTools] Sort suspense timeline by end time instead of just document order (#35011)
Right now it's possible for things like server environments to appear
before other content in the timeline just because it's in a different
document order.

Ofc the order in production is not guaranteed but we can at least use
the timing information we have as a hint towards the actual order.

Unfortunately since the end time of the RSC stream itself is always
after the content that resolved to produce it, it becomes kind of
determined by the chunking. Similarly since for a clean refresh, the
scripts and styles will typically load after the server content they
appear later. Similarly SSR typically finishes after the RSC parts.
Therefore a hack here is that I artificially delay everything with a
non-null environment (RSC) so that RSC always comes after client-side
(Suspense). This is also consistent with how we color things that have
an environment even if children are just Suspense.

To ensure that we never show a child before a parent, in the timeline,
each child has a minimum time of its parent.
2025-10-29 15:05:04 -04:00
Błażej Kustra
39c6545cef Fix indices of hooks in devtools when using useSyncExternalStore (#34547)
## Summary

This PR updates getChangedHooksIndices to account for the fact that
useSyncExternalStore internally mounts two hooks, while DevTools should
treat it as a single user-facing hook.

It introduces a helper isUseSyncExternalStoreHook to detect this case
and adjust iteration so the extra internal hook is skipped when counting
changes.

Before:


https://github.com/user-attachments/assets/0db72a4e-21f7-44c7-ba02-669a272631e5

After:


https://github.com/user-attachments/assets/4da71392-0396-408d-86a7-6fbc82d8c4f5

## How did you test this change?

I used this component to reproduce this issue locally (I followed
instructions in `packages/react-devtools/CONTRIBUTING.md`).

```ts
function Test() {
  // 1
  React.useSyncExternalStore(
    () => {},
    () => {},
    () => {},
  );
  // 2
  const [state, setState] = useState('test'); 
  return (
    <>
      <div
        onClick={() => setState(Math.random())}
        style={{backgroundColor: 'red'}}>
        {state}
      </div>
    </>
  );
}
```
2025-10-21 13:59:20 +01:00
Sebastian Markbåge
21272a680f Lower case "rsc stream" debug info (#34921)
This is an aesthetic thing. Most simple I/O entries are things like
"script", "stylesheet", "fetch" etc. which are all a single word and
lower case. The "RSC stream" name sticks out and draws unnecessary
attention to itself where as it's really the least interesting to look
at.

I don't love the name because I'm not sure how to explain it. It's
really mainly the byte size of the payload itself without considering
things like server awaits things which will have their own cause. So I'm
trying to communicate the download size of the stream of downloading the
`.rsc` file or the `"rsc stream"`.
2025-10-20 02:42:38 -04:00
Sebastian "Sebbie" Silbermann
b485f7cf64 [DevTools] Don't attach filtered IO to grandparent Suspense (#34916) 2025-10-20 00:47:27 +02:00
Sebastian "Sebbie" Silbermann
40c7a7f6ca [DevTools] Use same Suspense naming heuristics when reconnecting (#34898) 2025-10-18 12:54:05 +02:00
Sebastian Markbåge
03ba0c76e1 [DevTools] Include some sub-pixel precision in rects (#34873)
Currently the sub-pixel precision is lost which can lead to things not
lining up properly and being slightly off or overlapping.

We need some sub-pixel precision.

Ideally we'd just keep the floating point as is. I'm not sure why the
operations is limited to integers. We don't send it as a typed array
anyway it seems which would ideally be more optimal. Even if we did, we
haven't defined a precision for the protocol. Is it 32bit integer?
64bit? If it's 64bit we can fit a float anyway. Ideally it would be more
variable precision like just pushing into a typed array directly with
the option to write whatever precision we want.
2025-10-16 10:50:41 -04:00
Sebastian Markbåge
7385d1f61a [DevTools] Add inspection button to Suspense tab (#34867)
Add inspection button to Suspense tab which lets you select only among
Suspense nodes. It highlights all the DOM nodes in the root of the
Suspense node instead of just the DOM element you hover. The name is
inferred.

<img width="1172" height="841" alt="Screenshot 2025-10-15 at 8 03 34 PM"
src="https://github.com/user-attachments/assets/f04d965b-ef6e-4196-9ba0-51626148fa1a"
/>
2025-10-16 10:49:23 -04:00
Sebastian Markbåge
903366b8b1 [DevTools] Don't select on hover (#34860)
We should only persist a selection once you click. Currently, we persist
the selection if you just hover which means you lose your selection
immediately when just starting to inspect. That's not what Chrome
Elements tab does - it selects on click.
2025-10-15 13:43:55 -04:00
Sebastian Markbåge
6773248311 [DevTools] Track whether a boundary is currently suspended and make transparent (#34853)
This makes the rects that are currently in a suspended state appear
ghostly so that you can see where along the timeline you are in the
rects screen.

<img width="451" height="407" alt="Screenshot 2025-10-14 at 11 43 20 PM"
src="https://github.com/user-attachments/assets/f89e362b-a0d5-46e3-8171-564909715cd1"
/>
2025-10-15 10:26:07 -04:00
Sebastian Markbåge
751edd6e2c [DevTools] Measure text nodes (#34851)
We can't measure Text nodes directly but we can measure a Range around
them.

This is useful since it's common, at least in examples, to use text
nodes as children of a Suspense boundary. Especially fallbacks.
2025-10-15 10:24:45 -04:00
Sebastian Markbåge
6cfc9c1ff3 [DevTools] Don't measure fallbacks when suspended (#34850)
We already do this in the update pass. That's what
`shouldMeasureSuspenseNode` does.

We also don't update measurements when we're inside an offscreen tree.

However, we didn't check if the boundary itself was in a suspended state
when in the `measureUnchangedSuspenseNodesRecursively` path.

This caused boundaries to disappear when their fallback didn't have a
rect (including their timeline entries).
2025-10-15 10:12:26 -04:00
Sebastian "Sebbie" Silbermann
73507ec457 [DevTools] Exclude Suspense boundaries in hidden Activity (#34756) 2025-10-14 13:57:08 +02:00
Sebastian Markbåge
c825f03067 [DevTools] Hide State and Props in the Sidebar for Suspense (#34630)
We're showing too much noise in the side-panel when selecting a Suspense
boundary. The interesting thing to see directly is the "Suspended by".

The "props" are mostly useless because the `"name"` prop is already in
the tree. I'm now also showing it in the title bar of the selected
element panel. The "children" and "fallback" props are just the thing
that you can see in the tree view anyway.

The "state" is this weird section with just one field in it, which we
already have duplicated in the top toolbar as well. We can just delete
this. I make sure to show the icon and a "suspended..." section while
the boundary is still loading but now yet resuspended by force
suspending.

While still loading:

<img width="600" height="193" alt="Screenshot 2025-09-27 at 11 54 37 PM"
src="https://github.com/user-attachments/assets/1c3f3a96-46e0-4b11-806f-032569c7d5b5"
/>

After loading:

<img width="602" height="266" alt="Screenshot 2025-09-27 at 11 54 53 PM"
src="https://github.com/user-attachments/assets/c43cc4cb-036f-4ced-9b0d-226c6320cd76"
/>

Resuspended after loading:

<img width="602" height="300" alt="Screenshot 2025-09-27 at 11 55 07 PM"
src="https://github.com/user-attachments/assets/0be01735-48a7-47dc-b5cf-e72ec71e0148"
/>
2025-10-02 15:18:41 -04:00
Sebastian "Sebbie" Silbermann
4a28227960 [DevTools] Inspect the Initial Paint when inspecting a Root (#34454) 2025-10-02 19:18:15 +02:00
Sebastian "Sebbie" Silbermann
e4a27db283 [DevTools] Defer Suspense tab to 19.3.0-canary (#34688) 2025-10-02 19:13:52 +02:00
Sebastian "Sebbie" Silbermann
bc828bf6e3 [DevTools] Recommend React Performance tracks if supported when Timeline profiler is not supported (#34684) 2025-10-02 18:33:50 +02:00
Sebastian "Sebbie" Silbermann
1f460f31ee [DevTools] Fix host instance highlighting (#34661) 2025-10-01 17:32:34 +02:00
Sebastian Markbåge
554a373d7e [DevTools] Use the scrollWidth/Height for the root when the root is the documentElement (#34651)
If I can scroll the document due to it overflowing, I should be able to
scroll the suspense tab as much. The real rect for the root when it's
the document is really the full scroll height.

This doesn't fully eliminate the need to do recursive bounding boxes for
the root since it's still possible to have the rects overflow. E.g. if
they're currently resuspended or inside nested scrolls.

~However, maybe we should have the actual paintable root rect just be
this rectangle instead of including the recursive ones.~ Actually never
mind. The root really represents the Transition so it doesn't make sense
to give it any specific rectangle. It's rather the whole background.
2025-09-30 14:37:40 -04:00
Sebastian Markbåge
5dd163b49e [DevTools] Auto-scroll when stepping through the timeline (#34653)
This brings the Suspense boundary that's switching into view so that
when you play the loading sequence you can see how it plays out.
Otherwise it's really hard to find where things are changing.

This assumes we'll also scroll synchronize the suspense tab which will
bring it into view there too.
2025-09-30 14:37:14 -04:00
Sebastian "Sebbie" Silbermann
8674c3ba28 [DevTools] Enable Suspense tab for Canary releases (#34591) 2025-09-29 16:23:30 +02:00
Sebastian Markbåge
8309724cb4 [Fiber][DevTools] Add scheduleRetry to DevTools Hook (#34635)
When forcing suspense/error we're doing that by scheduling a sync update
on the fiber. Resuspending a Suspense boundary can only happen sync
update so that makes sense. Erroring also forces a sync commit. This
means that no View Transitions fire.

However, unsuspending (and dismissing an error dialog) can be async so
the reveal should be able to be async.

This adds another hook for scheduling using the Retry lane. That way
when you play through a reveal sequence of Suspense boundaries (like
playing through the timeline), it'll run the animations that would've
ran during a loading sequence.
2025-09-28 13:51:35 -04:00
Sebastian Markbåge
6a51a9fea6 [DevTools] Track Server Environment Names of Each SuspenseNode (#34605)
Tracks the environment names of the I/O in each SuspenseNode and sent it
to the front end when the suspenders change.

In the front end, every child boundary should really be treated as it
has all environment names of the parents too since they're blocked by
the parent too. We could do this tracking on backend but if there's ever
one added on the root would need to be send for every child.

This lets us highlight which subtrees are blocked by content on the
server.

---------

Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>
2025-09-26 09:43:38 -04:00
Sebastian "Sebbie" Silbermann
c44fbf43b1 [DevTools] Fix instrumentation error when reconciling promise-as-a-child (#34587) 2025-09-24 22:50:12 +02:00
Sebastian "Sebbie" Silbermann
012b371cde [DevTools] Handle LegacyHidden Fibers like Offscreen Fibers. (#34564) 2025-09-23 20:14:53 +02:00
Sebastian "Sebbie" Silbermann
03a96c75db [DevTools] Record Suspense node for roots in legacy renderers (#34516) 2025-09-18 18:50:23 +02:00
Sebastian "Sebbie" Silbermann
a51f925217 [DevTools] Only check if we previously removed IO if its removal failed (#34506) 2025-09-16 19:55:03 +02:00
Sebastian "Sebbie" Silbermann
941cd803a7 [DevTools] Don't keep stale root instances we never mounted around (#34504) 2025-09-16 19:17:28 +02:00
Sebastian "Sebbie" Silbermann
851bad0c88 [DevTools] Ignore repeated removals of the same IO (#34495) 2025-09-16 18:54:52 +02:00
Sebastian "Sebbie" Silbermann
5502d85cc7 [DevTools] Unmount fallbacks in the context of the parent Suspense (#34475)
Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
2025-09-13 11:03:32 +02:00
Sebastian "Sebbie" Silbermann
a9ad64c852 [DevTools] Stop mounting empty roots (#34467) 2025-09-11 20:00:53 +02:00
Sebastian "Sebbie" Silbermann
7fc888dde2 [DevTools] Stop recording reorders in disconnected subtrees (#34464) 2025-09-11 19:13:14 +02:00
Sebastian "Sebbie" Silbermann
b1c519f3d4 [DevTools] Only show boundaries with unique suspenders by default in the timeline (#34397) 2025-09-11 11:33:05 +02:00
Sebastian Markbåge
886b3d36d7 [DevTools] Show suspended by subtree from Activity to next Suspense boundary (#34438)
Stacked on #34435.

This adds a method to get all suspended by filtered by a specific
Instance. The purpose of this is to power the feature when you filter by
Activity. This would show you the "root" within that Activity boundary.

This works by selecting the nearest Suspense boundary parent and then
filtering its data based on if all the instances for a given I/O info is
within the Activity instance. If something suspended within the Suspense
boundary but outside the Activity it's not included even if it's also
suspending inside the Activity since we assume it would've already been
loaded then.

Right now I wire this up to be a special case when you select an
Activity boundary same as when you select a Suspense boundary in the
Components tab but we could also only use this when you select the root
in the Suspense tab for example.
2025-09-10 09:44:51 -04:00
Sebastian Markbåge
288d428af1 [DevTools] Only show the highest end/byteSize I/O of RSC streams (#34435)
Stacked on #34425.

RSC stream info is split into one I/O entry per chunk. This means that
when a single instance or boundary depends on multiple chunks, it'll
show the same stream multiple times. This makes it so just the last one
is shown.

This is a special case for the name "RSC stream" but ideally we'd more
explicitly model the concept of awaiting only part of a stream.

<img width="667" height="427" alt="Screenshot 2025-09-09 at 2 09 43 PM"
src="https://github.com/user-attachments/assets/890f6f61-4657-4ca9-82fd-df55a696bacc"
/>

Another remaining issue is that it's possible for an intermediate chunk
to be depended on by just a child boundary. In that case that can be
considered a "unique suspender" even though the parent depends on a
later one. Ideally it would dedupe on everything below. Could also model
it as every Promise depends on its chunk and every previous chunk.
2025-09-10 09:08:36 -04:00
Sebastian "Sebbie" Silbermann
8943025358 [DevTools] Fix handling of host roots on mount (#34400) 2025-09-08 22:53:02 +02:00
Sebastian "Sebbie" Silbermann
ba6590dd7c [DevTools] Rerender boundaries when they unsuspend when advancing the timeline (#34359) 2025-09-04 10:49:16 +02:00
Sebastian "Sebbie" Silbermann
cb1e73be04 [DevTools] Batch Suspense toggles when advancing the Suspense timeline (#34251) 2025-08-26 17:22:30 +02:00
Sebastian "Sebbie" Silbermann
75dc0026d6 [DevTools] Initial version of Suspense timeline (#34233) 2025-08-25 17:47:29 +02:00
Jan Kassens
df10309e2b Update Flow to 0.279 (#34277)
Multiple of these version upgrades required minor additional
annotations.
2025-08-25 11:02:56 -04:00
Jan Kassens
4049cfeeab Update Flow to 0.273 (#34274)
This version introduces "Natural Inference" which requires a couple more
type annotations to make Flow pass.
2025-08-22 16:58:01 -04:00
Jan Kassens
06cfa99f37 Update Flow to 0.267 (#34272)
Changes to type inference require some more annotations.
2025-08-22 15:53:07 -04:00
Jan Kassens
d260b0d8b8 Update Flow to 0.265 (#34270)
Looks like this version removed `Object.prototype` although I didn't see
that in the changelog. This is fine for this code here.
2025-08-22 15:22:22 -04:00
Jan Kassens
6de32a5a07 Update Flow to 0.263 (#34269)
This update was a bit more involved.

- `React$Component` was removed, I replaced it with Flow component
types.
- Flow removed shipping the standard library. This adds the environment
libraries back from `flow-typed` which seemed to have changed slightly
(probably got more precise and less `any`s). Suppresses some new type
errors.
2025-08-22 12:10:13 -04:00