* Failing test: DevTools hook freezes timeline
The DevTools hook calls `requestCurrentTime` after the commit phase has
ended, which has the accidnental consequence of freezing the start
time for subsequent updates. If enough time goes by, the next update
will instantly expire.
I'll push a fix in the next commit.
* Read current time without marking event start time
`requestCurrentTime` is only meant to be used for updates, because
subsequent calls within the same event will receive the same time.
Messing this up has bad consequences.
I renamed it to `requestCurrentTimeForUpdate` and created a new
function that returns the current time without the batching heuristic,
called `getCurrentTime`.
Swapping `requestCurrentTime` for `getCurrentTime` in the DevTools
hook fixes the regression test added in the previous commit.
* Add a failing test for SuspenseList bug
* Store lastEffect before rendering
We can't reset the effect list to null because we don't rereconcile the
children so we drop deletion effects if we do that.
Instead we store the last effect as it was before we started rendering
so we can go back to where it was when we reset it.
We actually already do something like this when we delete the last row
for the tail="hidden" mode so we had a field available for it already.
Without the enableSuspenseServerRenderer flag there will never be a boundary match. Also when it is enabled, there might not be a boundary match if something was conditionally rendered by mistake.
With this PR it will now client render the content of a Suspense boundary in that case and issue a DEV only hydration warning. This is the only sound semantics for this case.
Unfortunately, landing this will once again break #16938. It will be less bad though because at least it'll just work by client rendering the content instead of hydrating and issue a DEV only warning.
However, we must land this before enabling the enableSuspenseServerRenderer flag since it does this anyway.
I did notice that we special case fallback={undefined} due to our unfortunate semantics for that. So technically a workaround that works is actually setting the fallback to undefined on the server and during hydration. Then flip it on only after hydration. That could be a workaround if you want to be able to have a Suspense boundary work only after hydration for some reason.
It's kind of unfortunate but at least those semantics are internally consistent. So I added a test for that.
This changes the "default" retryTime to NoWork which schedules at Normal
pri.
Dehydrated bouundaries normally hydrate at Never priority except when they
retry where we accidentally increased them to Normal because Never was used
as the default value. This changes it so NoWork is the default.
Dehydrated boundaries however get initialized to Never as the default.
Therefore they now hydrate as Never pri unless their priority gets
increased by a forced rerender or selective hydration.
This revealed that erroring at this Never priority can cause an infinite
rerender. So I fixed that too.
* Transfer any pending promises from inner boundary to list
For non-hidden modes, this boundary should commit so this shouldn't be
needed but the nested boundary can make a second pass which forces these
to be recreated without resuspending. In this case, the outer list assumes
that it can collect the inner promises to still rerender if needed.
* Propagate suspense "context" change to nested SuspenseLists
This means that we always rerender any nested SuspenseLists together.
This bug looks similar to the previous one but is not based on the lack of
retry but that the retry only happens on the outer boundary but the inner
doesn't get a retry ping since it didn't know about its own promise after
the second pass.
* Remove dormant createBatch experiment
In a hybrid React app with multiple roots, `createBatch` is used to
coordinate an update to a root with its imperative container.
We've pivoted away from multi-root, hybrid React apps for now to focus
on single root apps.
This PR removes the API from the codebase. It's possible we'll add back
some version of this feature in the future.
* Remove unused export
Adds an API to explicitly prioritize hydrating the path to a particular DOM node without relying on events to do it.
The API uses the current scheduler priority to schedule it. For the same priority, the last one wins. This allows a similar effect as continuous events. This is useful for example to hydrate based on scroll position, or prioritize components that will upgrade to client-rendered-only content.
I considered having an API that explicitly overrides the current target(s). However that makes it difficult to coordinate across components in an app.
This just hydrates one target at a time but if it is blocked on I/O we could consider increasing priority of later targets too.
Currently, when a node suspends, if its subtree contains a portal, the portal is not hidden. This hides portals in the subtree when it's not wrapped in a host component .
* Prioritize the last continuous target
This ensures that the current focus target is always hydrated first.
Slightly higher than the usual Never expiration time used for hydration.
The priority increases with each new queued item so that the last always
wins.
* Don't export the moving target
It's not useful for comparison purposes anyway.
* Regression test: Suspense + hydration + legacy
* Allow Suspense Mismatch on the Client to Silently Proceed
This fixes but isn't actually the semantics that we want this case to have.
* Rename lowPriorityWarning to lowPriorityWarningWithoutStack
This maintains parity with the other warning-like functions.
* Duplicate the toWarnDev tests to test toLowPriorityWarnDev
* Make a lowPriorityWarning version of warning.js
* Extract both variants in print-warning
Avoids parsing lowPriorityWarning.js itself as the way it forwards the
call to lowPriorityWarningWithoutStack is not analyzable.
* Idle updates should not be blocked by hidden work
Use the special `Idle` expiration time for updates that are triggered at
Scheduler's `IdlePriority`, instead of `Never`.
The key difference between Idle and Never¹ is that Never work can be
committed in an inconsistent state without tearing the UI. The main
example is offscreen content, like a hidden subtree.
¹ "Never" isn't the best name. I originally called it that because it
"never" expires, but neither does Idle. Since it's mostly used for
offscreen subtrees, we could call it "Offscreen." However, it's also
used for dehydrated Suspense boundaries, which are inconsistent in the
sense that they haven't finished yet, but aren't visibly inconsistent
because the server rendered HTML matches what the hydrated tree would
look like.
* Reset as early as possible using local variable
* Updates in a hidden effect should be Idle
I had made them Never to avoid an extra render when a hidden effect
updates the hidden component -- if they are Idle, we have to render once
at Idle, which bails out on the hidden subtree, then again at Never to
actually process the update -- but the problem of needing an extra
render pass to bail out hidden updates already exists and we should fix
that properly instead of adding yet another special case.
* Outline push/pop logic in `renderRoot`
I want to get rid of the the `isSync` argument to `renderRoot`, and
instead use separate functions for concurrent and synchronous render.
As a first step, this extracts the push/pop logic that happens before
and after the render phase into helper functions.
* Extract `catch` block into helper function
Similar to previous commit. Extract error handling logic into
a separate function so it can be reused.
* Fork `renderRoot` for sync and concurrent
Removes `isSync` argument in favor of separate functions.
* Extra "root completion" logic to separate function
Moving this out to avoid an accidental early return, which would
bypass the call to `ensureRootIsScheduled` and freeze the UI.
* Inline `renderRoot`
Inlines `renderRoot` into `performConcurrentWorkOnRoot` and
`performSyncWorkOnRoot`. This lets me remove the `isSync` argument
and also get rid of a redundant try-catch wrapper.
* [suspense][error handling] Add failing unit test
Covers an edge case where an error is thrown inside the complete phase
of a component that is in the return path of a component that suspends.
The second error should also be handled (i.e. able to be captured by
an error boundary.
The test is currently failing because there's a call to
`completeUnitOfWork` inside the main render phase `catch` block. That
call is not itself wrapped in try-catch, so anything that throws is
treated as a fatal/unhandled error.
I believe this bug is only observable if something in the host config
throws; and, only in legacy mode, because in concurrent/batched mode,
`completeUnitOfWork` on fiber that throws follows the "unwind" path
only, not the "complete" path, and the "unwind" path does not call
any host config methods.
* [scheduler][profiler] Start time of delayed tasks
Fixes a bug in the Scheduler profiler where the start time of a delayed
tasks is always 0.
* Remove ad hoc `throw`
Fatal errors (errors that are not captured by an error boundary) are
currently rethrown from directly inside the render phase's `catch`
block. This is a refactor hazard because the code in this branch has
to mirror the code that happens at the end of the function, when exiting
the render phase in the normal case.
This commit moves the throw to the end, using a new root exit status.
* Handle errors that occur on unwind
* Add Event Replaying Infra
* Wire up Roots and Suspense boundaries, to retry events, after they commit
* Replay discrete events in order in a separate scheduler callback
* Add continuous events
These events only replay their last target if the target is not yet
hydrated. That way we don't have to wait for a previously hovered
boundary before invoking the current target.
* Enable tests from before
These tests were written with replaying in mind and now we can properly
enable them.
* Unify replaying and dispatching
* Mark system flags as a replay and pass to legacy events
That way we can check if this is a replay and therefore needs a special
case. One such special case is "mouseover" where we check the
relatedTarget.
* Eagerly listen to all replayable events
To minimize breakages in a minor, I only do this for the new root APIs
since replaying only matters there anyway. Only if hydrating.
For Flare, I have to attach all active listeners since the current
system has one DOM listener for each. In a follow up I plan on optimizing
that by only attaching one if there's at least one active listener
which would allow us to start with only passive and then upgrade.
* Desperate attempt to save bytese
* Add test for mouseover replaying
We need to check if the "relatedTarget" is mounted due to how the old
event system dispatches from the "out" event.
* Fix for nested boundaries and suspense in root container
This is a follow up to #16673 which didn't have a test because it wasn't
observable yet. This shows that it had a bug.
* Rename RESPONDER_EVENT_SYSTEM to PLUGIN_EVENT_SYSTEM
* Support disabling interaction tracing for suspense promises
If a thrown Promise has the __reactDoNotTraceInteractions attribute, React will not wrapped its callbacks to continue tracing any current interaction(s).
* Added optional '__reactDoNotTraceInteractions' attribute to Flow Thenable type
This reverts commit ab4951fc03.
* Track "pending" and "suspended" ranges
A FiberRoot can have pending work at many distinct priorities. (Note: we
refer to these levels as "expiration times" to distinguish the concept
from Scheduler's notion of priority levels, which represent broad
categories of work. React expiration times are more granualar. They're
more like a concurrent thread ID, which also happens to correspond to a
moment on a timeline. It's an overloaded concept and I'm handwaving over
some of the details.)
Given a root, there's no convenient way to read all the pending levels
in the entire tree, i.e. there's no single queue-like structure that
tracks all the levels, because that granularity of information is not
needed by our algorithms. Instead we track the subset of information
that we actually need — most importantly, the highest priority level
that exists in the entire tree.
Aside from that, the other information we track includes the range of
pending levels that are known to be suspended, and therefore should not
be worked on.
This is a refactor of how that information is tracked, and what each
field represents:
- A *pending* level is work that is unfinished, or not yet committed.
This includes work that is suspended from committing.
`firstPendingTime` and `lastPendingTime` represent the range of
pending work. (Previously, "pending" was the same as "not suspended.")
- A *suspended* level is work that did not complete because data was
missing. `firstSuspendedTime` and `lastSuspendedTime` represent the
range of suspended work. It is a subset of the pending range. (These
fields are new to this commit.)
- `nextAfterSuspendedTime` represents the next known level that comes
after the suspended range.
This commit doesn't change much in terms of observable behavior. The one
change is that, when a level is suspended, React will continue working
on the next known level instead of jumping straight to the last pending
level. Subsequent commits will use this new structure for a more
substantial refactor for how tasks are scheduled per root.
* Get next expiration time from FiberRoot
Given a FiberRoot, we should be able to determine the next expiration
time that needs to be worked on, taking into account the levels that
are pending, suspended, pinged, and so on.
This removes the `expirationTime` argument from
`scheduleCallbackForRoot`, and renames it to `ensureRootIsScheduled` to
reflect the new signature. The expiration time is instead read from the
root using a new function, `getNextExpirationTimeToWorkOn`.
The next step will be to remove the `expirationTime` argument from
`renderRoot`, too.
* Don't bind expiration time to render callback
This is a fragile pattern because there's only meant to be a single
task per root, running at a single expiration time. Instead of binding
the expiration time to the render task, or closing over it, we should
determine the correct expiration time to work on using fields we
store on the root object itself.
This removes the "return a continuation" pattern from the
`renderRoot` function. Continuation handling is now handled by
the wrapper function, which I've renamed from `runRootCallback` to
`performWorkOnRoot`. That function is merely an entry point to
`renderRoot`, so I've also removed the callback argument.
So to sum up, at at the beginning of each task, `performWorkOnRoot`
determines which expiration time to work on, then calls `renderRoot`.
And before exiting, it checks if it needs to schedule another task.
* Update error recovery test to match new semantics
* Remove `lastPendingTime` field
It's no longer used anywhere
* Restart on update to already suspended root
If the work-in-progress root already suspended with a delay, then the
current render definitely won't finish. We should interrupt the render
and switch to the incoming update.
* Restart on suspend if return path has an update
Similar to the previous commit, if we suspend with a delay, and
something in the return path has a pending update, we should abort
the current render and switch to the update instead.
* Track the next unprocessed level globally
Instead of backtracking the return path. The main advantage over the
backtracking approach is that we don't have to backtrack from the source
fiber. (The main disadvantages are that it requires another module-level
variable, and that it could include updates from unrelated
sibling paths.)
* Re-arrange slightly to prevent refactor hazard
It should not be possible to perform any work on a root without
calling `ensureRootIsScheduled` before exiting. Otherwise, we could
fail to schedule a callback for pending work and the app could freeze.
To help prevent a future refactor from introducing such a bug, this
change makes it so that `renderRoot` is always wrapped in try-finally,
and the `finally` block calls `ensureRootIsScheduled`.
* Remove recursive calls to `renderRoot`.
There are a few leftover cases where `renderRoot` is called recursively.
All of them are related to synchronously flushing work before its
expiration time.
We can remove these calls by tracking the last expired level on the
root, similar to what we do for other types of pending work, like pings.
* Remove argument from performSyncWorkOnRoot
Read the expiration time from the root, like we do
in performConcurrentWorkOnRoot.
* Track "pending" and "suspended" ranges
A FiberRoot can have pending work at many distinct priorities. (Note: we
refer to these levels as "expiration times" to distinguish the concept
from Scheduler's notion of priority levels, which represent broad
categories of work. React expiration times are more granualar. They're
more like a concurrent thread ID, which also happens to correspond to a
moment on a timeline. It's an overloaded concept and I'm handwaving over
some of the details.)
Given a root, there's no convenient way to read all the pending levels
in the entire tree, i.e. there's no single queue-like structure that
tracks all the levels, because that granularity of information is not
needed by our algorithms. Instead we track the subset of information
that we actually need — most importantly, the highest priority level
that exists in the entire tree.
Aside from that, the other information we track includes the range of
pending levels that are known to be suspended, and therefore should not
be worked on.
This is a refactor of how that information is tracked, and what each
field represents:
- A *pending* level is work that is unfinished, or not yet committed.
This includes work that is suspended from committing.
`firstPendingTime` and `lastPendingTime` represent the range of
pending work. (Previously, "pending" was the same as "not suspended.")
- A *suspended* level is work that did not complete because data was
missing. `firstSuspendedTime` and `lastSuspendedTime` represent the
range of suspended work. It is a subset of the pending range. (These
fields are new to this commit.)
- `nextAfterSuspendedTime` represents the next known level that comes
after the suspended range.
This commit doesn't change much in terms of observable behavior. The one
change is that, when a level is suspended, React will continue working
on the next known level instead of jumping straight to the last pending
level. Subsequent commits will use this new structure for a more
substantial refactor for how tasks are scheduled per root.
* Get next expiration time from FiberRoot
Given a FiberRoot, we should be able to determine the next expiration
time that needs to be worked on, taking into account the levels that
are pending, suspended, pinged, and so on.
This removes the `expirationTime` argument from
`scheduleCallbackForRoot`, and renames it to `ensureRootIsScheduled` to
reflect the new signature. The expiration time is instead read from the
root using a new function, `getNextExpirationTimeToWorkOn`.
The next step will be to remove the `expirationTime` argument from
`renderRoot`, too.
* Don't bind expiration time to render callback
This is a fragile pattern because there's only meant to be a single
task per root, running at a single expiration time. Instead of binding
the expiration time to the render task, or closing over it, we should
determine the correct expiration time to work on using fields we
store on the root object itself.
This removes the "return a continuation" pattern from the
`renderRoot` function. Continuation handling is now handled by
the wrapper function, which I've renamed from `runRootCallback` to
`performWorkOnRoot`. That function is merely an entry point to
`renderRoot`, so I've also removed the callback argument.
So to sum up, at at the beginning of each task, `performWorkOnRoot`
determines which expiration time to work on, then calls `renderRoot`.
And before exiting, it checks if it needs to schedule another task.
* Update error recovery test to match new semantics
* Remove `lastPendingTime` field
It's no longer used anywhere
* Restart on update to already suspended root
If the work-in-progress root already suspended with a delay, then the
current render definitely won't finish. We should interrupt the render
and switch to the incoming update.
* Restart on suspend if return path has an update
Similar to the previous commit, if we suspend with a delay, and
something in the return path has a pending update, we should abort
the current render and switch to the update instead.
* Track the next unprocessed level globally
Instead of backtracking the return path. The main advantage over the
backtracking approach is that we don't have to backtrack from the source
fiber. (The main disadvantages are that it requires another module-level
variable, and that it could include updates from unrelated
sibling paths.)
* Re-arrange slightly to prevent refactor hazard
It should not be possible to perform any work on a root without
calling `ensureRootIsScheduled` before exiting. Otherwise, we could
fail to schedule a callback for pending work and the app could freeze.
To help prevent a future refactor from introducing such a bug, this
change makes it so that `renderRoot` is always wrapped in try-finally,
and the `finally` block calls `ensureRootIsScheduled`.
* Remove recursive calls to `renderRoot`.
There are a few leftover cases where `renderRoot` is called recursively.
All of them are related to synchronously flushing work before its
expiration time.
We can remove these calls by tracking the last expired level on the
root, similar to what we do for other types of pending work, like pings.
* Remove argument from performSyncWorkOnRoot
Read the expiration time from the root, like we do
in performConcurrentWorkOnRoot.
This happens for example when a deleted boundary transfers its pending
promises to the list so that the list can be retried.
This wasn't caught by unit tests because this flag wasn't on in those
tests.