Stacked on #34544
We only have getBoundingClientRect available from RN currently. This
should work as a substitute for this case because the equivalent of
multi-rect elements in RN is a nested Text component. We only include
the rects of top-level host components here so we can assume that
calling getBoundingClientRect on each child is the same result.
Tested in react-native with Fantom.
Stacked on #34533 for root fragment handling
This is the same approach as DOM, where we call getRootNode on the
parent.
Tests are in react-native using Fantom.
This rule was a leftover from a while ago and doesn't actually lint
anything useful. Specifically, you get a lint error if you try to opt
out a component that isn't already bailing out. If there's a bailout the
compiler already safely skips over it, so adding `'use no memo'` there
is unnecessary.
Fixes#31407
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34703).
* __->__ #34703
* #34700
Previously, the `recommended` config used the legacy ESLint format
(plugins as an array of strings). This causes errors when used with
ESLint v9's `defineConfig()` helper. This was following [eslint's own
docs](https://eslint.org/docs/latest/extend/plugins#backwards-compatibility-for-legacy-configs):
> With this approach, both configuration systems recognize
"recommended". The old config system uses the recommended key while the
current config system uses the flat/recommended key. The defineConfig()
helper first looks at the recommended key, and if that is not in the
correct format, it looks for the flat/recommended key. This allows you
an upgrade path if you’d later like to rename flat/recommended to
recommended when you no longer need to support the old config system.
However,
[`isLegacyConfig()`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L73-L81)
(also see
[`eslintrcKeys`](https://github.com/eslint/rewrite/blob/main/packages/config-helpers/src/define-config.js#L24-L35))
function doesn't check for the `plugins` key, so our config was
incorrectly treated as flat config despite being in legacy format.
This PR fixes the issue, along with a few other fixes combined:
1. Convert `recommended` to flat config format
2. Separate basic rules (exhaustive-deps, rules-of-hooks) from compiler
rules
3. Add `recommended-latest-legacy` config for non-flat config users who
want all recommended rules (including compiler rules)
4. Adding more types for the exported config
Our shipped presets in 6.x.x will essentially be:
- `recommended-legacy`: legacy (non-flat), with basic rules only
- `recommended-latest-legacy`: legacy (non-flat), all rules (basic +
compiler)
- `flat/recommended`: flat, basic rules only (now the same as
recommended, but to avoid making a breaking change we'll just keep it
around in 6.x.x)
- `recommended-latest`: flat, all rules (basic + compiler)
- `recommended`: flat, basic rules only
In the next breaking release 7.x.x, we will collapse down the presets
into three:
- `recommended-legacy`: all recommended rules
- `recommended`: all recommended rules
- `recommended-experimental`: all recommended rules + new bleeding edge
experimental rules
Closes#34679
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34700).
* #34703
* __->__ #34700
This auto updates to select the last entry in the timeline until we make
the first selection. That way when new content loads in, we show the
last timeline of what is visible.
When we flush a Suspense boundary we might not flush the fallback
segment, it might only flush a placeholder instead. In this case the
segment can flush again but we do not want to flush the boundary itself
a second time. We now detach the boundary after flushing it.
better solution to: https://github.com/facebook/react/pull/34668
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"
/>
Rebased on #34454.
Always include the root in the timeline even if it has no unique
suspenders, since even if it won't suspend, we have to be able to see
that and step to one step before the next boundary to see the first
boundary that does suspend in its fallback state.
Also, if there's no current selection on initial mount, select the last
entry in the timeline. We usually do this with `selectedSuspenseID` but
that doesn't happen on initial load. So this does it on initial load if
nothing else is selected by then. That way when you reload you get the
initial root selected.
There's a problem here because we should really use one source of truth
and `selectedSuspenseID` doesn't really do anything now. Either it
should be its separate source of truth and you can't show components in
the side-panel or it should be derived from the other state.
If it's derived, once there's a selection, e.g. in the root, then even
if new timelines load it will never change but that's probably a good
thing.
The View Transition docs were unclear about this but apparently the
`finished` promise never settles if the animation never started. So if
there's an error that rejects the `ready` promise, we'll never run the
clean up which can cause it to stall.
Fixes#34662.
However, ultimately that is caused by Chrome stalling our default
`onDefaultTransitionIndicator` but it should be unblocked after 10
seconds, not a minute.
Follow up to #34649. This adds the compiler rules back so they can be
opted-in 6.1.0, but aren't included in the presets as that would be a
breaking change.
Called Before:
> `logEvent` is a function created with React Hook "useEffectEvent", and
can only be called from the same component.
Called After:
> `logEvent` is a function created with React Hook "useEffectEvent", and
can only be called from Effects and Effect Events in the same component.
Referenced Before:
> `logEvent` is a function created with React Hook "useEffectEvent", and
can only be called from the same component. They cannot be assigned to
variables or passed down.
Referenced After:
> `logEvent` is a function created with React Hook "useEffectEvent", and
can only be called from Effects and Effect Events in the same component.
It cannot be assigned to a variable or passed down.
Reset EventTime when clearing timers. We need to track repeat updates
separately.
Typically we always reset all timers when we've logged an update. The
same update shouldn't be logged again.
I was trying to be clever and not reset the XEventTime because we also
need the timestamp to know if it's a repeat event. However, because of
this it looked like we had an event schedule an update even after we had
reset them.
This always resets the XEventTime to -1.1 and then stashes the old time
on EventRepeatTime which is our indication whether the next update was a
repeat of the old event.
---------
Co-authored-by: Ruslan Lesiutin <28902667+hoxyq@users.noreply.github.com>
Co-authored-by: Ricky <rickhanlonii@gmail.com>
Like in the diff below, we can read from the shared configuration to
check exhaustive deps.
I allow the classic additionalHooks configuration to override it so that
this change
is backwards compatible.
--
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34637).
* __->__ #34637
* #34497
We need to be able to specify additional effect hooks for the
RulesOfHooks lint rule
in order to allow useEffectEvent to be called by custom effects.
ExhaustiveDeps
does this with a regex suppplied to the rule, but that regex is not
accessible from
other rules.
This diff introduces a `react-hooks` entry you can put in the eslint
settings that
allows you to specify custom effect hooks and share them across all
rules.
This works like:
```
{
settings: {
'react-hooks': {
additionalEffectHooks: string,
},
},
}
```
The next diff allows useEffect to read from the same configuration.
----
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34497).
* #34637
* __->__ #34497
We've observed some scenarios, where cascading update happens in an
effect that was shorter than 0.05ms. In this case, this effect won't be
displayed on a timeline, because of the threshold that we are using, but
it would be shown in entry properties or in a stack trace.
To avoid confusion, we should always log such effects.
Validated via manually changing the threshold to 100ms+ and observing
that only effects that triggered an update are visible on a timeline.
Otherwise, when a context is propagated into an Activity (or Suspense)
this will leave work behind on the Offscreen component itself. Which
will cause an extra unnecessary render and commit pass just to figure
out that we're still defering it to idle.
This is because lazy context propagation, when calling to schedule some
work walks back up the tree all the way to the root. This is usually
fine for other nodes since they'll recompute their remaining child lanes
on the way up. However, for the Offscreen component we'll have already
computed it. We need to set it after propagation to ensure it gets
reset.
We selected the root. This means that we're currently viewing the
Transition that rendered the whole screen. In laymans terms this is
really "Initial Paint". Once we add subtree selection, then the
equivalent should be called "Transition" since in that case it's really
about a Transition within the page. So if you've selected an Activity
tree this should be called "Transition".
Once we add the environment support to the timeline. The first entry on
the timeline should also be called "Initial Paint" when you haven't
selected an Activity and "Transition" when you have.
Technically they're both meant to be "Transition" but nobody thinks of
initial load as a "Transition" from the previous MPA page.
<img width="1214" height="419" alt="Screenshot 2025-09-29 at 5 18 58 PM"
src="https://github.com/user-attachments/assets/cae263e3-133c-4fa9-9587-a7b2344199f4"
/>
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.
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.
## Summary
Experimentation has completed for this at Meta and we've observed
positive impact on key React Native surfaces.
## How did you test this change?
yarn flow fabric
When you double click it will hide or show by jumping to the selected
index or one step before the selected.
Let's you go from a suspense boundary into the timeline to find its
position. I also highlight the step in the timeline when you hover the
rect.
This only works if it's in the selected root but all of those should be
merged into one single timeline.
One thing that's weird about the SuspenseNodes now is that they
sometimes gets deleted but not always when they're resupended. Nested
ones maybe? This means that if you double click to hide it, you can't
double click again to show it. This seems like an unrelated bug that we
should fix.
We could potentially repurpose the existing "Suspend" button in the
toolbar to do this too, or maybe add another icon there.
Stacked on #34625.
This is a nice way to step through the timeline and simulate the visuals
on screen as you do it. It's also convenient to step through one at a
time, especially with the forwards button.
However, the secondary purpose of this is that it helps anchor the UI
visually as something like a timeline like in a video so that the
timeline itself becomes more identifiable.
https://github.com/user-attachments/assets/cb367c8e-9efb-4a00-a58e-4579be20beb8