Commit Graph
934 Commits
Author SHA1 Message Date
Josh StoryandGitHub 9617d39eca [Fizz] Add proper assertion for stream fix (#27543)
In https://github.com/facebook/react/pull/27541 I added tests to assert
that the write after close bug was fixed. However the Node
implementation has an abort behavior preventing reproduction of the
original issue and the Browser build does not use AsyncLocalStorage
which also prevents reproduction. This change moves the Browser test to
a Edge runtime where AsyncLocalStorage is polyfilled. In this
implementation the test does correctly fail when the patch is removed.
2023-10-18 12:01:09 -07:00
Josh StoryandGitHub 601e5c3850 [Fizz][Float] Do not write after closing the stream (#27541)
Float methods can hang on to a reference to a Request after the request
is closed due to AsyncLocalStorage. If a Float method is called at this
point we do not want to attempt to flush anything. This change updates
the closing logic to also call `stopFlowing` which will ensure that any
checks against the destination properly reflect that we cannot do any
writes. In addition it updates the enqueueFlush logic to existence check
the destination inside the work function since it can change across the
work scheduling gap if it is async.

fixes: https://github.com/facebook/react/issues/27540
2023-10-18 10:05:18 -07:00
Asan AzimkulovandGitHub 67cc9ba878 Fix: error messages (#27523)
<!--
  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
Changes `before before` to `before` in error messages.

<!--
Explain the **motivation** for making this change. What existing problem
does the pull request solve?
-->
I want to improve error logs

## How did you test this change?

<!--
Demonstrate the code is solid. Example: The exact commands you ran and
their output, screenshots / videos if the pull request changes the user
interface.
How exactly did you verify that your PR solves the issue you wanted to
solve?
  If you leave this empty, your PR will very likely be closed.
-->
2023-10-16 15:05:42 +01:00
Andrew ClarkandGitHub 309c8ad968 Track entangled lanes separately from update lane (#27505)
A small refactor to how the lane entanglement mechanism works. We can
now distinguish between the lane that "spawned" a render task (i.e. a
new update) versus the lanes that it's entangled with. Both the update
lane and the entangled lanes will be included while rendering, but by
keeping them separate, we don't lose the original priority.

In practical terms, this means we can now entangle a low priority update
with a higher priority lane while rendering at the lower priority.

To do this, lanes that are entangled at the root are now tracked using
the same variable that we use to track the "base lanes" when revealing a
previously hidden tree — conceptually, they are the same thing. I also
renamed this variable (from subtreeLanes to entangledRenderLanes) to
better reflect how it's used.

My primary motivation is related to useDeferredValue, which I'll address
in a later PR.
2023-10-15 12:29:52 -04:00
Sebastian MarkbågeandGitHub 09fbee89d6 [Fizz] Don't pop the replay stack if we've already rendered past an element (#27513)
This is the same problem as we had with keyPath before where if the
element itself suspends, we have to restore the replay node to what it
was before, however, if something below the element suspends we
shouldn't pop it because that will pop it back up the stack.

Instead of passing replay as an argument to every renderElement
function, I use a hack to compare if the node is still the same as the
one we tried to render, then that means we haven't stepped down into the
child yet. Maybe this is not quite correct because in theory you could
have a recursive node that just renders itself over and over until some
context bails out.

This solves an issue where if you suspended in an element it would retry
trying to replay from that element but using the postponed state from
the root.
2023-10-13 09:21:21 -04:00
Josh StoryandGitHub bb778528d1 [Fizz] Fix children rendering in custom elements with enableCustomElementPropertySupport (#27511)
The `enableCustomElementPropertySupport` flag changes React's handling
of custom elements in a way that is more useful that just treating every
prop as an attribute. However when server rendering we have no choice
but to serialize props as attributes. When this flag is on and React
supports more prop types on the client like functions and objects the
server implementation needs to be a bit more naunced in how it renders
these components. With this flag on `false`, function, and object props
are omitted entirely and `true` is normalized to `""`. There was a bug
however in the implementation which caused children more complex than a
single string to be omitted because it matched the object type filter.
This change reorganizes the code a bit to put these filters in the
default prop handline case, leaving children, style, and innerHTML to be
handled via normal logic.

fixes: https://github.com/facebook/react/issues/27286
2023-10-12 15:02:26 -07:00
Sebastian MarkbågeandGitHub 1fc58281af Disable ServerContext in canary (#27474)
We previously added a warning in #27424. This just removes it from
canary/stable channels but keeps it in experimental for now.
2023-10-11 12:13:09 -04:00
Devon GovettandGitHub 537228f9fd Add support for onScrollEnd event (#26789)
## Summary

This adds support for the new
[scrollend](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollend_event)
event. It was recently added to the spec, and is currently supported in
Firefox 109 and Chrome Canary (shipping in Chrome 114). You can read
more about this event
[here](https://developer.chrome.com/blog/scrollend-a-new-javascript-event/).
This PR adds support for the `onScrollEnd` prop, following the
implementation for `onScroll`.

## How did you test this change?

Added unit tests.
2023-10-10 21:00:57 -07:00
Andrew ClarkandGitHub be67db46b6 Add optional initialValue argument to useDeferredValue (#27500)
Adds a second argument to useDeferredValue called initialValue:

```js
const value = useDeferredValue(finalValue, initialValue);
```

During the initial render of a component, useDeferredValue will return
initialValue. Once that render finishes, it will spawn an additional
render to switch to finalValue.

This same sequence should occur whenever the hook is hidden and revealed
again, i.e. by a Suspense or Activity, though this part is not yet
implemented.

When initialValue is not provided, useDeferredValue has no effect during
initial render, but during an update, it will remain on the previous
value, then spawn an additional render to switch to the new value. (This
is the same behavior that exists today.)

During SSR, initialValue is always used, if provided.

This feature is currently behind an experimental flag. We plan to ship
it in a non-breaking release.
2023-10-10 16:39:02 -04:00
Sebastian MarkbågeandGitHub 0fba3ecf73 [Fizz] Reset error component stack and fix error messages (#27456)
The way we collect component stacks right now are pretty fragile.

We expect that we'll call captureBoundaryErrorDetailsDev whenever an
error happens. That resets lastBoundaryErrorComponentStackDev to null
but if we don't, it just lingers and we don't set it to anything new
then which leaks the previous component stack into the next time we have
an error. So we need to reset it in a bunch of places.

This is still broken with erroredReplay because it has the inverse
problem that abortRemainingReplayNodes can call
captureBoundaryErrorDetailsDev more than one time. So the second
boundary won't get a stack.

We probably should try to figure out an alternative way to carry along
the stack. Perhaps WeakMap keyed by the error object.

This also fixes an issue where we weren't invoking the onShellReady
event if we error a replay. That event is a bit weird for resuming
because we're probably really supposed to just invoke it immediately if
we have already flushed the shell in the prerender which is always atm.
Right now, it gets invoked later than necessary because you could have a
resumed hole ready before a sibling in the shell is ready and that's
blocked.
2023-10-04 16:48:12 -04:00
Andrew ClarkandGitHub bfefb22842 Upgrade Server Actions to canary (#27459)
Upgrades the stability of Server Actions from experimental to canary.

- Turns on enableAsyncActions and enableFormActions
- Removes "experimental_" prefix from useOptimistic, useFormStatus, and
useFormState
2023-10-04 14:51:36 -04:00
Sophie AlpertandGitHub db69f95e48 Fix checkbox and radio hydration (#27401)
Fixes whatever part of https://github.com/facebook/react/issues/26876
and https://github.com/vercel/next.js/issues/49499 that
https://github.com/facebook/react/pull/27394 didn't fix, probably.

From manual tests I believe this behavior brings us back to parity with
latest stable release (18.2.0). It's awkward that we keep the user's
state even for controlled inputs, so the DOM is out of sync with React
state.

Previously the .defaultChecked assignment done in updateInput() was
changing the actual checkedness because the dirty flag wasn't getting
set, meaning that hydrating could change which radio button is checked,
even in the absence of user interaction! Now we go back to always
detaching again.
2023-10-02 11:38:44 -07:00
Sophie AlpertandGitHub 4f4c52a3c8 Fix controlled radios, maybe for real this time (#27443)
Fixes #26876 for real?

In 18.2.0 (last stable), we set .checked unconditionally:


https://github.com/facebook/react/blob/v18.2.0/packages/react-dom/src/client/ReactDOMInput.js#L129-L135

This is important because if we are updating two radios' checkedness
from (false, true) to (true, false), we need to make sure that
input2.checked is explicitly set to false, even though setting
`input1.checked = true` already unchecks input2.

I think this fix is not complete because there is no guarantee that all
the inputs rerender at the same time? Hence the TODO. But in practice
they usually would and I _think_ this is comparable to what we had
before.

Also treating function and symbol as false like we used to and like we
do on initial mount.
2023-10-02 11:38:13 -07:00
a6ed60a8eb [Fizz] Don't double replay elements when it's the postpone point (#27440)
The `resumeElement` function wasn't actually doing the correct thing
because it was resuming the element itself but what the child slot means
is that we're supposed to resume in the direct child of the element.
This is difficult to check for since it's all the possible branches that
the element can render into, so instead we just check this in
renderNode. It means the hottest path always checks the task which is
unfortunate.

And also, resuming using the correct nextSegmentId.

Fixes two bugs surfaced by this test.

---------

Co-authored-by: Josh Story <josh.c.story@gmail.com>
2023-09-29 18:24:38 -04:00
Andrew ClarkandGitHub d900fadbf9 Bugfix: Selective hydration triggers false update loop error (#27439)
This adds a regression test and fix for a case where a sync update
triggers selective hydration, which then leads to a "Maximum update
depth exceeded" error, even though there was only a single update. This
happens when a single sync update flows into many sibling dehydrated
Suspense boundaries.

This fix is, if a commit was the result of selective hydration, we
should not increment the nested update count, because those renders
conceptually are not updates.

Ideally, they wouldn't even be in a separate commit — we should be able
to hydrate a tree and apply an update on top of it within the same
render phase. We could do this once we implement resumable context
stacks.
2023-09-29 14:29:35 -04:00
1ebedbec2b Add Server Context deprecation warning (#27424)
As agreed, we're removing Server Context. This was never official
documented.

We've found that it's not that useful in practice. Often the better
options are:

- Read things off the url or global scope like params or cookies.
- Use the module system for global dependency injection.
- Use `React.cache()` to dedupe multiple things instead of computing
once and passing down.

There are still legit use cases for Server Context but you have to be
very careful not to pass any large data, so in generally we recommend
against it anyway.

Yes, prop drilling is annoying but it's not impossible for the cases
this is needed. I would personally always pick it over Server Context
anyway.

Semantically, Server Context also blocks object deduping due to how it
plays out with Server Components that can't be deduped. This is much
more important feature.

Since it's already in canary along with the rest of RSC, we're adding a
warning for a few versions before removing completely to help migration.

---------

Co-authored-by: Josh Story <josh.c.story@gmail.com>
2023-09-28 11:03:19 -04:00
Josh StoryandGitHub 49eba01930 [Fizz][Float] Refactor Resources (#27400)
Refactors Resources to have a more compact and memory efficient
struture. Resources generally are just an Array of chunks. A resource is
flushed when it's chunks is length zero. A resource does not have any
other state.

Stylesheets and Style tags are different and have been modeled as a unit
as a StyleQueue. This object stores the style rules to flush as part of
style tags using precedence as well as all the stylesheets associated
with the precedence. Stylesheets still need to track state because it
affects how we issue boundary completion instructions. Additionally
stylesheets encode chunks lazily because we may never write them as html
if they are discovered late.

The preload props transfer is now maximally compact (only stores the
props we would ever actually adopt) and only stores props for
stylesheets and scripts because other preloads have no resource
counterpart to adopt props into. The ResumableState maps that track
which keys have been observed are being overloaded. Previously if a key
was found it meant that a resource already exists (either in this render
or in a prior prerender). Now we discriminate between null and object
values. If map value is null we can assume the resource exists but if it
is an object that represents a prior preload for that resource and the
resource must still be constructed.
2023-09-26 09:59:39 -07:00
Sebastian MarkbågeandGitHub bff6be8eb1 [Fizz] Track postpones in fallbacks (#27421)
This fixes so that you can postpone in a fallback. This postpones the
parent boundary. I track the fallbacks in a separate replay node so that
when we resume, we can replay the fallback itself and finish the
fallback and then possibly later the content. By doing this we also
ensure we don't complete the parent too early since now it has a render
task on it.

There is one case that this surfaces that isn't limited to
prerender/resume but also render/hydrateRoot. I left todos in the tests
for this.

If you postpone in a fallback, and suspend in the content but eventually
don't postpone in the content then we should be able to just skip
postponing since the content rendered and we no longer need the
fallback. This is a bit of a weird edge case though since fallbacks are
supposed to be very minimal.

This happens because in both cases the fallback starts rendering early
as soon as the content suspends. This also ensures that the parent
doesn't complete early by increasing the blocking tasks. Unfortunately,
the fallback will irreversibly postpone its parent boundary as soon as
it hits a postpone.

When you suspend, the same thing happens but we typically deal with this
by doing a "soft" abort on the fallback since we don't need it anymore
which unblocks the parent boundary. We can't do that with postpone right
now though since it's considered a terminal state.

I think I'll just leave this as is for now since it's an edge case but
it's an annoying exception in the model. Makes me feel I haven't quite
nailed it just yet.
2023-09-25 19:02:25 -04:00
Sebastian MarkbågeandGitHub 69728fde0a [Flight] Dedupe suspense boundaries when it has already been found earlier (#27408)
If we track a postponed SuspenseBoundary parent that we have to replay
through before it has postponed and it postpones itself later, we need
to upgrade it to a postponed replay boundary instead of adding two.
2023-09-23 12:33:48 -04:00
Sebastian MarkbågeandGitHub d9e00f795b Stop flowing and then abort if a stream is cancelled (#27405)
We currently abort a stream either it's explicitly told to abort (e.g.
by an abortsignal). In this case we still finish writing what we have as
well as instructions for the client about what happened so it can
trigger fallback cases and log appropriately.

We also abort a request if the stream itself cancels. E.g. if you can't
write anymore. In this case we should not write anything to the outgoing
stream since it's supposed to be closed already now. However, we should
still abort the request so that more work isn't performed and so that we
can log the reason for it to the onError callback.

We should also not do any work after aborting.

There we need to stop the "flow" of bytes - so I call stopFlowing in the
cancel case before aborting.

The tests were testing this case but we had changed the implementation
to only start flowing at initial read (pull) instead of start like we
used to. As a result, it was no longer covering this case. We have to
call reader.read() in the tests to start the flow so that we need to
cancel it.

We also were missing a final assertion on the error logs and since we
were tracking them explicitly the extra error was silenced.
2023-09-22 15:16:49 -04:00
Sophie AlpertandGitHub 3c27178a2f Update tracked value after resetting radio group (#27394)
Fixes #26876, I think. Review each commit separately (all assertions
pass in main already, except the last assertInputTrackingIsClean in
"should control radio buttons").

I'm actually a little confused on two things here:
* All the isCheckedDirty assertions are true. But I don't think we set
.checked unconditionally? So how does this happen?
* https://github.com/facebook/react/issues/26876#issuecomment-1611662862
claims that
https://github.com/facebook/react/compare/d962f35ca...1f248bdd7 contains
the faulty change, but it doesn't appear to change the restoration logic
that I've touched here. (One difference outside restoration is that
updateProperties did previously set `.checked` when `nextProp !==
lastProp` whereas the new logic in updateInput is to set it when
`node.checked !== !!checked`.)

But it seems to me like we need this call here anyway, and if it fixes
it then it fixes it? I think technically speaking we probably should do
all the updateInput() calls and then all the updateValueIfChanged()
calls—in particular I think if clicking A changed the checked radio
button from B to C then the code as I have it would be incorrect, but
that also seems unlikely so idk whether to care.

cc @zhengjitf @Luk-z who did some investigation on the original issue
2023-09-20 21:57:09 -07:00
Sebastian MarkbågeandGitHub b775564d35 [Fizz] Ensure Resumable State is Serializable (#27388)
Moves writing queues to renderState.

We shouldn't need the resource tracking's value. We just need to know if
that resource has already been emitted. We can use a Set for this. To
ensure that set is directly serializable we can just use a
dictionary-like object with no value.

See individual commits for special cases.
2023-09-20 12:21:53 -04:00
Sebastian MarkbågeandGitHub 925c66a647 [Fizz] Client render the nearest child or parent suspense boundary if replay errors or is aborted (#27386)
Based on #27385.

When we error or abort during replay, that doesn't actually error the
component that errored because that has already rendered. The error only
affects any child that is not yet completed. Therefore the error kind of
gets thrown at the resumable point.

The resumable point might be a hole in the replay path, in which case
throwing there errors the parent boundary just the same as if the replay
component errored. If the hole is inside a deeper Suspense boundary
though, then it's that Suspense boundary that gets client rendered. I.e.
the child boundary. We can still finish any siblings.

In the shell all resumable points are inside a boundary since we must
have finished the shell. Therefore if you error in the root, we just
simply just turn all incomplete boundaries into client renders.
2023-09-18 11:25:56 -04:00
Sebastian MarkbågeandGitHub a5fc797db1 [Fizz] Replay Postponed Paths (#27379)
This forks Task into ReplayTask and RenderTask.

A RenderTask is the normal mode and it has a segment to write into.

A ReplayTask doesn't have a segment to write into because that has
already been written but instead it has a ReplayState which keeps track
of the next set of paths to follow. Once we hit a "Resume" node we
convert it into a RenderTask and continue rendering from there.

We can resume at either an Element position or a Slot position. An
Element pointing to a component doesn't mean we resume that component,
it means we resume in the child position directly below that component.
Slots are slots inside arrays.

Instead of statically forking most paths, I kept using the same path and
checked for the existence of a segment or replay state dynamically at
runtime.

However, there's still quite a bit of forking here like retryRenderTask
and retryReplayTask. Even in the new forks there's a lot of duplication
like resumeSuspenseBoundary, replaySuspenseBoundary and
renderSuspenseBoundary. There's opportunity to simplify this a bit.
2023-09-15 15:24:04 -04:00
Sebastian MarkbågeandGitHub 7a3cb8f9cf Track the key path difference between right before the first array (#27360)
There's a subtle difference if you suspend before the first array or
after. In Fiber, we don't deal with this because we just suspend the
parent and replay it if lazy() or Usable are used in its child slots. In
Fizz we try to optimize this a bit more and enable resuming inside the
component.

Semantically, it's different if you suspend/postpone before the first
child array or inside that child array. Because when you resume the
inner result might be another array and either that's part of the parent
path or part of the inner slot.

There might be more clever way of structuring this but I just use -1 to
indicate that we're not yet inside the array and is in the root child
position. If that renders an element, then that's just the same as the 0
slot.

We need to also encode this in the resuming. I called that resuming the
element or resuming the slot.
2023-09-12 12:30:42 -04:00
Josh StoryandGitHub bbc8530ed7 [Float] Refactor public interface and internal HostDispatcher implementation (#27361)
When Float was first developed the internal implementation and external
interface were the same. This is problematic for a few reasons. One, the
public interface is typed but it is also untrusted and we should not
assume that it is actually respected. Two, the internal implementations
can get called from places other than the the public interface and
having to construct an options argument that ends up being destructured
to process the request is computationally wasteful and may limit JIT
optimizations to some degree. Lastly, the wire format was not as
compressed as it could be and it was untyped.

This refactor aims to address that by separating the public interface
from the internal implementations so we can solve these challenges and
also make it easier to change Float in the future

* The internal dispatcher method preinit is now preinitStyle and
preinitScript.
* The internal dispatcher method preinitModule is now
preinitModuleScript in anticipation of different implementations for
other module types in the future.
* The wire format is explicitly typed and only includes options if they
are actually used omitting undefined and nulls.
* Some function arguments are not options even if they are optional. For
instance precedence can be null/undefined because we deafult it to
'default' however we don't cosnider this an option because it is not
something we transparently apply as props to the underlying instance.
* Fixes a problem with keying images in flight where srcset and sizes
were not being taken into account.
* Moves argument validation into the ReactDOMFloat file where it is
shared with all runtimes that expose these methods
* Fixes crossOrigin serialization to use empty string except when
'use-credentials'
2023-09-12 08:09:10 -07:00
Sebastian MarkbågeandGitHub bb1d8d1667 Encode lazy Node slots properly in key path and resumable paths (#27359)
It's possible to postpone a specific node and not using a wrapper
component. Therefore we encode the resumable slot as the index slot.
When it's a plain client component that postpones, it's encoded as the
child slot inside that component which is the one that's postponed
rather than the component itself.

Since it's possible for a child slot to suspend (e.g. React.lazy's
microtask in this case) retryTask might need to keep its index around
when it resolves.
2023-09-11 19:25:24 -04:00
Andrew ClarkandGitHub a4aceafc63 Fix: Skip hidden inputs before text instance (#27358)
Found a hydration bug that happens when you pass a Server Action to
`formAction` and the next node is a text instance.

The HTML generated by Fizz is something like this:

```html
<button name="$ACTION_REF_5" formAction="" formEncType="multipart/form-data" formMethod="POST">
  <input type="hidden" name="$ACTION_5:0" value="..."/>
  <input type="hidden" name="$ACTION_5:1" value="..."/>
  <input type="hidden" name="$ACTION_KEY" value="..."/>Count: <!-- -->0
</button>
```

Fiber is supposed to skip over the extra hidden inputs, but it doesn't
handle this correctly if the next expected node isn't a host instance.
In this case, it's a text instance.

Not sure if the proper fix is to change the HTML that is generated, or
to change the hydration logic, but in this PR I've done the latter.
2023-09-11 18:22:37 -04:00
Josh StoryandGitHub 41f0e9dae3 [Float][Fizz] include preloadModule and preinitModule on react-dom/server-rendering-stub (#27347)
When implementing `preloadModule` and `preinitModule` these methods were
not exposed on the server rendering stub when they should have been.
This PR corrects that omission.
2023-09-07 14:48:22 -07:00
Andrew ClarkandGitHub 8b26f07a88 useFormState: Emit comment to mark whether state matches (#27307)
A planned feature of useFormState is that if the page load is the result
of an MPA-style form submission — i.e. a form was submitted before it
was hydrated, using Server Actions — the state of the hook should
transfer to the next page.

I haven't implemented that part yet, but as a prerequisite, we need some
way for Fizz to indicate whether a useFormState hook was rendered using
the "postback" state. That way we can do all state matching logic on the
server without having to replicate it on the client, too.

The approach here is to emit a comment node for each useFormState hook.
We use one of two comment types: `<!--F-->` for a normal useFormState
hook, and `<!--F!-->` for a hook that was rendered using the postback
state. React will read these markers during hydration. This is similar
to how we encode Suspense boundaries.

Again, the actual matching algorithm is not yet implemented — for now,
the "not matching" marker is always emitted.

We can optimize this further by not emitting any markers for a render
that is not the result of a form postback, which I'll do in subsequent
PRs.
2023-09-07 16:05:44 -04:00
Josh StoryandGitHub 3566de59e2 [Fizz][Float] <img> inside <picture> should not preload during SSR (#27346)
img tags inside picture tags should not automatically be preloaded
because usually the img is a fallback. We will consider a more
comprehensive way of preloading picture tags which may require a
technique like using an inline script to construct the image in the
browser but for now we simply omit the preloads to avoid harming load
times by loading fallbacks.
2023-09-07 12:48:40 -07:00
Sebastian MarkbågeandGitHub b70a0d7022 [Fizz] Track postponed holes in the prerender pass (#27317)
This is basically the implementation for the prerender pass.

Instead of forking basically the whole implementation for prerender, I
just add a conditional field on the request. If it's `null` it behaves
like before. If it's non-`null` then instead of triggering client
rendered boundaries it triggers those into a "postponed" state which is
basically just a variant of "pending". It's supposed to be filled in
later.

It also builds up a serializable tree of which path can be followed to
find the holes. This is basically a reverse `KeyPath` tree.

It is unfortunate that this approach adds more code to the regular Fizz
builds but in practice. It seems like this side is not going to add much
code and we might instead just want to merge the builds so that it's
smaller when you have `prerender` and `resume` in the same bundle -
which I think will be common in practice.

This just implements the prerender side, and not the resume side, which
is why the tests have a TODO. That's in a follow up PR.
2023-08-31 12:23:26 -04:00
Andrew ClarkandGitHub 456d153bb5 Client implementation of useFormState (#27278)
This implements useFormState in Fiber. (It does not include any
progressive enhancement features; those will be added later.)

useFormState is a hook for tracking state produced by async actions. It
has a signature similar to useReducer, but instead of a reducer, it
accepts an async action function.

```js
async function action(prevState, payload) {
  // ..
}
const [state, dispatch] = useFormState(action, initialState)
```

Calling dispatch runs the async action and updates the state to the
returned value.

Async actions run before React's render cycle, so unlike reducers, they
can contain arbitrary side effects.
2023-08-28 11:05:40 -04:00
Andrew ClarkandGitHub ab31a9ed28 Selective Hydration: Don't suspend if showing fallback (#27230)
A transition that flows into a dehydrated boundary should not suspend if
the boundary is showing a fallback.

This is related to another issue where Fizz streams in the initial HTML
after a client navigation has already happened. That issue is not fixed
by this commit, but it does make it less likely. Need to think more
about the larger issue.
2023-08-24 20:04:34 -04:00
Josh StoryandGitHub 9d4582dffd [Float][Fizz][Static] add importMap option to Fizz and Static server renderers (#27260)
Import maps need to be emitted before any scripts or preloads so the
browser can properly locate these resources.

Unlike most scripts, importmaps are singletons meaning you can only have
one per document and they must appear before any modules are loaded or
preloaded. In the future there may be a way to dynamically add more
mappings however the proposed API for this seems likely to be a
javascript API and not an html tag.

Given the unique constraints here this PR implements React's support of
importMaps as the following

1. an `importMap` option accepting a plain object mapping module
specifier to path is accepted in any API that renders a preamble (head
content). Notably this precludes resume rendering because in resume
cases the preamble should have already been produced as part of the
prerender step.
2. the importMap is stringified and emitted as a `<script
type="importmap">...</script>` in the preamble.
3. the importMap is escaped identically to how bootstrapScriptContent is
escaped, notably, isntances of `</script>` are escaped to avoid breaking
out of the script context

Users can still render importmap tags however with Float enabled this is
rather pointless as most modules will be hoisted above the importmap
that is rendered. In practice this means the only functional way to use
import maps with React is to use this config API.
2023-08-24 13:48:28 -07:00
Andrew ClarkandGitHub b4cdd3e892 Scaffolding for useFormState (#27270)
This exposes, but does not yet implement, a new experimental API called
useFormState. It's gated behind the enableAsyncActions flag.

useFormState has a similar signature to useReducer, except instead of a
reducer it accepts an (async) action function. React will wait until the
promise resolves before updating the state:

```js
async function action(prevState, payload) {
  // ..
}
const [state, dispatch] = useFormState(action, initialState)
```

When used in combination with Server Actions, it will also support
progressive enhancement — a form that is submitted before it has
hydrated will have its state transferred to the next page. However, like
the other action-related hooks, it works with fully client-driven
actions, too.
2023-08-23 10:58:09 -04:00
Sebastian MarkbågeandGitHub 856dc5e433 Fix escaping in action error URL (#27273)
This URL is generated on the client (there's an equivalent but shorter
SSR version too) when a function is used as an action. It should never
happen but it'll be invoked if a form is manually submitted or event is
stopped early.

The `'` wasn't escaped so this yielded invalid syntax. Which is an error
too but much less helpful. `missing ) after argument list`. Added a test
that evals to make sure it's correct syntax.
2023-08-22 19:10:00 -04:00
Sebastian MarkbågeandGitHub 31034b6de7 [Fizz] Split ResponseState/Resources into RenderState/ResumableState (#27268)
This exposes a `resume()` API to go with the `prerender()` (only in
experimental). It doesn't work yet since we don't yet emit the postponed
state so not yet tested.

The main thing this does is rename ResponseState->RenderState and
Resources->ResumableState. We separated out resources into a separate
concept preemptively since it seemed like separate enough but probably
doesn't warrant being a separate concept. The result is that we have a
per RenderState in the Config which is really just temporary state and
things that must be flushed completely in the prerender. Most things
should be ResumableState.

Most options are specified in the `prerender()` and transferred into the
`resume()` but certain options that are unique per request can't be.
Notably `nonce` is special. This means that bootstrap scripts and
external runtime can't use `nonce` in this mode. They need to have a CSP
configured to deal with external scripts, but not inline.

We need to be able to restore state of things that we've already emitted
in the prerender. We could have separate snapshot/restore methods that
does this work when it happens but that means we have to explicitly do
that work. This design is trying to keep to the principle that we just
work with resumable data structures instead so that we're designing for
it with every feature. It also makes restoring faster since it's just
straight into the data structure.

This is not yet a serializable format. That can be done in a follow up.

We also need to vet that each step makes sense. Notably stylesToHoist is
a bit unclear how it'll work.
2023-08-22 15:21:36 -04:00
Josh StoryandGitHub 86198b9231 [Float][Fizz][Legacy] hoisted elements no longer emit before <html> in legacy apis such as renderToString() (#27269)
renderToString is a legacy server API which used a trick to avoid having
the DOCTYPE included when rendering full documents by setting the root
formatcontext to HTML_MODE rather than ROOT_HTML_MODE. Previously this
was of little consequence but with Float the Root mode started to be
used for things like determining if we could flush hoistable elements
yet. In issue #27177 we see that hoisted elements can appear before the
<html> tag when using a legacy API `renderToString`.

This change exports a DOCTYPE from FizzConfigDOM and FizzConfigDOMLegacy
respectively, using an empty chunk in the legacy case. The only runtime
perf cost here is that for legacy APIs there is an extra empty chunk to
write when rendering a top level <html> tag which is trivial enough

Fixes #27177
2023-08-22 10:54:33 -07:00
Andrew ClarkandGitHub dd480ef923 Fix: Stylesheet in error UI suspends indefinitely (#27265)
This fixes the regression test added in the previous commit. The
"Suspensey commit" implementation relies on the
`shouldRemainOnPreviousScreen` function to determine whether to 1)
suspend the commit 2) activate a parent fallback and schedule a retry.
The issue was that we were sometimes attempting option 2 even when there
was no parent fallback.

Part of the reason this bug landed is due to how `throwException` is
structured. In the case of Suspensey commits, we pass a special "noop"
thenable to `throwException` as a way to trigger the Suspense path. This
special thenable must never have a listener attached to it. This is not
a great way to structure the logic, it's just a consequence of how the
code evolved over time. We should refactor it into multiple functions so
we can trigger a fallback directly without having to check the type. In
the meantime, I added an internal warning to help detect similar
mistakes in the future.
2023-08-22 11:22:30 -04:00
Andrew ClarkandGitHub 7d1c3c1589 Regression test: Stylesheet suspends indefinitely when part of error boundary UI during initial hydration (#27258)
Adds a failing test for a case discovered by Next.js. An error boundary
is triggered during initial hydration, and the error fallback includes a
stylesheet. If the stylesheet has not yet been loaded, the commit
suspends, but never resolves even after the stylesheet finishes loading.

Triggering this bug depends on several very specific code paths being
triggered simultaneously. There are a few ways we could fix the bug;
I'll submit as one or more separate PRs to show that each one is
sufficient.
2023-08-21 19:14:56 -04:00
Josh StoryandGitHub e505316920 [Float][Flight][Fizz][Fiber] Implement preloadModule and preinitModule (#27220)
Stacked on #27224 

### Implements `ReactDOM.preloadModule()`
`preloadModule` is a function to preload modules of various types.
Similar to `preload` this is useful when you expect to use a Resource
soon but can't render that resource directly. At the moment the only
sensible module to preload is script modules along with some other `as`
variants such as `as="serviceworker"`. In the future when there is some
notion of how to preload style module script or json module scripts this
API will be extended to support those as well.

##### Arguments
1. `href: string` -> the href or src value you want to preload.
2. `options?: {...}` -> 
2.1. `options.as?: string` -> the `as` property the modulepreload link
should render with. If not provided it will be omitted which will cause
the modulepreload to be treated like a script module
2.2. `options.crossOrigin?: string` -> modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3. `options.integrity?: string` -> an integrity hash for subresource
integrity APIs
  
##### Rendering
each preloaded module will emit a `<link rel="modulepreload" href="..."
/>`
if `as` is specified and is something other than `"script"` the as
attribute will also be included
if crossOrigin or integrity as specified their attributes will also be
included

During SSR these script tags will be emitted before content. If we have
not yet flushed the document head they will be emitted there after
things that block paint such as font preloads, img preloads, and
stylesheets.

On the client these link tags will be appended to the document.head.
  
### Implements `ReactDOM.preinitModule()`
`preinitModule` is a function to loading module scripts before they are
required. It has the same use cases as `preinit`.

During SSR you would use this to tell the browsers to start fetching
code that will be used without having to wait for bootstrapping to
initiate module fetches.

ON the client you would use this to start fetching a module script early
for an anticipated navigation or other event that is likely to depend on
this module script.

the `as` property for Float methods drew inspiration from the `as`
attribute of the `<link rel="preload" ... >` tag but it is used as a
sort of tag for the kind of thing being targetted by Float methods. For
`preinitModule` we currently only support `as: "script"` and this is
also the assumed default type so you current never need to specify this
`as` value. In the future `preinitModule` will support additional module
script types such as `style` or `json`. The support of these types will
correspond to [Module Import
Attributes](https://github.com/tc39/proposal-import-attributes).

##### Arguments
1. `href: string` -> the href or src value you want to preinitialize
2. `options?: {...}` ->
2.1 `options.as?: string` -> only supports `script` and this is the
default behavior. Until we support import attributes such as `json` and
`style` there will not be much reason to provide an `as` option.
2.2. `options.crossOrigin?: string`: modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3 `options.integrity?: string` -> an integrity hash for subresource
integrity APIs

##### Rendering
each preinitialized `script` module will emit a `<script type="module"
async="" src"...">` During SSR these will appear behind display blocking
resources such as font preloads, img preloads, and stylesheets. In the
browser these will be appende to the head.

Note that for other `as` types the rendered output will be slightly
different. `<script type="module">import "..." with {type: "json"
}</script>`. Since this tag is an inline script variants of React that
do not use inline scripts will simply omit these preinitialization tags
from the SSR output. This is not implemented in this PR but will appear
in a future update.
2023-08-17 10:30:00 -07:00
Sebastian MarkbågeandGitHub ac1a16c67e Add Postpone API (#27238)
This adds an experimental `unstable_postpone(reason)` API.

Currently we don't have a way to model effectively an Infinite Promise.
I.e. something that suspends but never resolves. The reason this is
useful is because you might have something else that unblocks it later.
E.g. by updating in place later, or by client rendering.

On the client this works to model as an Infinite Promise (in fact,
that's what this implementation does). However, in Fizz and Flight that
doesn't work because the stream needs to end at some point. We don't
have any way of knowing that we're suspended on infinite promises. It's
not enough to tag the promises because you could await those and thus
creating new promises. The only way we really have to signal this
through a series of indirections like async functions, is by throwing.
It's not 100% safe because these values can be caught but it's the best
we can do.

Effectively `postpone(reason)` behaves like a built-in [Catch
Boundary](https://github.com/facebook/react/pull/26854). It's like
`raise(Postpone, reason)` except it's built-in so it needs to be able to
be encoded and caught by Suspense boundaries.

In Flight and Fizz these behave pretty much the same as errors. Flight
just forwards it to retrigger on the client. In Fizz they just trigger
client rendering which itself might just postpone again or fill in the
value. The difference is how they get logged.

In Flight and Fizz they log to `onPostpone(reason)` instead of
`onError(error)`. This log is meant to help find deopts on the server
like finding places where you fall back to client rendering. The reason
that you pass in is for that purpose to help the reason for any deopts.

I do track the stack trace in DEV but I don't currently expose it to
`onPostpone`. This seems like a limitation. It might be better to expose
the Postpone object which is an Error object but that's more of an
implementation detail. I could also pass it as a second argument.

On the client after hydration they don't get passed to
`onRecoverableError`. There's no global `onPostpone` API to capture
postponed things on the client just like there's no `onError`. At that
point it's just assumed to be intentional. It doesn't have any `digest`
or reason passed to the client since it's not logged.

There are some hacky solutions that currently just tries to reuse as
much of the existing code as possible but should be more properly
implemented.
- Fiber is currently just converting it to a fake Promise object so that
it behaves like an infinite Promise.
- Fizz is encoding the magic digest string `"POSTPONE"` in the HTML so
we know to ignore it but it should probably just be something neater
that doesn't share namespace with digests.

Next I plan on using this in the `/static` entry points for additional
features.

Why "postpone"? It's basically a synonym to "defer" but we plan on using
"defer" for other purposes and it's overloaded anyway.
2023-08-17 13:26:14 -04:00
Josh StoryandGitHub 5ea1397b2b Remove excess validation (#27223)
stacked on #27222 

When I initially developed Float I was trying to be extremely clever in
how to explain when there are mismatching Resource instances. I still
think that we should do some kind of validation here but I want to
implement something much simpler. In practice there are not many cases
where you would accidentally create the same resource twice but with
differing props. Since I am going to land `preloadModule` and
`preinitModule` soon and I want to avoid adding to the overly complex
dev validation there I am going to remove it now and add something later
that is simplified.
2023-08-14 10:08:55 -07:00
Josh StoryandGitHub 1a001dac62 [Fizz][Float] When src or srcSet is a data URI we should not preload the image (#27218)
Data URI's in images can't effectively be preloaded (the URI contains
the data so preloading only duplicates the data in the stream. If we
encounter an image with this protocol in the src attribute we should
avoid preloading it.
2023-08-12 09:21:45 -07:00
Josh StoryandGitHub 4e3618ae41 [Float][Fizz] Fix srcSet and sizes handling for suspensey img preloads (#27217)
imageSrcSet should have been srcSet when referencing an img tag.
imageSizes should have been sizes. This caused preloads for img tags
using srcSet and sizes to incorrectly render as having a href only,
dropping the srcSet and sizes part of the preload
2023-08-11 11:27:22 -07:00
Josh StoryandGitHub f359f9b41a [Fizz] Preload "suspensey" images (#27191)
Eventually we will treat images without `loading="lazy"` as suspensey
meaning we will coordinate the reveal of boundaries when these images
have loaded and ideally decoded. As a step in that direction this change
prioritizes these images for preloading to ensure the highest chance
that they are loaded before boundaries reveal (or initial paint). every
img rendered that is non lazy loading will emit a preload just behind
fonts.

This change implements a new resource queue for high priority image
preloads

There are a number of scenarios where we end up putting a preload in
this queue

1. If you render a non-lazy image and there are fewer than 10 high
priority image preloads
2. if you render a non-lazy image with fetchPriority "high"
3. if you preload as "image" with fetchPriority "high"

This means that by default we won't overrsaturate this queue with every
img rendered on the page but the earlier encountered ones will go first.
Essentially this is React's own implementation of fetchPriority="auto".

If however you specify that the fetchPriority is higher then in theory
an unlimited number of images can preload in this queue. This gives
users some control over queuing while still providing a good default
that does not require any opting into

Additionally we use fetchPriority "low" as a signal that an image does
not require preloading. This may end up being pointless if not using
lazy (which also opts out of preloading) because it might delay initial
paint but we'll start with this hueristic and consider changes in the
future when we have more information
2023-08-10 15:39:19 -07:00
Josh StoryandGitHub cb3404a0cc [Fizz]: Unify preload queue (#27190)
Currently React attempts to prioritize certain preloads over others
based on their type. This is at odds with allowing the user to control
priority by ordering which calls are made first. There are some asset
types that generally should just be prioritized first such as fonts
since we don't know when fonts will be used and they either block
display or may lead to fallback fonts being used. But for scripts and
stylesheets we can emit them in the order received with other arbitrary
preload types.

We will eventually add support for emitting suspensey image preloads
before other resources because these also block display however that
implementation will look at which images are actually rendered rather
than simply preloaded.
2023-08-07 15:51:20 -07:00
Josh StoryandGitHub 9edf470d6e [Fizz] declare bootstrap script preloads to be fetchPriority: 'low' (#27189)
Generally scripts should not be preloaded before images but if they
arrive earlier than image preloads (or images) the network (or server)
may be saturated responding to inflight script preloads and not
sufficiently prioritize images arriving later. This change marks the
preloaded bootstrap script with a `low` fetch priority to signal to
supporting browsers that the request should be deprioritized. This
should make the preload operate similar to async script fetch priority
which is low by default according to https://web.dev/fetch-priority/

Additionally the bootstrap script preloads will emit before
preinitialized scripts do. Normal script preloads will continue to be
prioritized after stylesheets

This change can land separatrely but is part of a larger effort to
implement elevating image loading and making script loading less
blocking. Later changes will emit used suspensey images earlier in the
queue and will stop favoring scripts over images that are explicitly
preloaded
2023-08-07 15:44:34 -07:00
StevenandGitHub 9377e10105 Add referrerPolicy option to ReactDOM.preload() (#27096) 2023-07-12 14:32:56 -07:00