In React 19 React will finally stop publishing UMD builds. This is
motivated primarily by the lack of use of UMD format and the added
complexity of maintaining build infra for these releases. Additionally
with ESM becoming more prevalent in browsers and services like esm.sh
which can host React as an ESM module there are other options for doing
script tag based react loading.
This PR removes all the UMD build configs and forks.
There are some fixtures that still have references to UMD builds however
many of them already do not work (for instance they are using legacy
features like ReactDOM.render) and rather than block the removal on
these fixtures being brought up to date we'll just move forward and fix
or removes fixtures as necessary in the future.
This adds support in Flight for serializing four kinds of streams:
- `ReadableStream` with objects as a model. This is a single shot
iterator so you can read it only once. It can contain any value
including Server Components. Chunks are encoded as is so if you send in
10 typed arrays, you get the same typed arrays out on the other side.
- Binary `ReadableStream` with `type: 'bytes'` option. This supports the
BYOB protocol. In this mode, the receiving side just gets `Uint8Array`s
and they can be split across any single byte boundary into arbitrary
chunks.
- `AsyncIterable` where the `AsyncIterator` function is different than
the `AsyncIterable` itself. In this case we assume that this might be a
multi-shot iterable and so we buffer its value and you can iterate it
multiple times on the other side. We support the `return` value as a
value in the single completion slot, but you can't pass values in
`next()`. If you want single-shot, return the AsyncIterator instead.
- `AsyncIterator`. These gets serialized as a single-shot as it's just
an iterator.
`AsyncIterable`/`AsyncIterator` yield Promises that are instrumented
with our `.status`/`.value` convention so that they can be synchronously
looped over if available. They are also lazily parsed upon read.
We can't do this with `ReadableStream` because we use the native
implementation of `ReadableStream` which owns the promises.
The format is a leading row that indicates which type of stream it is.
Then a new row with the same ID is emitted for every chunk. Followed by
either an error or close row.
`AsyncIterable`s can also be returned as children of Server Components
and then they're conceptually the same as fragment arrays/iterables.
They can't actually be used as children in Fizz/Fiber but there's a
separate plan for that. Only `AsyncIterable` not `AsyncIterator` will be
valid as children - just like sync `Iterable` is already supported but
single-shot `Iterator` is not. Notably, neither of these streams
represent updates over time to a value. They represent multiple values
in a list.
When the server stream is aborted we also close the underlying stream.
However, closing a stream on the client, doesn't close the underlying
stream.
A couple of possible follow ups I'm not planning on doing right now:
- [ ] Free memory by releasing the buffer if an Iterator has been
exhausted. Single shots could be optimized further to release individual
items as you go.
- [ ] We could clean up the underlying stream if the only pending data
that's still flowing is from streams and all the streams have cleaned
up. It's not very reliable though. It's better to do cancellation for
the whole stream - e.g. at the framework level.
- [ ] Implement smarter Binary Stream chunk handling. Currently we wait
until we've received a whole row for binary chunks and copy them into
consecutive memory. We need this to preserve semantics when passing
typed arrays. However, for binary streams we don't need that. We can
just send whatever pieces we have so far.
With the enableBinaryFlight flag on we should encode typed arrays and
blobs in the Reply direction too for parity.
It's already possible to pass Blobs inside FormData but you should be
able to pass them inside objects too.
We encode typed arrays as blobs and then unwrap them automatically to
the right typed array type.
Unlike the other protocol, I encode the type as a reference tag instead
of row tag. Therefore I need to rename the tags to avoid conflicts with
other tags in references. We are running out of characters though.
We have a different set of dispatchers that Flight uses. This also
includes the `jsx-runtime` which must also be aliased to use the right
version.
To ensure the right versions are used together we rename the export of
the SharedInternals from 'react' and alias it in relevant bundles.
This is similar to #28771 but for isomorphic. We need a make over for
these dispatchers anyway so this is the first step. Also helps flush out
some internals usage that will break anyway.
It flattens the inner mutable objects onto the ReactSharedInternals.
We used to assume that outlined models are emitted before the reference
(which was true before Blobs). However, it still wasn't safe to assume
that all the data will be available because an "import" (client
reference) can be async and therefore if it's directly a child of an
outlined model, it won't be able to update in place.
This is a similar problem as the one hit by @unstubbable in #28669 with
elements, but a little different since these don't follow the same way
of wrapping.
I don't love the structuring of this code which now needs to pass a
first class mapper instead of just being known code. It also shares the
host path which is just an identity function. It wouldn't necessarily
pass my own review but I don't have a better one for now. I'd really
prefer if this was done at a "row" level but that ends up creating even
more code.
Add test for Blob in FormData and async modules in Maps.
We currently support Blobs when passing from Client to Server so this
adds it in the other direction for parity - when `enableFlightBinary` is
enabled.
We intentionally only support the `Blob` type to pass-through, not
subtype `File`. That's because passing additional meta data like
filename might be an accidental leak. You can still pass a `File`
through but it'll appear as a `Blob` on the other side. It's also not
possible to create a faithful File subclass in all environments without
it actually being backed by a file.
This implementation isn't great but at least it works. It creates a few
indirections. This is because we need to be able to asynchronously emit
the buffers but we have to "block" the parent object from resolving
while it's loading.
Ideally, we should be able to create the Blob on the client early and
then stream in it lazily. Because the Blob API doesn't guarantee that
the data is available synchronously. Unfortunately, the native APIs
doesn't have this. We could implement custom versions of all the data
read APIs but then the blobs still wouldn't work with native APIs. So we
just have to wait until Blob accepts a stream in the constructor.
We should be able to stream each chunk early in the protocol though even
though we can't unblock the parent until they've all loaded. I didn't do
this yet mostly because of code structure and I'm lazy.
This implements the concept of a DEV-only "owner" for Server Components.
The owner concept isn't really super useful. We barely use it anymore,
but we do have it as a concept in DevTools in a couple of cases so this
adds it for parity. However, this is mainly interesting because it could
be used to wire up future owner-based stacks.
I do this by outlining the DebugInfo for a Server Component
(ReactComponentInfo). Then I just rely on Flight deduping to refer to
that. I refer to the same thing by referential equality so that we can
associate a Server Component parent in DebugInfo with an owner.
If you suspend and replay a Server Component, we have to restore the
same owner. To do that, I did a little ugly hack and stashed it on the
thenable state object. Felt unnecessarily complicated to add a stateful
wrapper for this one dev-only case.
The owner could really be anything since it could be coming from a
different implementation. Because this is the first time we have an
owner other than Fiber, I have to fix up a bunch of places that assumes
Fiber. I mainly did the `typeof owner.tag === 'number'` to assume it's a
Fiber for now.
This also doesn't actually add it to DevTools / RN Inspector yet. I just
ignore them there for now.
Because Server Components can be async the owner isn't tracked after an
await. We need per-component AsyncLocalStorage for that. This can be
done in a follow up.
Alternative to #28620.
Instead of emitting lazy references to not-yet-emitted models in the
Flight Server, this fixes the observed issue in
https://github.com/unstubbable/ai-rsc-test/pull/1 by adjusting the lazy
model resolution in the Flight Client to update stale blocked root
models, before assigning them as chunk values. In addition, the element
props are not outlined anymore in the Flight Server to avoid having to
also handle their staleness in blocked elements.
fixes#28595
## Summary
Fixes a type validation error introduced in newer versions of Node.js
when calling `Module.prototype._compile` in our unit tests. (I tried but
have yet to pinpoint the precise change in Node.js that introduced this
vaildation.)
The specific error that currently occurs when running unit tests with
Node.js v18.16.1:
```
TypeError: The "mod" argument must be an instance of Module. Received an instance of Object
80 |
81 | if (useServer) {
> 82 | originalCompile.apply(this, arguments);
| ^
83 |
84 | const moduleId: string = (url.pathToFileURL(filename).href: any);
85 |
```
This fixes the type validation error by mocking modules using `new
Module()` instead of plain objects.
## How did you test this change?
Ran the unit tests successfully:
```
$ node --version
v18.16.1
$ yarn test
```
This bumps the canary versions to v19 to communicate that the next
release will be a major. Once this lands, we can start merging breaking
changes into `main`.
## Overview
_Depends on https://github.com/facebook/react/pull/28514_
This PR adds a new React hook called `useActionState` to replace and
improve the ReactDOM `useFormState` hook.
## Motivation
This hook intends to fix some of the confusion and limitations of the
`useFormState` hook.
The `useFormState` hook is only exported from the `ReactDOM` package and
implies that it is used only for the state of `<form>` actions, similar
to `useFormStatus` (which is only for `<form>` element status). This
leads to understandable confusion about why `useFormState` does not
provide a `pending` state value like `useFormStatus` does.
The key insight is that the `useFormState` hook does not actually return
the state of any particular form at all. Instead, it returns the state
of the _action_ passed to the hook, wrapping it and returning a
trackable action to add to a form, and returning the last returned value
of the action given. In fact, `useFormState` doesn't need to be used in
a `<form>` at all.
Thus, adding a `pending` value to `useFormState` as-is would thus be
confusing because it would only return the pending state of the _action_
given, not the `<form>` the action is passed to. Even if we wanted to
tie them together, the returned `action` can be passed to multiple
forms, creating confusing and conflicting pending states during multiple
form submissions.
Additionally, since the action is not related to any particular
`<form>`, the hook can be used in any renderer - not only `react-dom`.
For example, React Native could use the hook to wrap an action, pass it
to a component that will unwrap it, and return the form result state and
pending state. It's renderer agnostic.
To fix these issues, this PR:
- Renames `useFormState` to `useActionState`
- Adds a `pending` state to the returned tuple
- Moves the hook to the `'react'` package
## Reference
The `useFormState` hook allows you to track the pending state and return
value of a function (called an "action"). The function passed can be a
plain JavaScript client function, or a bound server action to a
reference on the server. It accepts an optional `initialState` value
used for the initial render, and an optional `permalink` argument for
renderer specific pre-hydration handling (such as a URL to support
progressive hydration in `react-dom`).
Type:
```ts
function useActionState<State>(
action: (state: Awaited<State>) => State | Promise<State>,
initialState: Awaited<State>,
permalink?: string,
): [state: Awaited<State>, dispatch: () => void, boolean];
```
The hook returns a tuple with:
- `state`: the last state the action returned
- `dispatch`: the method to call to dispatch the wrapped action
- `pending`: the pending state of the action and any state updates
contained
Notably, state updates inside of the action dispatched are wrapped in a
transition to keep the page responsive while the action is completing
and the UI is updated based on the result.
## Usage
The `useActionState` hook can be used similar to `useFormState`:
```js
import { useActionState } from "react"; // not react-dom
function Form({ formAction }) {
const [state, action, isPending] = useActionState(formAction);
return (
<form action={action}>
<input type="email" name="email" disabled={isPending} />
<button type="submit" disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</form>
);
}
```
But it doesn't need to be used with a `<form/>` (neither did
`useFormState`, hence the confusion):
```js
import { useActionState, useRef } from "react";
function Form({ someAction }) {
const ref = useRef(null);
const [state, action, isPending] = useActionState(someAction);
async function handleSubmit() {
// See caveats below
await action({ email: ref.current.value });
}
return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}
```
## Benefits
One of the benefits of using this hook is the automatic tracking of the
return value and pending states of the wrapped function. For example,
the above example could be accomplished via:
```js
import { useActionState, useRef } from "react";
function Form({ someAction }) {
const ref = useRef(null);
const [state, setState] = useState(null);
const [isPending, setIsPending] = useTransition();
function handleSubmit() {
startTransition(async () => {
const response = await someAction({ email: ref.current.value });
setState(response);
});
}
return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}
```
However, this hook adds more benefits when used with render specific
elements like react-dom `<form>` elements and Server Action. With
`<form>` elements, React will automatically support replay actions on
the form if it is submitted before hydration has completed, providing a
form of partial progressive enhancement: enhancement for when javascript
is enabled but not ready.
Additionally, with the `permalink` argument and Server Actions,
frameworks can provide full progressive enhancement support, submitting
the form to the URL provided along with the FormData from the form. On
submission, the Server Action will be called during the MPA navigation,
similar to any raw HTML app, server rendered, and the result returned to
the client without any JavaScript on the client.
## Caveats
There are a few Caveats to this new hook:
**Additional state update**: Since we cannot know whether you use the
pending state value returned by the hook, the hook will always set the
`isPending` state at the beginning of the first chained action,
resulting in an additional state update similar to `useTransition`. In
the future a type-aware compiler could optimize this for when the
pending state is not accessed.
**Pending state is for the action, not the handler**: The difference is
subtle but important, the pending state begins when the return action is
dispatched and will revert back after all actions and transitions have
settled. The mechanism for this under the hook is the same as
useOptimisitic.
Concretely, what this means is that the pending state of
`useActionState` will not represent any actions or sync work performed
before dispatching the action returned by `useActionState`. Hopefully
this is obvious based on the name and shape of the API, but there may be
some temporary confusion.
As an example, let's take the above example and await another action
inside of it:
```js
import { useActionState, useRef } from "react";
function Form({ someAction, someOtherAction }) {
const ref = useRef(null);
const [state, action, isPending] = useActionState(someAction);
async function handleSubmit() {
await someOtherAction();
// The pending state does not start until this call.
await action({ email: ref.current.value });
}
return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}
```
Since the pending state is related to the action, and not the handler or
form it's attached to, the pending state only changes when the action is
dispatched. To solve, there are two options.
First (recommended): place the other function call inside of the action
passed to `useActionState`:
```js
import { useActionState, useRef } from "react";
function Form({ someAction, someOtherAction }) {
const ref = useRef(null);
const [state, action, isPending] = useActionState(async (data) => {
// Pending state is true already.
await someOtherAction();
return someAction(data);
});
async function handleSubmit() {
// The pending state starts at this call.
await action({ email: ref.current.value });
}
return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}
```
For greater control, you can also wrap both in a transition and use the
`isPending` state of the transition:
```js
import { useActionState, useTransition, useRef } from "react";
function Form({ someAction, someOtherAction }) {
const ref = useRef(null);
// isPending is used from the transition wrapping both action calls.
const [isPending, startTransition] = useTransition();
// isPending not used from the individual action.
const [state, action] = useActionState(someAction);
async function handleSubmit() {
startTransition(async () => {
// The transition pending state has begun.
await someOtherAction();
await action({ email: ref.current.value });
});
}
return (
<div>
<input ref={ref} type="email" name="email" disabled={isPending} />
<button onClick={handleSubmit} disabled={isPending}>
Submit
</button>
{state.errorMessage && <p>{state.errorMessage}</p>}
</div>
);
}
```
A similar technique using `useOptimistic` is preferred over using
`useTransition` directly, and is left as an exercise to the reader.
## Thanks
Thanks to @ryanflorence @mjackson @wesbos
(https://github.com/facebook/react/issues/27980#issuecomment-1960685940)
and [Allan
Lasser](https://allanlasser.com/posts/2024-01-26-avoid-using-reacts-useformstatus)
for their feedback and suggestions on `useFormStatus` hook.
This a follow up to #28564. It's alternative to #28609 which takes
#28610 into account.
It used to be possible to return JSX from an action with
`useActionState`.
```js
async function action(errors, payload) {
"use server";
try {
...
} catch (x) {
return <div>Error message</div>;
}
}
```
```js
const [errors, formAction] = useActionState(action);
return <div>{errors}</div>;
```
Returning JSX from an action is itself not anything problematic. It's
that it also has to return the previous state to the action reducer
again that's the problem. When this happens we accidentally could
serialize an Element back to the server.
I fixed this in #28564 so it's now blocked if you don't have a temporary
reference set.
However, you can't have that for the progressive enhancement case. The
reply is eagerly encoded as part of the SSR render. Typically you
wouldn't have these in the initial state so the common case is that they
show up after the first POST back that yields an error and it's only in
the no-JS case where this happens so it's hard to discover.
As noted in #28609 there's a security implication with allowing elements
to be sent across this kind of payload, so we can't just make it work.
When an error happens during SSR our general policy is to try to recover
on the client instead. After all, SSR is mainly a perf optimization in
React terms and it's not primarily intended for a no JS solution.
This PR takes the approach that if we fail to generate the progressive
enhancement payload. I.e. if the serialization of previous state /
closures throw. Then we fallback to the replaying semantics just client
actions instead which will succeed.
The effect of this is that this pattern mostly just works:
- First render in the typical case doesn't have any JSX in it so it just
renders a progressive enhanced form.
- If JS fails to hydrate or you click early we do a form POST. If that
hits an error and it tries to render it using JSX, then the new page
will render successfully - however this time with a Replaying form
instead.
- If you try to submit the form again it'll have to be using JS.
Meaning if you use JSX as the error return value of form state and you
make a first attempt that fails, then no JS won't work because either
the first or second attempt has to hydrate.
We have ideas for potentially optimizing away serializing unused
arguments like if you don't actually use previous state which would also
solve it but it wouldn't cover all cases such as if it was deeply nested
in complex state.
Another approach that I considered was to poison the prev state if you
passed an element back but let it through to the action but if you try
to render the poisoned value, it wouldn't work. The downside of this is
when to error. Because in the progressive enhancement case it wouldn't
error early but when you actually try to invoke it at which point it
would be too late to fallback to client replaying. It would probably
have to always error even on the client which is unfortunate since this
mostly just works as long as it hydrates.
Currently you can accidentally pass React Element to a Server Action. It
warns but in prod it actually works because we can encode the symbol and
otherwise it's mostly a plain object. It only works if you only pass
host components and no function props etc. which makes it potentially
error later. The first thing this does it just early hard error for
elements.
I made Lazy work by unwrapping though since that will be replaced by
Promises later which works.
Our protocol is not fully symmetric in that elements flow from Server ->
Client. Only the Server can resolve Components and only the client
should really be able to receive host components. It's not intended that
a Server can actually do something with them other than passing them to
the client.
In the case of a Reply, we expect the client to be stateful. It's
waiting for a response. So anything we can't serialize we can still pass
by reference to an in memory object. So I introduce the concept of a
TemporaryReferenceSet which is an opaque object that you create before
encoding the reply. This then stashes any unserializable values in this
set and encode the slot by id. When a new response from the Action then
returns we pass the same temporary set into the parser which can then
restore the objects. This lets you pass a value by reference to the
server and back into another slot.
For example it can be used to render children inside a parent tree from
a server action:
```
export async function Component({ children }) {
"use server";
return <div>{children}</div>;
}
```
(You wouldn't normally do this due to the waterfalls but for advanced
cases.)
A common scenario where this comes up accidentally today is in
`useActionState`.
```
export function action(state, formData) {
"use server";
if (errored) {
return <div>This action <strong>errored</strong></div>;
}
return null;
}
```
```
const [errors, formAction] = useActionState(action);
return <div>{errors}<div>;
```
It feels like I'm just passing the JSX from server to client. However,
because `useActionState` also sends the previous state *back* to the
server this should not actually be valid. Before this PR this actually
worked accidentally. You get a DEV warning but it used to work in prod.
Once you do something like pass a client reference it won't work tho. We
could perhaps make client references work by stashing where we got them
from but it wouldn't work with all possible JSX.
By adding temporary references to the action implementation this will
work again - on the client. It'll also be more efficient since we don't
send back the JSX content that you shouldn't introspect on the server
anyway.
However, a flaw here is that the progressive enhancement of this case
won't work because we can't use temporary references for progressive
enhancement since there's no in memory stash. What is worse is that it
won't error if you hydrate. ~It also will error late in the example
above because the first state is "undefined" so invoking the form once
works - it errors on the second attempt when it tries to send the error
state back again.~ It actually errors on the first invocation because we
need to eagerly serialize "previous state" into the form. So at least
that's better.
I think maybe the solution to this particular pattern would be to allow
JSX to serialize if you have no temporary reference set, and remember
client references so that client references can be returned back to the
server as client references. That way anything you could send from the
server could also be returned to the server. But it would only deopt to
serializing it for progressive enhancement. The consequence of that
would be that there's a lot of JSX that might accidentally seem like it
should work but it's only if you've gotten it from the server before
that it works. This would have to have pair them somehow though since
you can't take a client reference from one implementation of Flight and
use it with another.
## Overview
Adds a `pending` state to useFormState, which will be replaced by
`useActionState` in the next diff. We will keep `useFormState` around
for backwards compatibility, but functionally it will work the same as
`useActionState`, which has an `isPending` state returned.
The idea here is that host dispatchers are not bound to renders so we
need to be able to dispatch to them at any time. This updates the
implementation to chain these dispatchers so that each renderer can
respond to the dispatch. Semantically we don't always want every
renderer to do this for instance if Fizz handles a float method we don't
want Fiber to as well so each dispatcher implementation can decide if it
makes sense to forward the call or not. For float methods server
disaptchers will handle the call if they can resolve a Request otherwise
they will forward. For client dispatchers they will handle the call and
always forward. The choice needs to be made for each dispatcher method
and may have implications on correct renderer import order. For now we
just live with the restriction that if you want to use server and client
together (such as renderToString in the browser) you need to import the
server renderer after the client renderer.
This won't ever be serialized and is likely just a mistake.
This should be covered by the "use server" compiler since it ensures
that something that accepts a "this" won't be allowed to compile and if
it doesn't accept it, TypeScript should ideally forbid it to be passed.
So maybe this is unnecessary.
It's possible for the same function instance to appear more than once in
the same graph or even the same file.
Currently this errors on trying to reconfigure the property but it
really doesn't matter which one wins. First or last.
Regardless there will be an entry point generated that can get them.
Alternative to #28354.
If a client reference is one of the props being describes as part of
another error, we call toString on it, which errors.
We should error explicitly when a Symbol prop is extracted.
However, pragmatically I added the toString symbol tag even though we
don't know what the real tostring will be but we also lie about the
typeof.
We can however in addition to this give it a different description
because describing this property as an object isn't quite right.
We probably could extract the export name but that's kind of renderer
specific and I just added this shared module to Fizz which doesn't have
that which is unfortunate an consequence.
For default exports we don't have a good name of what the alias was in
the receiver. Could maybe call it "default" but for now I just call it
"client".
The `reference` that is passed into `registerServerReference` can be a
plain function. It does not need to have the three additonal properties
of a `ServerRefeference`. In fact, adding these properties (plus `bind`)
is precisely what `registerServerReference` does.
Same as #28327 but for Fizz.
One thing that's weird about this recoverable error is that we don't
send the regular stack for it, just the component stack it seems. This
is missing some potential information and if we move toward integrated
since stacks it would be one thing.
There are three parts to an RSC set up:
- React
- Bundler
- Endpoints
Most customizability is in the bundler configs. We deal with those as
custom builds.
To create a full set up, you need to also configure ways to expose end
points for example to call a Server Action. That's typically not
something the bundler is responsible for even though it's responsible
for gathering the end points that needs generation. Exposing which
endpoints to generate is a responsibility for the bundler.
Typically a meta-framework is responsible for generating the end points.
There's two ways to "call" a Server Action. Through JS and through a
Form. Through JS we expose the `callServer` callback so that the
framework can call the end point.
Forms by default POST back to the current page with an action serialized
into form data, which we have a decoder helper for. However, this is not
something that React is really opinionated about just like we're not
opinionated about the protocol used by callServer.
This exposes an option to configure the encoding of the form props.
`encodeFormAction` is to the SSR is what `callServer` is to the Browser.
A Flight Server can be a consumer of a stream from another Server. In
this case the meta data is attached to debugInfo properties on lazy,
Promises, Arrays or Elements that might in turn get forwarded to the
next stream. In this case we want to forward this debug information to
the client in the stream.
I also added a DEV only `environmentName` option to the Flight Server.
This lets you name the server that is producing the debug info so that
you can trace the origin of where that component is executing. This
defaults to `"server"`. DevTools could use this for badges or different
colors.
This adds a new DEV-only row type `D` for DebugInfo. If we see this in
prod, that's an error. It can contain extra debug information about the
Server Components (or Promises) that were compiled away during the
server render. It's DEV-only since this can contain sensitive
information (similar to errors) and since it'll be a lot of data, but
it's worth using the same stream for simplicity rather than a
side-channel.
In this first pass it's just the Server Component's name but I'll keep
adding more debug info to the stream, and it won't always just be a
Server Component's stack frame.
Each row can get more debug rows data streaming in as it resolves and
renders multiple server components in a row.
The data structure is just a side-channel and it would be perfectly fine
to ignore the D rows and it would behave the same as prod. With this
data structure though the data is associated with the row ID / chunk, so
you can't have inline meta data. This means that an inline Server
Component that doesn't get an ID otherwise will need to be outlined. The
way I outline Server Components is using a direct reference where it's
synchronous though so on the client side it behaves the same (i.e.
there's no lazy wrapper in this case).
In most cases the `_debugInfo` is on the Promises that we yield and we
also expose this on the `React.Lazy` wrappers. In the case where it's a
synchronous render it might attach this data to Elements or Arrays
(fragments) too.
In a future PR I'll wire this information up with Fiber to stash it in
the Fiber data structures so that DevTools can pick it up. This property
and the information in it is not limited to Server Components. The name
of the property that we look for probably shouldn't be `_debugInfo`
since it's semi-public. Should consider the name we use for that.
If it's a synchronous render that returns a string or number (text node)
then we don't have anywhere to attach them to. We could add a
`React.Lazy` wrapper for those but I chose to prioritize keeping the
data structure untouched. Can be useful if you use Server Components to
render data instead of React Nodes.
Server Context was never documented, and has been deprecated in
https://github.com/facebook/react/pull/27424.
This PR removes it completely, including the implementation code.
Notably, `useContext` is removed from the shared subset, so importing it
from a React Server environment would now should be a build error in
environments that are able to enforce that.
Conceptually a Server Component in the tree is the same as a Client
Component.
When we render a Server Component with a key, that key should be used as
part of the reconciliation process to ensure the children's state are
preserved when they move in a set. The key of a child should also be
used to clear the state of the children when that key changes.
Conversely, if a Server Component doesn't have a key it should get an
implicit key based on the slot number. It should not inherit the key of
its children since the children don't know if that would collide with
other keys in the set the Server Component is rendered in.
A Client Component also has an identity based on the function's
implementation type. That mainly has to do with the state (or future
state after a refactor) that Component might contain. To transfer state
between two implementations it needs to be of the same state type. This
is not a concern for a Server Components since they never have state so
identity doesn't matter.
A Component returns a set of children. If it returns a single child,
that's the same as returning a fragment of one child. So if you
conditionally return a single child or a fragment, they should
technically reconcile against each other.
The simple way to do this is to simply emit a Fragment for every Server
Component. That would be correct in all cases. Unfortunately that is
also unfortunate since it bloats the payload in the common cases. It
also means that Fiber creates an extra indirection in the runtime.
Ideally we want to fold Server Component aways into zero cost on the
client. At least where possible. The common cases are that you don't
specify a key on a single return child, and that you do specify a key on
a Server Component in a dynamic set.
The approach in this PR treats a Server Component that returns other
Server Components or Lazy Nodes as a sequence that can be folded away.
I.e. the parts that don't generate any output in the RSC payload.
Instead, it keeps track of their keys on an internal "context". Which
gets reset after each new reified JSON node gets rendered.
Then we transfer the accumulated keys from any parent Server Components
onto the child element. In the simple case, the child just inherits the
key of the parent.
If the Server Component itself is keyless but a child isn't, we have to
add a wrapper fragment to ensure that this fragment gets the implicit
key but we can still use the key to reset state. This is unusual though
because typically if you keyed something it's because it was already in
a fragment.
In the case a Server Component is keyed but forks its children using a
fragment, we need to key that fragment so that the whole set can move
around as one. In theory this could be flattened into a parent array but
that gets tricky if something suspends, because then we can't send the
siblings early.
The main downside of this approach is that switching between single
child and fragment in a Server Component isn't always going to reconcile
against each other. That's because if we saw a single child first, we'd
have to add the fragment preemptively in case it forks later. This
semantic of React isn't very well known anyway and it might be ok to
break it here for pragmatic reasons. The tests document this
discrepancy.
Another compromise of this approach is that when combining keys we don't
escape them fully. We instead just use a simple `,` separated concat.
This is probably good enough in practice. Additionally, since we don't
encode the implicit 0 index slot key, you can move things around between
parents which shouldn't really reconcile but does. This keeps the keys
shorter and more human readable.
Starting in version 19, users can import the `act` testing API from the
`react` package instead of using a renderer specific API, like
`react-dom/test-utils`.
This refactors the Flight render loop to behave more like Fizz with
similar naming conventions. So it's easier to apply similar techniques
across both. This is not necessarily better/faster - at least not yet.
This doesn't yet implement serialization by writing segments to chunks
but we probably should do that since the built-in parts that
`JSON.stringify` gets us isn't really much anymore (except serializing
strings). When we switch to that it probably makes sense for the whole
thing to be recursive.
Right now it's not technically fully recursive because each recursive
render returns the next JSON value to encode. So it's kind of like a
trampoline. This means we can't have many contextual things on the
stack. It needs to use the Server Context `__POP` trick. However, it
does work for things that are contextual only for one sequence of server
component abstractions in a row. Since those are now recursive.
An interesting observation here is that `renderModel` means that
anything can suspend while still serializing the outer siblings.
Typically only Lazy or Components would suspend but in principle a Proxy
can suspend/postpone too and now that is left serialized by reference to
a future value. It's only if the thing that we rendered was something
that can reduce to Lazy e.g. an Element that we can serialize it as a
lazy.
Similarly to how Suspense boundaries in Fizz can catch errors, anything
that can be reduced to Lazy can also catch an error rather than bubbling
it. It only errors when the Lazy resolves. Unlike Suspense boundaries
though, those things don't render anything so they're otherwise going to
use the destructive form. To ensure that throwing in an Element can
reuse the current task, this must be handled by `renderModel`, not for
example `renderElement`.
The internal file ReactSharedSubset is what the `react` module resolves
to when imported from a Server Component environment. We gave it this
name because, originally, the idea was that Server Components can access
a subset of the APIs available on the client.
However, since then, we've also added APIs that can _only_ by accessed
on the server and not the client. In other words, it's no longer a
subset, it's a slightly different overlapping set.
So this commit renames ReactSharedSubet to ReactServer and updates all
the references. This does not affect the public API, only our internal
implementation.
If this is a client reference we shouldn't dot into it, which would
throw in the proxy.
Interestingly our client references don't really have a `name`
associated with them for debug information so a component type doesn't
show up in error logs even though it seems like it should.
Postponing in a promise that is being serialized to the client from the
server should be possible however prior to this change Flight treated
this case like an error rather than a postpone. This fix adds support
for postponing in this position and adds a test asserting you can
successfully prerender the root if you unwrap this promise inside a
suspense boundary.
As part of the process of removing the deprecated `react-dom/test-utils`
package references to `act` from this module are replaced with
references to `unstable_act` in `react`. It is likely that the unstable
act implementation will be made stable. The test utils act is just a
reexport of the unstable_act implementation in react itself.
Updates useFormState to allow a sync function to be passed as an action.
A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.
I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.
creating the root after closing the response can lead to a promise that
never rejects. This is not intended use of the decodeReply API but if
pathalogical cases where you pass a raw FormData into this fucntion with
no zero chunk it can hang forever. This reordering causes a connection
error instead
---------
Co-authored-by: Zack Tanner <zacktanner@gmail.com>
Now that we no longer support Server Context, we can now deduplicate
objects. It's not completely safe for useId but only in the same way as
it's not safe if you reuse elements on the client, so it's not a new
issue.
This also solves cyclic object references.
The issue is that we prefer to inline objects into a plain JSON format
when an object is not going to get reused. In this case the object
doesn't have an id. We could potentially serialize a reference to an
existing model + a path to it but it bloats the format and complicates
the client.
In a smarter flush phase like we have in Fizz we could choose to inline
or outline depending on what we've discovered so far before a flush. We
can't do that here since we use native stringify. However, even in that
solution you might not know that you're going to discover the same
object later so it's not perfect deduping anyway.
Instead, I use a heuristic where I mark previously seen objects and if I
ever see that object again, then I'll outline it. The idea is that most
objects are just going to be emitted once and if it's more than once
it's fairly likely you have a shared reference to it somewhere and it
might be more than two.
The third object gets deduplicated (or "detriplicated").
It's not a perfect heuristic because when we write the second object we
will have already visited all the nested objects inside of it, which
causes us to outline every nested object too even those weren't
reference more than by that parent. Not sure how to solve for that.
If we for some other reason outline an object such as if it suspends,
then it's truly deduplicated since it already has an id.
Upgrades the stability of Server Actions from experimental to canary.
- Turns on enableAsyncActions and enableFormActions
- Removes "experimental_" prefix from useOptimistic, useFormStatus, and
useFormState