Stacked on #29491
Previously if you aborted during a render the currently rendering task
would itself be aborted which will cause the entire model to be replaced
by the aborted error rather than just the slot currently being rendered.
This change updates the abort logic to mark currently rendering tasks as
aborted but allowing the current render to emit a partially serialized
model with an error reference in place of the current model.
The intent is to support aborting from rendering synchronously, in
microtasks (after an await or in a .then) and in lazy initializers. We
don't specifically support aborting from things like proxies that might
be triggered during serialization of props
While most builds of Flight and Fizz schedule work in new tasks some do
execute work synchronously. While this is necessary for legacy APIs like
renderToString for modern APIs there really isn't a great reason to do
this synchronously.
We could schedule works as microtasks but we actually want to yield so
the runtime can run events and other things that will unblock additional
work before starting the next work loop.
This change updates all non-legacy uses to be async using the best
availalble macrotask scheduler.
Browser now uses postMessage
Bun uses setTimeout because while it also supports setImmediate the
scheduling is not as eager as the same API in node
the FB build also uses setTimeout
This change required a number of changes to tests which were utilizing
the sync nature of work in the Browser builds to avoid having to manage
timers and tasks. I added a patch to install MessageChannel which is
required by the browser builds and made this patched version integrate
with the Scheduler mock. This way we can effectively use `act` to flush
flight and fizz work similar to how we do this on the client.
This lets you click a stack frame on the client and see the Server
source code inline.
<img width="871" alt="Screenshot 2024-06-01 at 11 44 24 PM"
src="https://github.com/facebook/react/assets/63648/581281ce-0dce-40c0-a084-4a6d53ba1682">
<img width="840" alt="Screenshot 2024-06-01 at 11 43 37 PM"
src="https://github.com/facebook/react/assets/63648/00dc77af-07c1-4389-9ae0-cf1f45199efb">
We could do some logic on the server that sends a source map url for
every stack frame in the RSC payload. That would make the client
potentially config free. However regardless we need the config to
describe what url scheme to use since that’s not built in to the bundler
config. In practice you likely have a common pattern for your source
maps so no need to send data over and over when we can just have a
simple function configured on the client.
The server must return a source map, even if the file is not actually
compiled since the fake file is still compiled.
The source mapping strategy can be one of two models depending on if the
server’s stack traces (`new Error().stack`) are source mapped back to
the original (`—enable-source-maps`) or represents the location in
compiled code (like in the browser).
If it represents the location in compiled code it’s actually easier. You
just serve the source map generated for that file by the tooling.
If it is already source mapped it has to generate a source map where
everything points to the same location (as if not compiled) ideally with
a segment per logical ast node.
This didn't actually fail before but I'm just adding an extra check.
Currently Client References are always "function" proxies so they never
fall into this branch. However, we do in theory support objects as
client references too depending on environment. We have checks
elsewhere. So this just makes that consistent.
This is necessary to simplify the component stack handling to make way
for owner stacks. It also solves some hacks that we used to have but
don't quite make sense. It also solves the problem where things like key
warnings get silenced in RSC because they get deduped. It also surfaces
areas where we were missing key warnings to begin with.
Almost every type of warning is issued from the renderer. React Elements
are really not anything special themselves. They're just lazily invoked
functions and its really the renderer that determines there semantics.
We have three types of warnings that previously fired in
JSX/createElement:
- Fragment props validation.
- Type validation.
- Key warning.
It's nice to be able to do some validation in the JSX/createElement
because it has a more specific stack frame at the callsite. However,
that's the case for every type of component and validation. That's the
whole point of enableOwnerStacks. It's also not sufficient to do it in
JSX/createElement so we also have validation in the renderers too. So
this validation is really just an eager validation but also happens
again later.
The problem with these is that we don't really know what types are valid
until we get to the renderer. Additionally, by placing it in the
isomorphic code it becomes harder to do deduping of warnings in a way
that makes sense for that renderer. It also means we can't reuse logic
for managing stacks etc.
Fragment props validation really should just be part of the renderer
like any other component type. This also matters once we add Fragment
refs and other fragment features. So I moved this into Fiber. However,
since some Fragments don't have Fibers, I do the validation in
ChildFiber instead of beginWork where it would normally happen.
For `type` validation we already do validation when rendering. By
leaving it to the renderer we don't have to hard code an extra list.
This list also varies by context. E.g. class components aren't allowed
in RSC but client references are but we don't have an isomorphic way to
identify client references because they're defined by the host config so
the current logic is flawed anyway. I kept the early validation for now
without the `enableOwnerStacks` since it does provide a nicer stack
frame but with that flag on it'll be handled with nice stacks anyway. I
normalized some of the errors to ensure tests pass.
For `key` validation it's the same principle. The mechanism for the
heuristic is still the same - if it passes statically through a parent
JSX/createElement call then it's considered validated. We already did
print the error later from the renderer so this also disables the early
log in the `enableOwnerStacks` flag.
I also added logging to Fizz so that key warnings can print in SSR logs.
Flight is a bit more complex. For elements that end up on the client we
just pass the `validated` flag along to the client and let the client
renderer print the error once rendered. For server components we log the
error from Flight with the server component as the owner on the stack
which will allow us to print the right stack for context. The factoring
of this is a little tricky because we only want to warn if it's in an
array parent but we want to log the error later to get the right debug
info.
Fiber/Fizz has a similar factoring problem that causes us to create a
fake Fiber for the owner which means the logs won't be associated with
the right place in DevTools.
Follow up to https://github.com/facebook/react/pull/29201. If a chunk
had listeners attached already (e.g. because `.then` was called on the
chunk returned from `createFromReadableStream`),
`wakeChunkIfInitialized` would overwrite any listeners added during
chunk initialization. This caused cyclic [path
references](https://github.com/facebook/react/pull/28996) within that
chunk to never resolve. Fixed by merging the two arrays of listeners.
Fixes#29200
The cyclic state might have added listeners that will still need to be
invoked. This happens if we have a cyclic reference AND end up blocked.
We have already cleared these before entering the parsing when we enter
the CYCLIC state so we they already have the right type. If listeners
are added during this phase they should carry over to the blocked state.
---------
Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de>
Stacked on #28997.
We can use the technique of referencing an object by its row + property
name path for temporary references - like we do for deduping. That way
we don't need to generate an ID for temporary references. Instead, they
can just be an opaque marker in the slot and it has the implicit ID of
the row + path.
Then we can stash all objects, even the ones that are actually available
to read on the server, as temporary references. Without adding anything
to the payload since the IDs are implicit. If the same object is
returned to the client, it can be referenced by reference instead of
serializing it back to the client. This also helps preserve object
identity.
We assume that the objects are immutable when they pass the boundary.
I'm not sure if this is worth it but with this mechanism, if you return
the `FormData` payload from a `useActionState` it doesn't have to be
serialized on the way back to the client. This is a common pattern for
having access to the last submission as "default value" to the form
fields. However you can still control it by replacing it with another
object if you want. In MPA mode, the temporary references are not
configured and so it needs to be serialized in that case. That's
required anyway for hydration purposes.
I'm not sure if people will actually use this in practice though or if
FormData will always be destructured into some other object like with a
library that turns it into typed data, and back. If so, the object
identity is lost.
Uses the same technique as in #28996 to encode references to already
emitted objects. This now means that Reply can support cyclic objects
too for parity.
Instead of forcing an object to be outlined to be able to refer to it
later we can refer to it by the property path inside another parent
object.
E.g. this encodes such a reference as `'$123:props:children:foo:bar'`.
That way we don't have to preemptively outline object and we can dedupe
after the first time we've found it.
There's no cost on the client if it's not used because we're not storing
any additional information preemptively.
This works mainly because we only have simple JSON objects from the root
reference. Complex objects like Map, FormData etc. are stored as their
entries array in the look up and not the complex object. Other complex
objects like TypedArrays or imports don't have deeply nested objects in
them that can be referenced.
This solves the problem that we only dedupe after the third instance.
This dedupes at the second instance. It also solves the problem where
all nested objects inside deduped instances also are outlined.
The property paths can get pretty large. This is why a test on payload
size increased. We could potentially outline the reference itself at the
first dupe. That way we get a shorter ID to refer to in the third
instance.
This is the first step to experimenting with a new type of stack traces
behind the `enableOwnerStacks` flag - in DEV only.
The idea is to generate stacks that are more like if the JSX was a
direct call even though it's actually a lazy call. Not only can you see
which exact JSX call line number generated the erroring component but if
that's inside an abstraction function, which function called that
function and if it's a component, which component generated that
component. For this to make sense it really need to be the "owner" stack
rather than the parent stack like we do for other component stacks. On
one hand it has more precise information but on the other hand it also
loses context. For most types of problems the owner stack is the most
useful though since it tells you which component rendered this
component.
The problem with the platform in its current state is that there's two
ways to deal with stacks:
1) `new Error().stack`
2) `console.createTask()`
The nice thing about `new Error().stack` is that we can extract the
frames and piece them together in whatever way we want. That is great
for constructing custom UIs like error dialogs. Unfortunately, we can't
take custom stacks and set them in the native UIs like Chrome DevTools.
The nice thing about `console.createTask()` is that the resulting stacks
are natively integrated into the Chrome DevTools in the console and the
breakpoint debugger. They also automatically follow source mapping and
ignoreLists. The downside is that there's no way to extract the async
stack outside the native UI itself so this information cannot be used
for custom UIs like errors dialogs. It also means we can't collect this
on the server and then pass it to the client for server components.
The solution here is that we use both techniques and collect both an
`Error` object and a `Task` object for every JSX call.
The main concern about this approach is the performance so that's the
main thing to test. It's certainly too slow for production but it might
also be too slow even for DEV.
This first PR doesn't actually use the stacks yet. It just collects them
as the first step. The next step is to start utilizing this information
in error printing etc.
For RSC we pass the stack along across over the wire. This can be
concatenated on the client following the owner path to create an owner
stack leading back into the server. We'll later use this information to
restore fake frames on the client for native integration. Since this
information quickly gets pretty heavy if we include all frames, we strip
out the top frame. We also strip out everything below the functions that
call into user space in the Flight runtime. To do this we need to figure
out the frames that represents calling out into user space. The
resulting stack is typically just the one frame inside the owner
component's JSX callsite. I also eagerly strip out things we expect to
be ignoreList:ed anyway - such as `node_modules` and Node.js internals.
This is the same change as #28780 but for the Flight Reply receiver.
While it's not possible to create an "async module" reference in this
case - resolving a server reference can still be async if loading it
requires loading chunks like in a new server instance.
Since extracting a typed array from a Blob is async, that's also a case
where a dependency can be async.
This follows the same principle as in #28611.
We cannot serialize Blobs of a form data into HTML because you can't
initialize a file input to some value. However the serialization of
state in an Action can contain blobs. In this case we do error but
outside the try/catch that recovers to error to client replaying instead
of MPA mode. This errors earlier to ensure that this works.
Testing this is a bit annoying because JSDOM doesn't have any of the
Blob methods but the Blob needs to be compatible with FormData and the
FormData needs to be compatible with `<form>` nodes in these tests. So I
polyfilled those in JSDOM with some hacks.
A possible future enhancement would be to encode these blobs in a base64
mode instead and have some way to receive them on the server. It's just
a matter of layering this. I think the RSC layer's `FORM_DATA`
implementation can pass some flag to encode as base64 and then have
decodeAction include some way to parse them. That way this case would
work in MPA mode too.
Same as #28847 but in the other direction.
Like other promises, this doesn't actually stream in the outgoing
direction. It buffers until the stream is done. This is mainly due to
our protocol remains compatible with Safari's lack of outgoing streams
until recently.
However, the stream chunks are encoded as separate fields and so does
support the busboy streaming on the receiving side.
We currently don't test FormData / File dependent features in CI because
we use an old Node.js version in CI. We should probably upgrade to 18
since that's really the minimum version that supports all the features
out of the box.
JSDOM is not a faithful/compatible implementation of these APIs. The
recommended way to use Flight together with FormData/Blob/File in older
Node.js versions, is to polyfill using the `undici` library.
However, even in these versions the Blob implementation isn't quite
faithful so the Reply client needs a slight tweak for multi-byte typed
arrays.
Stacked on #28798.
Add another AsyncLocalStorage to the FlightServerConfig. This context
tracks data on a per component level. Currently the only thing we track
is the owner in DEV.
AsyncLocalStorage around each component comes with a performance cost so
we only do it DEV. It's not generally a particularly safe operation
because you can't necessarily associate side-effects with a component
based on execution scope. It can be a lazy initializer or cache():ed
code etc. We also don't support string refs anymore for a reason.
However, it's good enough for optional dev only information like the
owner.
This PR reorganizes the `react-dom` entrypoint to only pull in code that
is environment agnostic. Previously if you required anything from this
entrypoint in any environment the entire client reconciler was loaded.
In a prior release we added a server rendering stub which you could
alias in server environments to omit this unecessary code. After landing
this change this entrypoint should not load any environment specific
code.
While a few APIs are truly client (browser) only such as createRoot and
hydrateRoot many of the APIs you import from this package are only
useful in the browser but could concievably be imported in shared code
(components running in Fizz or shared components as part of an RSC app).
To avoid making these require opting into the client bundle we are
keeping them in the `react-dom` entrypoint and changing their
implementation so that in environments where they are not particularly
useful they do something benign and expected.
#### Removed APIs
The following APIs are being removed in the next major. Largely they
have all been deprecated already and are part of legacy rendering modes
where concurrent features of React are not available
* `render`
* `hydrate`
* `findDOMNode`
* `unmountComponentAtNode`
* `unstable_createEventHandle`
* `unstable_renderSubtreeIntoContainer`
* `unstable_runWithPrioirty`
#### moved Client APIs
These APIs were available on both `react-dom` (with a warning) and
`react-dom/client`. After this change they are only available on
`react-dom/client`
* `createRoot`
* `hydrateRoot`
#### retained APIs
These APIs still exist on the `react-dom` entrypoint but have normalized
behavior depending on which renderers are currently in scope
* `flushSync`: will execute the function (if provided) inside the
flushSync implemention of FlightServer, Fizz, and Fiber DOM renderers.
* `unstable_batchedUpdates`: This is a noop in concurrent mode because
it is now the only supported behavior because there is no legacy
rendering mode
* `createPortal`: This just produces an object. It can be called from
anywhere but since you will probably not have a handle on a DOM node to
pass to it it will likely warn in environments other than the browser
* preloading APIS such as `preload`: These methods will execute the
preload across all renderers currently in scope. Since we resolve the
Request object on the server using AsyncLocalStorage or the current
function stack in practice only one renderer should act upon the
preload.
In addition to these changes the server rendering stub now just rexports
everything from `react-dom`. In a future minor we will add a warning
when using the stub and in the next major we will remove the stub
altogether
For [`AsyncIterable`](https://github.com/facebook/react/pull/28847) we
encode `AsyncIterator` as a separate tag.
Previously we encoded `Iterator` as just an Array. This adds a special
encoding for this. Technically this is a breaking change.
This is kind of an edge case that you'd care about the difference but it
becomes more important to treat these correctly for the warnings here
#28853.
This disables symbol renaming in production builds. The original
variable and function names are preserved. All other forms of
compression applied by Closure (dead code elimination, inlining, etc)
are unchanged — the final program is identical to what we were producing
before, just in a more readable form.
The motivation is to make it easier to debug React issues that only
occur in production — the same reason we decided to start shipping
sourcemaps in #28827 and #28827.
However, because most apps run their own minification step on their npm
dependencies, it's not necessary for us to minify the symbols before
publishing — it'll be handled the app, if desired.
This is the same strategy Meta has used to ship React for years. The
React build itself has unminified symbols, but they get minified as part
of Meta's regular build pipeline.
Even if an app does not minify their npm dependencies, gzip covers most
of the cost of symbol renaming anyway.
This saves us from having to ship sourcemaps, which means even apps that
don't have sourcemaps configured will be able to debug the React build
as easily as they would any other npm dependency.
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.