Compare commits

..

36 Commits

Author SHA1 Message Date
Dennis Moradkhani 4b877b6c66
Updated copyright text to Copyright (c) Meta Platforms, Inc. and its … (#26830)
…affiliates.

## Summary

There were 8 different places where the copyright comment was wrong.
Rewrote from "Copyright (c) Facebook, Inc. and its affiliates." to
"Copyright (c) Meta Platforms, Inc. and its affiliates."

## How did you test this change?
No code was changed. Comment was still a comment after changes.

Co-authored-by: Dennis Moradkhani <denmo530@student.liu.se>
2023-05-19 17:52:17 -07:00
Sanket Singh 7bd330e0b0
Updated Copyright comment from Facebook to Meta (#26833)
## Summary
Changed the comment in react/packages/react
/react.shared-subset.js saying 
```
Copyright (c) Facebook, Inc. and affiliates ..
```
To 
```
Copyright (c) Meta Platforms, Inc. and affiliates ..
```
as raised in the following issues:
https://github.com/facebook/react/issues/26829

Files Changed: 
react/packages/react/react.shared-subset.js

## How did you test this change?

Tests Required: No
2023-05-19 17:51:44 -07:00
Sebastian Markbåge 5309f10285
Remove Flight Relay DOM/Native (#26828)
The bindings upstream in Relay has been removed so we don't need these
builds anymore. The idea is to revisit an FB integration of Flight but
it wouldn't use the Relay specific bindings. It's a bit unclear how it
would look but likely more like the OSS version so not worth keeping
these around.

The `dom-relay` name also included the FB specific Fizz implementation
of the streaming config so I renamed that to `dom-fb`. There's no Fizz
implementation for Native yet so I just removed `native-relay`.

We created a configurable fork for how to encode the output of Flight
and the Relay implementation encoded it as JSON objects instead of
strings/streams. The new implementation would likely be more stream-like
and just encode it directly as string/binary chunks. So I removed those
indirections so that this can just be declared inline in
ReactFlightServer/Client.
2023-05-17 20:33:25 -04:00
Ibrahem Mahyob d7a98a5e97
Fix strict mode badge URL (#26825)
## Summary
Closes https://github.com/facebook/react/issues/26821

[[Fix #26821]](https://github.com/facebook/react/issues/26821) Update
strict mode badge URL


Updated the URL in the strict mode badge to point to the correct React
documentation for StrictMode. The previous URL was outdated. Now, when a
component is not running in StrictMode, the badge links to
https://react.dev/reference/react/StrictMode for more information.


## How did you test this change?

I verified that the strict mode badge now correctly links to the updated
URL. Previously, it pointed to the outdated URL
(https://fb.me/devtools-strict-mode). After the update, it correctly
points to the React Dev documentation for StrictMode
(https://react.dev/reference/react/StrictMode).


_Since its my first contribution here, i have completed the CLA_
2023-05-17 13:34:34 +01:00
Ruslan Lesiutin 2468a87358
React DevTools 4.27.7 -> 4.27.8 (#26823)
Closes https://github.com/facebook/react/issues/26787,
https://github.com/facebook/react/issues/26793

Includes these changes:
* fix[devtools]: fixed duplicated backend activation with multiple
renderers ([hoxyq](https://github.com/hoxyq) in
[#26807](https://github.com/facebook/react/pull/26807))
2023-05-17 11:40:50 +01:00
Andrew Clark f8de255e94
Lower Suspense throttling heuristic to 300ms (#26803)
Now that the throttling mechanism applies more often, we've decided to
lower this a tad to ensure it's not noticeable. The idea is it should be
just large enough to prevent jank when lots of different parts of the UI
load in rapid succession, but not large enough to make the UI feel
sluggish. There's no perfect number, it's just a heuristic.
2023-05-16 15:59:15 -04:00
Andrew Clark 4bfcd02b2c
Fix Suspense throttling mechanism (#26802)
The throttling mechanism for fallbacks should apply to both their
appearance _and_ disappearance.

This was mostly addressed by #26611. See that PR for additional context.

However, a flaw in the implementation is that we only update the the
timestamp used for throttling when the fallback initially appears. We
don't update it when the real content pops in. If lots of content in
separate Suspense trees loads around the same time, you can still get
jank.

The issue is fixed by updating the throttling timestamp whenever the
visibility of a fallback changes. Not just when it appears.
2023-05-16 15:03:30 -04:00
Sophie Alpert 4cd7065665
Fix uSES hydration in strict mode (#26791)
Previously, we'd call and use getSnapshot on the second render resulting
in `Warning: Text content did not match. Server: "Nay!" Client: "Yay!"`
and then `Error: Text content does not match server-rendered HTML.`.

Fixes #26095. Closes #26113. Closes #25650.

---------

Co-authored-by: eps1lon <silbermann.sebastian@gmail.com>
2023-05-12 14:18:03 -07:00
Pieter De Baets a389046a52
[react-native] Always set RN$stopSurface (#26808)
## Summary
To support incremental adoption of bridgeless logic we want to default
to using these globals whenever they're available.

## How did you test this change?
https://github.com/facebook/react-native/pull/37410
2023-05-12 16:14:45 +01:00
Ruslan Lesiutin 67a05d03e3
fix[devtools]: fixed duplicated backend activation with multiple renderers (#26807)
## Summary
Initially reported in https://github.com/facebook/react/issues/26797.
Was not able to reproduce the exact same problem, but found this case:

1. Open corresponding codepen from the issue in debug mode
2. Open components tab of the extension
3. Refresh the page

Received multiple errors:
- Warning in the Console tab: Invalid renderer id "2".
- Error in the Components tab: Uncaught Error: Cannot add node "3"
because a node with that id is already in the Store.

This problem has occurred after landing a fix in
https://github.com/facebook/react/pull/26779. Looks like Chrome is
keeping the injected scripts (the backend in this case) and we start
backend twice.
2023-05-12 14:59:53 +01:00
Jan Kassens df12d7eac4
[www] reduce dynamic SchedulerFeatureFlags (#26617)
For these values we're not using dynamic values. We can statically
compile in the values we're running.
2023-05-10 17:26:05 -04:00
Andrew Clark 7cd98ef2bc
Fix nightly job to publish to "canary" channel (#26799)
When I was renaming the next channel to canary, I updated the
`publish_preleases` workflow correctly, but I skipped over
`publish_preleases_nightly`. Oops.
2023-05-09 22:27:35 -04:00
Andrew Clark b5810163e9
Change `yarn test` to default to experimental (#26741)
The idea is that the default `yarn test` command should be the one that
includes the most bleeding edge features, because during development you
probably want as many features enabled as possible.

That used to be `www-modern` but nowadays it's `experimental` because
we've landed a bunch of async actions stuff in experimental but it isn't
yet being tested at Meta.

So this switches the default to `experimental`.
2023-05-09 21:50:14 -04:00
Jan Kassens fda1f0b902
Flow upgrade to 0.205.1 (#26796)
Just a small upgrade to keep us current and remove unused suppressions
(probably fixed by some upgrade since).

- `*` is no longer allowed and has been an alias for `any` for a while
now.
2023-05-09 10:45:50 -04:00
Jan Kassens 7ac5e9a602
Small flowconfig fixes (#26784)
- The whole project root is included by default anyway, the include
section should be redundant and just misleading.
- The generated ignore paths ignore more than intended as they didn't
escape the `.` for regex.

Test Plan:
- wait for CI
- tested the ignore pattern change with renaming files and seeing the
expected files ignored for flow
2023-05-08 12:07:48 -04:00
Andrew Clark 16d053d592
Add useFormStatus to server rendering stub (#26788)
This was an oversight when I set up the hook in #26719.
2023-05-06 20:39:08 -04:00
dan efb381bbf9
[Release Script] Print a hint where to get the token (#26783)
I always forget where to get it.
2023-05-05 20:08:14 +01:00
Samuel Susla b00e27342d
Use native scheduler if defined in global scope (#26554)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
2023-05-05 14:01:31 +01:00
Ruslan Lesiutin 783e7fcfa3
React DevTools 4.27.6 -> 4.27.7 (#26780)
List of changes:
* DevTools: fix backend activation ([hoxyq](https://github.com/hoxyq) in
[#26779](https://github.com/facebook/react/pull/26779))
* fix[dynamic-scripts-injection]: unregister content scripts before
registration ([hoxyq](https://github.com/hoxyq) in
[#26765](https://github.com/facebook/react/pull/26765))
2023-05-04 18:22:47 +01:00
Ruslan Lesiutin 377c5175f7
DevTools: fix backend activation (#26779)
## Summary
We have a case:
1. Open components tab
2. Close Chrome / Firefox devtools window completely
3. Reopen browser devtools panel
4. Open components tab

Currently, in version 4.27.6, we cannot load the components tree.

This PR contains two changes:
- non-functional refactoring in
`react-devtools-shared/src/devtools/store.js`: removed some redundant
type castings.
- fixed backend manager logic (introduced in
https://github.com/facebook/react/pull/26615) to activate already
registered backends. Looks like frontend of devtools also depends on
`renderer-attached` event, without it component tree won't load.

## How did you test this change?
This fixes the case mentioned prior. Currently in 4.27.6 version it is
not working, we need to refresh the page to make it work.

I've tested this in several environments: chrome, firefox, standalone
with RN application.
2023-05-04 17:34:57 +01:00
Sebastian Markbåge aef7ce5547
[Flight] Progressively Enhanced Server Actions (#26774)
This automatically exposes `$$FORM_ACTIONS` on Server References coming
from Flight. So that when they're used in a form action, we can encode
the ID for the server reference as a hidden field or as part of the name
of a button.

If the Server Action is a bound function it can have complex data
associated with it. In this case this additional data is encoded as
additional form fields.

To process a POST on the server there's now a `decodeAction` helper that
can take one of these progressive posts from FormData and give you a
function that is prebound with the correct closure and FormData so that
you can just invoke it.

I updated the fixture which now has a "Server State" that gets
automatically refreshed. This also lets us visualize form fields.
There's no "Action State" here for showing error messages that are not
thrown, that's still up to user space.
2023-05-03 18:36:57 -04:00
Sebastian Markbåge c10010a6a0
[Fizz] Gracefully handle suspending in DOM configs (#26768)
E.g. if we suspend (throw a promise) in pushStartInstance today we might
have already pushed some chunks (or even child segments potentially). We
should revert back to where we were.

This doesn't usually happen because when we suspend in a component it
doesn't write anything itself, it'll always defer to som host instance
to do the writing.

There was a todo about this already but I'm not 100% sure it's always
safe when suspending. It should be safe when suspending just regularly
because it's just a noop. We might not even want "throwing a promise" in
this mechanism to be supported longer term but for now that's how a
suspend in internals.
2023-05-03 18:13:47 -04:00
Andrew Clark f533cee8cb
Add useFormStatus to Flight fixture (#26773) 2023-05-03 15:29:32 -04:00
Andrew Gadzik 2c1117a8d0
Reuse request so that a ReabableStream body does not become disturbed (#26771) 2023-05-03 15:06:10 -04:00
Sebastian Markbåge fa7a447b9c
[Fizz] Check for nullish values on ReactCustomFormAction (#26770)
Usually we don't have to do this since we only set these in the loop but
the ReactCustomFormAction props are optional so they might be undefined.

Also moved it to a general type since it's a semi-public API.
2023-05-03 14:35:26 -04:00
Andrew Clark b7972822b5
useOptimisticState -> useOptimistic (#26772)
Drop the "state". Just "useOptimistic". It's cleaner.

This is still an experimental API. May not be the final name.
2023-05-03 14:26:00 -04:00
Andrew Clark 388686f291
Add "canary" to list of allowed npm dist tags (#26767)
Forgot this allowlist existed. It's an extra safeguard, in case we mess
up the configuration somehow.
2023-05-03 12:36:34 -04:00
Ruslan Lesiutin 8a25302c66
fix[dynamic-scripts-injection]: unregister content scripts before registration (#26765)
## Summary
Fixes #26756.

DevTools is failing to inject `__REACT_DEVTOOLS_GLOBAL_HOOK__` hook in
incognito mode. This is not happening straight-forward, but if extension
is toggled on and off, the next time I try to open it I am receiving an
error that content script was already registered.

<img width="676" alt="Screenshot 2023-05-02 at 14 36 53"
src="https://user-images.githubusercontent.com/28902667/235877692-51c5d284-79d9-4b00-b62e-d25d5bb5e056.png">

- Unregistering content scripts before attempting to register them
again. We need to inject `__REACT_DEVTOOLS_GLOBAL_HOOK__` on each page,
so this should be expected behaviour.
- Fixed error logging
 
## How did you test this change?
Local build of extension for Chrome, trying the same steps, which
resulted in an error.
No regression in performance, tested on react.dev, still the same.
2023-05-03 17:27:39 +01:00
Andrew Clark 2c2476834a
Rename "next" prerelease channel to "canary" (#26761)
The "next" prerelease channel represents what will be published the next
time we do a stable release. We publish a new "next" release every day
using a timed CI workflow.

When we introduced this prerelease channel a few years ago, another name
we considered was "canary". But I proposed "next" instead to create a
greater distinction between this channel and the "experimental" channel
(which is published at the same cadence, but includes extra experimental
features), because some other projects use "canary" to refer to releases
that are more unstable than how we would use it.

The main downside of "next" is someone might mistakenly assume the name
refers to Next.js. We were aware of this risk at the time but didn't
think it would be an issue in practice.

However, colloquially, we've ended up referring to this as the "canary"
channel anyway to avoid precisely that confusion.

So after further discussion, we've agreed to rename to "canary".

This affects the label used in the version string (e.g.
`18.3.0-next-a1c2d3e4` becomes `18.3.0-canary-a1c2d3e4`) as well as the
npm dist tags used to publish the releases. For now, I've chosen to
publish the canaries using both `@canary` and `@next` dist tags, so that
downstream consumers who might depend on `@next` have time to adjust. We
can remove that later after the change has been communicated.
2023-05-03 12:10:32 -04:00
Andrew Clark fa4314841e
Remove deprecated workflow key from Circle config (#26762)
This key was only valid during the 2.0 beta period so we can remove it.
2023-05-02 14:59:36 -04:00
Samuel Susla 5dd90c5623
Use content hash for react-native builds (#26734) 2023-05-02 12:04:02 +01:00
Sebastian Markbåge 559e83aebb
[Fizz] Allow an action provide a custom set of props to use for progressive enhancement (#26749)
Stacked on top of #26735.

This allows a framework to add a `$$FORM_ACTION` property to a function.
This lets the framework return a set of props to use in place of the
function but only during SSR. Effectively, this lets you implement
progressive enhancement of form actions using some other way instead of
relying on the replay feature.

This will be used by RSC on Server References automatically by
convention in a follow up, but this mechanism can also be used by other
frameworks/libraries.
2023-05-01 16:01:14 -04:00
Sebastian Markbåge 67f4fb0213
Allow forms to skip hydration of hidden inputs (#26735)
This allows us to emit extra ephemeral data that will only be used on
server rendered forms.

First I refactored the shouldSkip functions to now just do that work
inside the canHydrate methods. This makes the Config bindings a little
less surface area but it also helps us optimize a bit since we now can
look at the code together and find shared paths.

canHydrate returns the instance if it matches, that used to just be
there to refine the type but it can also be used to just return a
different instance later that we find. If we don't find one, we'll bail
out and error regardless so no need to skip past anything.
2023-05-01 15:35:57 -04:00
Josh Story 8ea96ef84d
[Fizz] Encode external fizz runtime into chunks eagerly (#26752)
in https://github.com/facebook/react/pull/26738 we added nonce to the
ResponseState. Initially it was used in a variety of places but the
version that got merged only included it with the external fizz runtime.
This PR updates the config for the external fizz runtime so that the
nonce is encoded into the script chunks at request creation time.

The rationale is that for live-requests, streaming is more likely than
not so doing the encoding work at the start is better than during flush.
For cases such as SSG where the runtime is not required the extra
encoding is tolerable (not a live request). Bots are an interesting case
because if you want fastest TTFB you will end up requiring the runtime
but if you are withholding until the stream is done you have already
sacrificed fastest TTFB and the marginal slowdown of the extraneous
encoding is hopefully neglibible

I'm writing this so later if we learn that this tradeoff isn't worth it
we at least understand why I made the change in the first place.
2023-05-01 10:50:36 -07:00
Andrew Clark 491aec5d61
Implement experimental_useOptimisticState (#26740)
This adds an experimental hook tentatively called useOptimisticState.
(The actual name needs some bikeshedding.)

The headline feature is that you can use it to implement optimistic
updates. If you set some optimistic state during a transition/action,
the state will be automatically reverted once the transition completes.

Another feature is that the optimistic updates will be continually
rebased on top of the latest state.

It's easiest to explain with examples; we'll publish documentation as
the API gets closer to stabilizing. See tests for now.

Technically the use cases for this hook are broader than just optimistic
updates; you could use it implement any sort of "pending" state, such as
the ones exposed by useTransition and useFormStatus. But we expect
people will most often reach for this hook to implement the optimistic
update pattern; simpler cases are covered by those other hooks.
2023-05-01 13:19:20 -04:00
Dan Ott 9545e4810c
Add nonce support to bootstrap scripts and external runtime (#26738)
Adds support for nonce on `bootstrapScripts`, `bootstrapModules` and the external fizz runtime
2023-05-01 09:19:02 -07:00
179 changed files with 3155 additions and 3463 deletions

View File

@ -426,7 +426,6 @@ jobs:
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
workflows:
version: 2
build_and_test:
unless: << pipeline.parameters.prerelease_commit_sha >>
@ -605,10 +604,20 @@ workflows:
when: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- publish_prerelease:
name: Publish to Next channel
name: Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: stable
dist_tag: "next"
# The tags to use when publishing canaries. The main one we should
# always include is "canary" but we can use multiple (e.g. alpha,
# beta, rc). To declare multiple, use a comma-separated string, like
# this:
# dist_tag: "canary,alpha,beta,rc"
#
# TODO: We currently tag canaries with "next" in addition to "canary"
# because this used to be called the "next" channel and some
# downstream consumers might still expect that tag. We can remove this
# after some time has elapsed and the change has been communicated.
dist_tag: "canary,next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -616,7 +625,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Next channel
- Publish to Canary channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: experimental
dist_tag: experimental
@ -634,10 +643,10 @@ workflows:
- main
jobs:
- publish_prerelease:
name: Publish to Next channel
name: Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: stable
dist_tag: "next"
dist_tag: "canary,next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -645,7 +654,7 @@ workflows:
# will sometimes fail if you try to concurrently publish two
# different versions of the same package, even if they use different
# dist tags.
- Publish to Next channel
- Publish to Canary channel
commit_sha: << pipeline.git.revision >>
release_channel: experimental
dist_tag: experimental

View File

@ -416,7 +416,6 @@ module.exports = {
{
files: [
'packages/react-native-renderer/**/*.js',
'packages/react-server-native-relay/**/*.js',
],
globals: {
nativeFabricUIManager: 'readonly',

View File

@ -228,7 +228,16 @@ jobs:
name: compiled-rn
path: compiled-rn/
- run: git status -u
- name: Check if only the REVISION file has changed
id: check_should_commit
run: |
if git status --porcelain | grep -qv '/REVISION$'; then
echo "should_commit=true" >> "$GITHUB_OUTPUT"
else
echo "should_commit=false" >> "$GITHUB_OUTPUT"
fi
- name: Commit changes to branch
if: steps.check_should_commit.outputs.should_commit == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: |

View File

@ -7,12 +7,12 @@
//
// The @latest channel uses the version as-is, e.g.:
//
// 18.0.0
// 18.3.0
//
// The @next channel appends additional information, with the scheme
// The @canary channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 18.0.0-alpha-a1c2d3e4
// 18.3.0-canary-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
@ -20,9 +20,13 @@
const ReactVersion = '18.3.0';
// The label used by the @next channel. Represents the upcoming release's
// stability. Could be "alpha", "beta", "rc", etc.
const nextChannelLabel = 'next';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
// choose to change it to "alpha", "beta", "rc", etc.
//
// It only affects the label used in the version string. To customize the
// npm dist tags used during publish, refer to .circleci/config.yml.
const canaryChannelLabel = 'canary';
const stablePackages = {
'eslint-plugin-react-hooks': '5.0.0',
@ -40,14 +44,14 @@ const stablePackages = {
scheduler: '0.24.0',
};
// These packages do not exist in the @next or @latest channel, only
// These packages do not exist in the @canary or @latest channel, only
// @experimental. We don't use semver, just the commit sha, so this is just a
// list of package names instead of a map.
const experimentalPackages = [];
module.exports = {
ReactVersion,
nextChannelLabel,
canaryChannelLabel,
stablePackages,
experimentalPackages,
};

View File

@ -15,8 +15,8 @@
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -11,12 +11,12 @@
__REACT_DEVTOOLS_GLOBAL_HOOK__ = parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
</script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@next/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@next/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@next/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@next/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler.development.js"></script>
<script src="https://unpkg.com/scheduler@canary/umd/scheduler-tracing.development.js"></script>
<script src="https://unpkg.com/react@canary/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@canary/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-cache@canary/umd/react-cache.development.js"></script>
<!-- Don't use this in production: -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
</head>
@ -38,4 +38,4 @@
Learn more at https://reactjs.org/docs/getting-started.html
-->
</body>
</html>
</html>

View File

@ -95,6 +95,8 @@ app.all('/', async function (req, res, next) {
if (req.get('rsc-action')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
proxiedHeaders['rsc-action'] = req.get('rsc-action');
} else if (req.get('Content-type')) {
proxiedHeaders['Content-type'] = req.get('Content-type');
}
const promiseForData = request(

View File

@ -36,6 +36,7 @@ const bodyParser = require('body-parser');
const busboy = require('busboy');
const app = express();
const compress = require('compression');
const {Readable} = require('node:stream');
app.use(compress());
@ -45,7 +46,7 @@ const {readFile} = require('fs').promises;
const React = require('react');
app.get('/', async function (req, res) {
async function renderApp(res, returnValue) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@ -91,37 +92,74 @@ app.get('/', async function (req, res) {
),
React.createElement(App),
];
const {pipe} = renderToPipeableStream(root, moduleMap);
// For client-invoked server actions we refresh the tree and return a return value.
const payload = returnValue ? {returnValue, root} : root;
const {pipe} = renderToPipeableStream(payload, moduleMap);
pipe(res);
}
app.get('/', async function (req, res) {
await renderApp(res, null);
});
app.post('/', bodyParser.text(), async function (req, res) {
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
await import('react-server-dom-webpack/server');
const {
renderToPipeableStream,
decodeReply,
decodeReplyFromBusboy,
decodeAction,
} = await import('react-server-dom-webpack/server');
const serverReference = req.get('rsc-action');
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
if (serverReference) {
// This is the client-side case
const [filepath, name] = serverReference.split('#');
const action = (await import(filepath))[name];
// Validate that this is actually a function we intended to expose and
// not the client trying to invoke arbitrary functions. In a real app,
// you'd have a manifest verifying this before even importing it.
if (action.$$typeof !== Symbol.for('react.server.reference')) {
throw new Error('Invalid action');
}
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
let args;
if (req.is('multipart/form-data')) {
// Use busboy to streamingly parse the reply from form-data.
const bb = busboy({headers: req.headers});
const reply = decodeReplyFromBusboy(bb);
req.pipe(bb);
args = await reply;
} else {
args = await decodeReply(req.body);
}
const result = action.apply(null, args);
try {
// Wait for any mutations
await result;
} catch (x) {
// We handle the error on the client
}
// Refresh the client and return the value
renderApp(res, result);
} else {
args = await decodeReply(req.body);
// This is the progressive enhancement case
const UndiciRequest = require('undici').Request;
const fakeRequest = new UndiciRequest('http://localhost', {
method: 'POST',
headers: {'Content-Type': req.headers['content-type']},
body: Readable.toWeb(req),
duplex: 'half',
});
const formData = await fakeRequest.formData();
const action = await decodeAction(formData);
try {
// Wait for any mutations
await action();
} catch (x) {
const {setServerState} = await import('../src/ServerState.js');
setServerState('Error: ' + x.message);
}
renderApp(res, null);
}
const result = action.apply(null, args);
const {pipe} = renderToPipeableStream(result, {});
pipe(res);
});
app.get('/todos', function (req, res) {

View File

@ -11,6 +11,8 @@ import Form from './Form.js';
import {like, greet} from './actions.js';
import {getServerState} from './ServerState.js';
export default async function App() {
const res = await fetch('http://localhost:3001/todos');
const todos = await res.json();
@ -23,7 +25,7 @@ export default async function App() {
</head>
<body>
<Container>
<h1>Hello, world</h1>
<h1>{getServerState()}</h1>
<Counter />
<Counter2 />
<ul>

View File

@ -1,29 +1,25 @@
'use client';
import * as React from 'react';
import {flushSync} from 'react-dom';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
export default function Button({action, children}) {
const [isPending, setIsPending] = React.useState(false);
function ButtonDisabledWhilePending({action, children}) {
const {pending} = useFormStatus();
return (
<button disabled={pending} formAction={action}>
{children}
</button>
);
}
export default function Button({action, children}) {
return (
<ErrorBoundary>
<form>
<button
disabled={isPending}
formAction={async () => {
// TODO: Migrate to useFormPending once that exists
flushSync(() => setIsPending(true));
try {
const result = await action();
console.log(result);
} finally {
React.startTransition(() => setIsPending(false));
}
}}>
<ButtonDisabledWhilePending action={action}>
{children}
</button>
</ButtonDisabledWhilePending>
</form>
</ErrorBoundary>
);

View File

@ -1,25 +1,20 @@
'use client';
import * as React from 'react';
import {flushSync} from 'react-dom';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
function Status() {
const {pending} = useFormStatus();
return pending ? 'Saving...' : null;
}
export default function Form({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
<ErrorBoundary>
<form
action={async formData => {
// TODO: Migrate to useFormPending once that exists
flushSync(() => setIsPending(true));
try {
const result = await action(formData);
alert(result);
} finally {
React.startTransition(() => setIsPending(false));
}
}}>
<form action={action}>
<label>
Name: <input name="name" />
</label>
@ -27,7 +22,7 @@ export default function Form({action, children}) {
File: <input type="file" name="file" />
</label>
<button>Say Hi</button>
{isPending ? 'Saving...' : null}
<Status />
</form>
</ErrorBoundary>
);

View File

@ -0,0 +1,9 @@
let serverState = 'Hello World';
export function setServerState(message) {
serverState = message;
}
export function getServerState() {
return serverState;
}

View File

@ -1,11 +1,15 @@
'use server';
import {setServerState} from './ServerState.js';
export async function like() {
setServerState('Liked!');
return new Promise((resolve, reject) => resolve('Liked'));
}
export async function greet(formData) {
const name = formData.get('name') || 'you';
setServerState('Hi ' + name);
const file = formData.get('file');
if (file) {
return `Ok, ${name}, here is ${file.name}:

View File

@ -1,11 +1,29 @@
import * as React from 'react';
import {use, Suspense} from 'react';
import {use, Suspense, useState, startTransition} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
// TODO: This should be a dependency of the App but we haven't implemented CSS in Node yet.
import './style.css';
let updateRoot;
async function callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
const {returnValue, root} = await createFromFetch(response, {callServer});
// Refresh the tree with the new RSC payload.
startTransition(() => {
updateRoot(root);
});
return returnValue;
}
let data = createFromFetch(
fetch('/', {
headers: {
@ -13,22 +31,14 @@ let data = createFromFetch(
},
}),
{
async callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
return createFromFetch(response);
},
callServer,
}
);
function Shell({data}) {
return use(data);
const [root, setRoot] = useState(use(data));
updateRoot = setRoot;
return root;
}
ReactDOM.hydrateRoot(document, <Shell data={data} />);

View File

@ -65,8 +65,8 @@
"eslint-plugin-react-internal": "link:./scripts/eslint-rules",
"fbjs-scripts": "^3.0.1",
"filesize": "^6.0.1",
"flow-bin": "^0.202.0",
"flow-remove-types": "^2.202.0",
"flow-bin": "^0.205.1",
"flow-remove-types": "^2.205.1",
"glob": "^7.1.6",
"glob-stream": "^6.1.0",
"google-closure-compiler": "^20230206.0.0",

View File

@ -7,4 +7,4 @@
* @flow
*/
export * from './src/ReactFlightClientStream';
export * from './src/ReactFlightClient';

View File

@ -13,28 +13,37 @@ import type {LazyComponent} from 'react/src/ReactLazy';
import type {
ClientReference,
ClientReferenceMetadata,
UninitializedModel,
Response,
SSRManifest,
StringDecoder,
} from './ReactFlightClientConfig';
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
import type {CallServerCallback} from './ReactFlightReplyClient';
import {
resolveClientReference,
preloadModule,
requireModule,
parseModel,
dispatchHint,
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientConfig';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {
encodeFormAction,
knownServerReferences,
} from './ReactFlightReplyClient';
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type {CallServerCallback};
type UninitializedModel = string;
export type JSONValue =
| number
@ -153,15 +162,15 @@ Chunk.prototype.then = function <T>(
}
};
export type ResponseBase = {
export type Response = {
_bundlerConfig: SSRManifest,
_callServer: CallServerCallback,
_chunks: Map<number, SomeChunk<any>>,
...
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
};
export type {Response};
function readChunk<T>(chunk: SomeChunk<T>): T {
// If we have resolved content, we try to initialize it first which
// might put us back into one of the other states.
@ -500,11 +509,14 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
return callServer(metaData.id, bound.concat(args));
});
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, metaData);
return proxy;
}
export function parseModelString(
function parseModelString(
response: Response,
parentObject: Object,
key: string,
@ -624,7 +636,7 @@ export function parseModelString(
return value;
}
export function parseModelTuple(
function parseModelTuple(
response: Response,
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
): any {
@ -648,17 +660,25 @@ function missingCall() {
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): ResponseBase {
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response = {
const response: Response = {
_bundlerConfig: bundlerConfig,
_callServer: callServer !== undefined ? callServer : missingCall,
_chunks: chunks,
_partialRow: '',
_stringDecoder: (null: any),
_fromJSON: (null: any),
};
if (supportsBinaryStreams) {
response._stringDecoder = createStringDecoder();
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
export function resolveModel(
function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
@ -672,7 +692,7 @@ export function resolveModel(
}
}
export function resolveModule(
function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
@ -721,7 +741,7 @@ export function resolveModule(
}
type ErrorWithDigest = Error & {digest?: string};
export function resolveErrorProd(
function resolveErrorProd(
response: Response,
id: number,
digest: string,
@ -750,7 +770,7 @@ export function resolveErrorProd(
}
}
export function resolveErrorDev(
function resolveErrorDev(
response: Response,
id: number,
digest: string,
@ -781,7 +801,7 @@ export function resolveErrorDev(
}
}
export function resolveHint(
function resolveHint(
response: Response,
code: string,
model: UninitializedModel,
@ -790,6 +810,105 @@ export function resolveHint(
dispatchHint(code, hintModel);
}
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.slice(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case 'H': {
const code = row[colon + 2];
resolveHint(response, code, row.slice(colon + 3));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.slice(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.slice(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function close(response: Response): void {
// In case there are any remaining unresolved chunks, they won't
// be resolved now. So we need to issue an error to those.

View File

@ -1,33 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type StringDecoder = void;
export const supportsBinaryStreams = false;
export function createStringDecoder(): void {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readPartialStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}
export function readFinalStringChunk(
decoder: StringDecoder,
buffer: Uint8Array,
): string {
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error('Should never be called');
}

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ResponseBase} from './ReactFlightClient';
import type {StringDecoder} from './ReactFlightClientConfig';
export type Response = ResponseBase & {
_partialRow: string,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
};
export type UninitializedModel = string;
export function parseModel<T>(response: Response, json: UninitializedModel): T {
return JSON.parse(json, response._fromJSON);
}

View File

@ -1,146 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {CallServerCallback} from './ReactFlightClient';
import type {Response} from './ReactFlightClientConfigStream';
import type {SSRManifest} from './ReactFlightClientConfig';
import {
resolveModule,
resolveModel,
resolveErrorProd,
resolveErrorDev,
resolveHint,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
} from './ReactFlightClient';
import {
readPartialStringChunk,
readFinalStringChunk,
supportsBinaryStreams,
createStringDecoder,
} from './ReactFlightClientConfig';
export type {Response};
function processFullRow(response: Response, row: string): void {
if (row === '') {
return;
}
const colon = row.indexOf(':', 0);
const id = parseInt(row.slice(0, colon), 16);
const tag = row[colon + 1];
// When tags that are not text are added, check them here before
// parsing the row as text.
// switch (tag) {
// }
switch (tag) {
case 'I': {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case 'H': {
const code = row[colon + 2];
resolveHint(response, code, row.slice(colon + 3));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
id,
errorInfo.digest,
errorInfo.message,
errorInfo.stack,
);
} else {
resolveErrorProd(response, id, errorInfo.digest);
}
return;
}
default: {
// We assume anything else is JSON.
resolveModel(response, id, row.slice(colon + 1));
return;
}
}
}
export function processStringChunk(
response: Response,
chunk: string,
offset: number,
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
const fullrow = response._partialRow + chunk.slice(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
response._partialRow += chunk.slice(offset);
}
export function processBinaryChunk(
response: Response,
chunk: Uint8Array,
): void {
if (!supportsBinaryStreams) {
throw new Error("This environment don't support binary chunks.");
}
const stringDecoder = response._stringDecoder;
let linebreak = chunk.indexOf(10); // newline
while (linebreak > -1) {
const fullrow =
response._partialRow +
readFinalStringChunk(stringDecoder, chunk.subarray(0, linebreak));
processFullRow(response, fullrow);
response._partialRow = '';
chunk = chunk.subarray(linebreak + 1);
linebreak = chunk.indexOf(10); // newline
}
response._partialRow += readPartialStringChunk(stringDecoder, chunk);
}
function createFromJSONCallback(response: Response) {
// $FlowFixMe[missing-this-annot]
return function (key: string, value: JSONValue) {
if (typeof value === 'string') {
// We can't use .bind here because we need the "this" value.
return parseModelString(response, this, key, value);
}
if (typeof value === 'object' && value !== null) {
return parseModelTuple(response, value);
}
return value;
};
}
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): Response {
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
// It should be inlined to one object literal but minor changes can break it.
const stringDecoder = supportsBinaryStreams ? createStringDecoder() : null;
const response: any = createResponseBase(bundlerConfig, callServer);
response._partialRow = '';
if (supportsBinaryStreams) {
response._stringDecoder = stringDecoder;
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
}
export {reportGlobalError, getRoot, close} from './ReactFlightClient';

View File

@ -7,12 +7,7 @@
* @flow
*/
import type {Thenable} from 'shared/ReactTypes';
import {
knownServerReferences,
createServerReference,
} from './ReactFlightServerReferenceRegistry';
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
import {
REACT_ELEMENT_TYPE,
@ -28,6 +23,10 @@ import {
} from 'shared/ReactSerializationErrors';
import isArray from 'shared/isArray';
import type {
FulfilledThenable,
RejectedThenable,
} from '../../shared/ReactTypes';
type ReactJSONValue =
| string
@ -39,6 +38,15 @@ type ReactJSONValue =
export opaque type ServerReference<T> = T;
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();
// Serializable values
export type ReactServerValue =
// References are passed by their value
@ -283,7 +291,6 @@ export function processReply(
// Possibly a Date, whose toJSON automatically calls toISOString
// $FlowFixMe[incompatible-use]
const originalValue = parent[key];
// $FlowFixMe[method-unbinding]
if (originalValue instanceof Date) {
return serializeDateFromDateJSON(value);
}
@ -363,4 +370,104 @@ export function processReply(
}
}
export {createServerReference};
const boundCache: WeakMap<
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
Thenable<FormData>,
> = new WeakMap();
function encodeFormData(reference: any): Thenable<FormData> {
let resolve, reject;
// We need to have a handle on the thenable so that we can synchronously set
// its status from processReply, when it can complete synchronously.
const thenable: Thenable<FormData> = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
processReply(
reference,
'',
(body: string | FormData) => {
if (typeof body === 'string') {
const data = new FormData();
data.append('0', body);
body = data;
}
const fulfilled: FulfilledThenable<FormData> = (thenable: any);
fulfilled.status = 'fulfilled';
fulfilled.value = body;
resolve(body);
},
e => {
const rejected: RejectedThenable<FormData> = (thenable: any);
rejected.status = 'rejected';
rejected.reason = e;
reject(e);
},
);
return thenable;
}
export function encodeFormAction(
this: any => Promise<any>,
identifierPrefix: string,
): ReactCustomFormAction {
const reference = knownServerReferences.get(this);
if (!reference) {
throw new Error(
'Tried to encode a Server Action from a different instance than the encoder is from. ' +
'This is a bug in React.',
);
}
let data: null | FormData = null;
let name;
const boundPromise = reference.bound;
if (boundPromise !== null) {
let thenable = boundCache.get(reference);
if (!thenable) {
thenable = encodeFormData(reference);
boundCache.set(reference, thenable);
}
if (thenable.status === 'rejected') {
throw thenable.reason;
} else if (thenable.status !== 'fulfilled') {
throw thenable;
}
const encodedFormData = thenable.value;
// This is hacky but we need the identifier prefix to be added to
// all fields but the suspense cache would break since we might get
// a new identifier each time. So we just append it at the end instead.
const prefixedData = new FormData();
// $FlowFixMe[prop-missing]
encodedFormData.forEach((value: string | File, key: string) => {
prefixedData.append('$ACTION_' + identifierPrefix + ':' + key, value);
});
data = prefixedData;
// We encode the name of the prefix containing the data.
name = '$ACTION_REF_' + identifierPrefix;
} else {
// This is the simple case so we can just encode the ID.
name = '$ACTION_ID_' + reference.id;
}
return {
name: name,
method: 'POST',
encType: 'multipart/form-data',
data: data,
};
}
export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
// Expose encoder for use by SSR.
// TODO: Only expose this in SSR builds and not the browser client.
proxy.$$FORM_ACTION = encodeFormAction;
knownServerReferences.set(proxy, {id: id, bound: null});
return proxy;
}

View File

@ -1,32 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Thenable} from 'shared/ReactTypes';
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
type ServerReferenceId = any;
export const knownServerReferences: WeakMap<
Function,
{id: ServerReferenceId, bound: null | Thenable<Array<any>>},
> = new WeakMap();
export function createServerReference<A: Iterable<any>, T>(
id: ServerReferenceId,
callServer: CallServerCallback,
): (...A) => Promise<T> {
const proxy = function (): Promise<T> {
// $FlowFixMe[method-unbinding]
const args = Array.prototype.slice.call(arguments);
return callServer(id, args);
};
knownServerReferences.set(proxy, {id: id, bound: null});
return proxy;
}

View File

@ -25,7 +25,6 @@
declare var $$$config: any;
export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
@ -39,9 +38,6 @@ export const dispatchHint = $$$config.dispatchHint;
export opaque type Source = mixed;
export type UninitializedModel = string;
export const parseModel = $$$config.parseModel;
export opaque type StringDecoder = mixed; // eslint-disable-line no-undef
export const supportsBinaryStreams = $$$config.supportsBinaryStreams;

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,7 +8,6 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export type Response = any;

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -8,6 +8,5 @@
*/
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from 'react-server-dom-relay/src/ReactFlightClientConfigDOMRelay';
export * from '../ReactFlightClientConfigNoStream';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from 'react-server-native-relay/src/ReactFlightClientConfigNativeRelay';
export * from '../ReactFlightClientConfigNoStream';

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-core",
"version": "4.27.6",
"version": "4.27.8",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -167,7 +167,6 @@ function onDisconnected() {
disconnectedCallback();
}
// $FlowFixMe[missing-local-annot]
function onError({code, message}: $FlowFixMe) {
safeUnmount();

View File

@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Chrome Developer Tools.",
"version": "4.27.6",
"version_name": "4.27.6",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"icons": {
"16": "icons/16-production.png",

View File

@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Microsoft Edge Developer Tools.",
"version": "4.27.6",
"version_name": "4.27.6",
"version": "4.27.8",
"version_name": "4.27.8",
"minimum_chrome_version": "102",
"icons": {
"16": "icons/16-production.png",

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "React Developer Tools",
"description": "Adds React debugging tools to the Firefox Developer Tools.",
"version": "4.27.6",
"version": "4.27.8",
"applications": {
"gecko": {
"id": "@react-devtools",

View File

@ -11,7 +11,7 @@
/* eslint-disable no-unused-vars */
type JestMockFn<TArguments: $ReadOnlyArray<*>, TReturn> = {
type JestMockFn<TArguments: $ReadOnlyArray<any>, TReturn> = {
(...args: TArguments): TReturn,
/**
* An object for introspecting mock calls
@ -626,7 +626,7 @@ interface JestExpectType {
* Use .toBeInstanceOf(Class) to check that an object is an instance of a
* class.
*/
toBeInstanceOf(cls: Class<*>): void;
toBeInstanceOf(cls: Class<any>): void;
/**
* .toBeNull() is the same as .toBe(null) but the error messages are a bit
* nicer.
@ -815,7 +815,7 @@ type JestObjectType = {
* Returns a new, unused mock function. Optionally takes a mock
* implementation.
*/
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
fn<TArguments: $ReadOnlyArray<any>, TReturn>(
implementation?: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>,
/**

View File

@ -29,5 +29,6 @@ function setup(hook: ?DevToolsHook) {
initBackend,
setupNativeStyleEditor,
});
hook.emit('devtools-backend-installed', COMPACT_VERSION_NAME);
}

View File

@ -16,7 +16,6 @@ import {COMPACT_VERSION_NAME} from './utils';
let welcomeHasInitialized = false;
// $FlowFixMe[missing-local-annot]
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
@ -59,13 +58,20 @@ function setup(hook: ?DevToolsHook) {
// register renderers that have already injected themselves.
hook.renderers.forEach(renderer => {
registerRenderer(renderer);
registerRenderer(renderer, hook);
});
// Activate and remove from required all present backends, registered within the hook
hook.backends.forEach((_, backendVersion) => {
requiredBackends.delete(backendVersion);
activateBackend(backendVersion, hook);
});
updateRequiredBackends();
// register renderers that inject themselves later.
hook.sub('renderer', ({renderer}) => {
registerRenderer(renderer);
registerRenderer(renderer, hook);
updateRequiredBackends();
});
@ -78,12 +84,16 @@ function setup(hook: ?DevToolsHook) {
const requiredBackends = new Set<string>();
function registerRenderer(renderer: ReactRenderer) {
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
let version = renderer.reconcilerVersion || renderer.version;
if (!hasAssignedBackend(version)) {
version = COMPACT_VERSION_NAME;
}
requiredBackends.add(version);
// Check if required backend is already activated, no need to require again
if (!hook.backends.has(version)) {
requiredBackends.add(version);
}
}
function activateBackend(version: string, hook: DevToolsHook) {
@ -91,6 +101,7 @@ function activateBackend(version: string, hook: DevToolsHook) {
if (!backend) {
throw new Error(`Could not find backend for version "${version}"`);
}
const {Agent, Bridge, initBackend, setupNativeStyleEditor} = backend;
const bridge = new Bridge({
listen(fn) {
@ -151,6 +162,10 @@ function activateBackend(version: string, hook: DevToolsHook) {
// tell the service worker which versions of backends are needed for the current page
function updateRequiredBackends() {
if (requiredBackends.size === 0) {
return;
}
window.postMessage(
{
source: 'react-devtools-backend-manager',

View File

@ -6,38 +6,48 @@ import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
const ports = {};
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
// Manifest V3 method of injecting content scripts (not yet supported in Firefox)
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
chrome.scripting.registerContentScripts(
[
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
{
id: 'renderer',
matches: ['<all_urls>'],
js: ['build/renderer.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
],
function () {
// When the content scripts are already registered, an error will be thrown.
// It happens when the service worker process is incorrectly duplicated.
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
}
async function dynamicallyInjectContentScripts() {
const contentScriptsToInject = [
{
id: 'hook',
matches: ['<all_urls>'],
js: ['build/installHook.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
);
{
id: 'renderer',
matches: ['<all_urls>'],
js: ['build/renderer.js'],
runAt: 'document_start',
world: chrome.scripting.ExecutionWorld.MAIN,
},
];
try {
// For some reason dynamically injected scripts might be already registered
// Registering them again will fail, which will result into
// __REACT_DEVTOOLS_GLOBAL_HOOK__ hook not being injected
// Not specifying ids, because Chrome throws an error
// if id of non-injected script is provided
await chrome.scripting.unregisterContentScripts();
// equivalent logic for Firefox is in prepareInjection.js
// Manifest V3 method of injecting content script
// TODO(hoxyq): migrate Firefox to V3 manifests
// Note: the "world" option in registerContentScripts is only available in Chrome v102+
// It's critical since it allows us to directly run scripts on the "main" world on the page
// "document_start" allows it to run before the page's scripts
// so the hook can be detected by react reconciler
await chrome.scripting.registerContentScripts(contentScriptsToInject);
} catch (error) {
console.error(error);
}
}
if (!IS_FIREFOX) {
dynamicallyInjectContentScripts();
}
chrome.runtime.onConnect.addListener(function (port) {

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools-inline",
"version": "4.27.6",
"version": "4.27.8",
"description": "Embed react-devtools within a website",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -1243,10 +1243,9 @@ describe('Timeline profiler', () => {
function Example() {
const setHigh = React.useState(0)[1];
const setLow = React.useState(0)[1];
const startTransition = React.useTransition()[1];
updaterFn = () => {
startTransition(() => {
React.startTransition(() => {
setLow(prevLow => prevLow + 1);
});
setHigh(prevHigh => prevHigh + 1);
@ -1265,24 +1264,6 @@ describe('Timeline profiler', () => {
const timelineData = stopProfilingAndGetTimelineData();
expect(timelineData.schedulingEvents).toMatchInlineSnapshot(`
[
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000000001000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "
in Example (at **)",
"lanes": "0b0000000000000000000000010000000",
"timestamp": 10,
"type": "schedule-state-update",
"warning": null,
},
{
"componentName": "Example",
"componentStack": "

View File

@ -15,7 +15,7 @@ import {hasAssignedBackend} from './utils';
import type {DevToolsHook, ReactRenderer, RendererInterface} from './types';
// this is the backend that is compactible with all older React versions
// this is the backend that is compatible with all older React versions
function isMatchingRender(version: string): boolean {
return !hasAssignedBackend(version);
}
@ -31,6 +31,7 @@ export function initBackend(
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return () => {};
}
const subs = [
hook.sub(
'renderer-attached',
@ -64,10 +65,6 @@ export function initBackend(
];
const attachRenderer = (id: number, renderer: ReactRenderer) => {
// skip if already attached
if (renderer.attached) {
return;
}
// only attach if the renderer is compatible with the current version of the backend
if (!isMatchingRender(renderer.reconcilerVersion || renderer.version)) {
return;
@ -102,7 +99,6 @@ export function initBackend(
} else {
hook.emit('unsupported-renderer-version', id);
}
renderer.attached = true;
};
// Connect renderers that have already injected themselves.

View File

@ -244,7 +244,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -281,7 +280,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -318,7 +316,6 @@ export function attach(
parentIDStack.pop();
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {
@ -350,7 +347,6 @@ export function attach(
return result;
} catch (err) {
// $FlowFixMe[incompatible-type] found when upgrading Flow
parentIDStack = [];
throw err;
} finally {

View File

@ -54,7 +54,10 @@ let supportsUserTiming =
let supportsUserTimingV3 = false;
if (supportsUserTiming) {
const CHECK_V3_MARK = '__v3';
const markOptions = ({}: {startTime?: number});
const markOptions: {
detail?: mixed,
startTime?: number,
} = {};
Object.defineProperty(markOptions, 'startTime', {
get: function () {
supportsUserTimingV3 = true;
@ -64,7 +67,6 @@ if (supportsUserTiming) {
});
try {
// $FlowFixMe[extra-arg]: Flow expects the User Timing level 2 API.
performance.mark(CHECK_V3_MARK, markOptions);
} catch (error) {
// Ignore

View File

@ -171,8 +171,6 @@ export type ReactRenderer = {
// 18.0+
injectProfilingHooks?: (profilingHooks: DevToolsProfilingHooks) => void,
getLaneLabelMap?: () => Map<Lane, string> | null,
// set by backend after successful attaching
attached?: boolean,
...
};

View File

@ -169,7 +169,7 @@ export default class Store extends EventEmitter<{
// Renderer ID is needed to support inspection fiber props, state, and hooks.
_rootIDToRendererID: Map<number, number> = new Map();
// These options may be initially set by a confiugraiton option when constructing the Store.
// These options may be initially set by a configuration option when constructing the Store.
_supportsNativeInspection: boolean = true;
_supportsProfiling: boolean = false;
_supportsReloadAndProfile: boolean = false;
@ -486,7 +486,7 @@ export default class Store extends EventEmitter<{
}
containsElement(id: number): boolean {
return this._idToElement.get(id) != null;
return this._idToElement.has(id);
}
getElementAtIndex(index: number): Element | null {
@ -539,13 +539,13 @@ export default class Store extends EventEmitter<{
}
getElementIDAtIndex(index: number): number | null {
const element: Element | null = this.getElementAtIndex(index);
const element = this.getElementAtIndex(index);
return element === null ? null : element.id;
}
getElementByID(id: number): Element | null {
const element = this._idToElement.get(id);
if (element == null) {
if (element === undefined) {
console.warn(`No element found with id "${id}"`);
return null;
}
@ -607,7 +607,10 @@ export default class Store extends EventEmitter<{
let currentID = element.parentID;
let index = 0;
while (true) {
const current = ((this._idToElement.get(currentID): any): Element);
const current = this._idToElement.get(currentID);
if (current === undefined) {
return null;
}
const {children} = current;
for (let i = 0; i < children.length; i++) {
@ -615,7 +618,12 @@ export default class Store extends EventEmitter<{
if (childID === previousID) {
break;
}
const child = ((this._idToElement.get(childID): any): Element);
const child = this._idToElement.get(childID);
if (child === undefined) {
return null;
}
index += child.isCollapsed ? 1 : child.weight;
}
@ -637,7 +645,12 @@ export default class Store extends EventEmitter<{
if (rootID === currentID) {
break;
}
const root = ((this._idToElement.get(rootID): any): Element);
const root = this._idToElement.get(rootID);
if (root === undefined) {
return null;
}
index += root.weight;
}
@ -647,7 +660,7 @@ export default class Store extends EventEmitter<{
getOwnersListForElement(ownerID: number): Array<Element> {
const list: Array<Element> = [];
const element = this._idToElement.get(ownerID);
if (element != null) {
if (element !== undefined) {
list.push({
...element,
depth: 0,
@ -665,8 +678,8 @@ export default class Store extends EventEmitter<{
// Seems better to defer the cost, since the set of ids is probably pretty small.
const sortedIDs = Array.from(unsortedIDs).sort(
(idA, idB) =>
((this.getIndexOfElementID(idA): any): number) -
((this.getIndexOfElementID(idB): any): number),
(this.getIndexOfElementID(idA) || 0) -
(this.getIndexOfElementID(idB) || 0),
);
// Next we need to determine the appropriate depth for each element in the list.
@ -677,7 +690,7 @@ export default class Store extends EventEmitter<{
// at which point, our depth is just the depth of that node plus one.
sortedIDs.forEach(id => {
const innerElement = this._idToElement.get(id);
if (innerElement != null) {
if (innerElement !== undefined) {
let parentID = innerElement.parentID;
let depth = 0;
@ -689,7 +702,7 @@ export default class Store extends EventEmitter<{
break;
}
const parent = this._idToElement.get(parentID);
if (parent == null) {
if (parent === undefined) {
break;
}
parentID = parent.parentID;
@ -710,7 +723,7 @@ export default class Store extends EventEmitter<{
getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current != null) {
while (current !== undefined) {
if (current.parentID === 0) {
const rendererID = this._rootIDToRendererID.get(current.id);
return rendererID == null ? null : rendererID;
@ -723,7 +736,7 @@ export default class Store extends EventEmitter<{
getRootIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current != null) {
while (current !== undefined) {
if (current.parentID === 0) {
return current.id;
} else {
@ -765,10 +778,8 @@ export default class Store extends EventEmitter<{
const weightDelta = 1 - element.weight;
let parentElement: void | Element = ((this._idToElement.get(
element.parentID,
): any): Element);
while (parentElement != null) {
let parentElement = this._idToElement.get(element.parentID);
while (parentElement !== undefined) {
// We don't need to break on a collapsed parent in the same way as the expand case below.
// That's because collapsing a node doesn't "bubble" and affect its parents.
parentElement.weight += weightDelta;
@ -776,7 +787,7 @@ export default class Store extends EventEmitter<{
}
}
} else {
let currentElement = element;
let currentElement: ?Element = element;
while (currentElement != null) {
const oldWeight = currentElement.isCollapsed
? 1
@ -791,10 +802,8 @@ export default class Store extends EventEmitter<{
: currentElement.weight;
const weightDelta = newWeight - oldWeight;
let parentElement: void | Element = ((this._idToElement.get(
currentElement.parentID,
): any): Element);
while (parentElement != null) {
let parentElement = this._idToElement.get(currentElement.parentID);
while (parentElement !== undefined) {
parentElement.weight += weightDelta;
if (parentElement.isCollapsed) {
// It's important to break on a collapsed parent when expanding nodes.
@ -808,10 +817,8 @@ export default class Store extends EventEmitter<{
currentElement =
currentElement.parentID !== 0
? // $FlowFixMe[incompatible-type] found when upgrading Flow
this.getElementByID(currentElement.parentID)
: // $FlowFixMe[incompatible-type] found when upgrading Flow
null;
? this.getElementByID(currentElement.parentID)
: null;
}
}
@ -833,7 +840,7 @@ export default class Store extends EventEmitter<{
}
_adjustParentTreeWeight: (
parentElement: Element | null,
parentElement: ?Element,
weightDelta: number,
) => void = (parentElement, weightDelta) => {
let isInsideCollapsedSubTree = false;
@ -848,9 +855,7 @@ export default class Store extends EventEmitter<{
break;
}
parentElement = ((this._idToElement.get(
parentElement.parentID,
): any): Element);
parentElement = this._idToElement.get(parentElement.parentID);
}
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
@ -906,13 +911,16 @@ export default class Store extends EventEmitter<{
const stringTable: Array<string | null> = [
null, // ID = 0 corresponds to the null string.
];
const stringTableSize = operations[i++];
const stringTableSize = operations[i];
i++;
const stringTableEnd = i + stringTableSize;
while (i < stringTableEnd) {
const nextLength = operations[i++];
const nextString = utfDecodeString(
(operations.slice(i, i + nextLength): any),
);
const nextLength = operations[i];
i++;
const nextString = utfDecodeString(operations.slice(i, i + nextLength));
stringTable.push(nextString);
i += nextLength;
}
@ -921,7 +929,7 @@ export default class Store extends EventEmitter<{
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD: {
const id = ((operations[i + 1]: any): number);
const id = operations[i + 1];
const type = ((operations[i + 2]: any): ElementType);
i += 3;
@ -934,8 +942,6 @@ export default class Store extends EventEmitter<{
);
}
let ownerID: number = 0;
let parentID: number = ((null: any): number);
if (type === ElementTypeRoot) {
if (__DEBUG__) {
debug('Add', `new root node ${id}`);
@ -997,10 +1003,10 @@ export default class Store extends EventEmitter<{
haveRootsChanged = true;
} else {
parentID = ((operations[i]: any): number);
const parentID = operations[i];
i++;
ownerID = ((operations[i]: any): number);
const ownerID = operations[i];
i++;
const displayNameStringID = operations[i];
@ -1018,17 +1024,17 @@ export default class Store extends EventEmitter<{
);
}
if (!this._idToElement.has(parentID)) {
const parentElement = this._idToElement.get(parentID);
if (parentElement === undefined) {
this._throwAndEmitError(
Error(
`Cannot add child "${id}" to parent "${parentID}" because parent node was not found in the Store.`,
),
);
continue;
}
const parentElement = ((this._idToElement.get(
parentID,
): any): Element);
parentElement.children.push(id);
const [displayNameWithoutHOCs, hocDisplayNames] =
@ -1065,23 +1071,25 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
const removeLength = operations[i + 1];
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
const id = operations[i];
const element = this._idToElement.get(id);
if (!this._idToElement.has(id)) {
if (element === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove node "${id}" because no matching node was found in the Store.`,
),
);
continue;
}
i += 1;
const element = ((this._idToElement.get(id): any): Element);
const {children, ownerID, parentID, weight} = element;
if (children.length > 0) {
this._throwAndEmitError(
@ -1091,7 +1099,7 @@ export default class Store extends EventEmitter<{
this._idToElement.delete(id);
let parentElement = null;
let parentElement: ?Element = null;
if (parentID === 0) {
if (__DEBUG__) {
debug('Remove', `node ${id} root`);
@ -1106,14 +1114,18 @@ export default class Store extends EventEmitter<{
if (__DEBUG__) {
debug('Remove', `node ${id} from parent ${parentID}`);
}
parentElement = ((this._idToElement.get(parentID): any): Element);
parentElement = this._idToElement.get(parentID);
if (parentElement === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
),
);
continue;
}
const index = parentElement.children.indexOf(id);
parentElement.children.splice(index, 1);
}
@ -1167,19 +1179,21 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
const id = operations[i + 1];
const numChildren = operations[i + 2];
i += 3;
if (!this._idToElement.has(id)) {
const element = this._idToElement.get(id);
if (element === undefined) {
this._throwAndEmitError(
Error(
`Cannot reorder children for node "${id}" because no matching node was found in the Store.`,
),
);
continue;
}
const element = ((this._idToElement.get(id): any): Element);
const children = element.children;
if (children.length !== numChildren) {
this._throwAndEmitError(

View File

@ -237,7 +237,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
strictModeBadge = (
<a
className={styles.StrictModeNonCompliant}
href="https://fb.me/devtools-strict-mode"
href="https://react.dev/reference/react/StrictMode"
rel="noopener noreferrer"
target="_blank"
title="This component is not running in StrictMode. Click to learn more.">

View File

@ -45,7 +45,6 @@ const resource: Resource<
(element: Element) => {
const request = inProgressRequests.get(element);
if (request != null) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
return request.promise;
}

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -1,7 +1,7 @@
{
"private": true,
"name": "react-devtools-timeline",
"version": "4.27.6",
"version": "4.27.8",
"license": "MIT",
"dependencies": {
"@elg/speedscope": "1.9.0-a6f84db",

View File

@ -4,6 +4,23 @@
---
### 4.27.8
May 17, 2023
#### Bugfixes
* Fixed duplicated backend activation with multiple renderers ([hoxyq](https://github.com/hoxyq) in [#26807](https://github.com/facebook/react/pull/26807))
---
### 4.27.7
May 4, 2023
#### Bugfixes
* Fixed to work when browser devtools panel is reopened ([hoxyq](https://github.com/hoxyq) in [#26779](https://github.com/facebook/react/pull/26779))
* Fixed to work in Chrome incognito mode ([hoxyq](https://github.com/hoxyq) in [#26765](https://github.com/facebook/react/pull/26765))
---
### 4.27.6
April 20, 2023

View File

@ -1,6 +1,6 @@
{
"name": "react-devtools",
"version": "4.27.6",
"version": "4.27.8",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"repository": {
@ -27,7 +27,7 @@
"electron": "^23.1.2",
"ip": "^1.1.4",
"minimist": "^1.2.3",
"react-devtools-core": "4.27.6",
"react-devtools-core": "4.27.8",
"update-notifier": "^2.1.0"
}
}

View File

@ -2791,7 +2791,6 @@ function diffHydratedGenericElement(
case 'formAction':
if (enableFormActions) {
const serverValue = domElement.getAttribute(propKey);
const hasFormActionURL = serverValue === EXPECTED_FORM_ACTION_URL;
if (typeof value === 'function') {
extraAttributes.delete(propKey.toLowerCase());
// The server can set these extra properties to implement actions.
@ -2806,13 +2805,14 @@ function diffHydratedGenericElement(
extraAttributes.delete('method');
extraAttributes.delete('target');
}
if (hasFormActionURL) {
// Expected
continue;
}
warnForPropDifference(propKey, serverValue, value);
// Ideally we should be able to warn if the server value was not a function
// however since the function can return any of these attributes any way it
// wants as a custom progressive enhancement, there's nothing to compare to.
// We can check if the function has the $FORM_ACTION property on the client
// and if it's not, warn, but that's an unnecessary constraint that they
// have to have the extra extension that doesn't do anything on the client.
continue;
} else if (hasFormActionURL) {
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
extraAttributes.delete(propKey.toLowerCase());
warnForPropDifference(propKey, 'function', value);
continue;

View File

@ -89,6 +89,7 @@ import {
enableHostSingletons,
enableTrustedTypesIntegration,
diffInCommitPhase,
enableFormActions,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
@ -1038,150 +1039,164 @@ export function isHydratableText(text: string): boolean {
return text !== '';
}
export function shouldSkipHydratableForInstance(
instance: HydratableInstance,
type: string,
props: Props,
): boolean {
if (instance.nodeType !== ELEMENT_NODE) {
// This is a suspense boundary or Text node.
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
// and this is a hydration error.
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
// but it seems reasonable and conservative to reject this as a hydration error as well
return false;
} else if (
instance.nodeName.toLowerCase() !== type.toLowerCase() ||
isMarkedHoistable(instance)
) {
// We are either about to
return true;
} else {
// We have an Element with the right type.
const element: Element = (instance: any);
const anyProps = (props: any);
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
// using high entropy attributes for certain types. This technique will fail for strange insertions like
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
switch (type) {
// case 'title':
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
case 'meta': {
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
// implications are minimal
if (!element.hasAttribute('itemprop')) {
// This is a Hoistable
return true;
}
break;
}
case 'link': {
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
// matches.
const rel = element.getAttribute('rel');
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
// This is a stylesheet resource
return true;
} else if (
rel !== anyProps.rel ||
element.getAttribute('href') !==
(anyProps.href == null ? null : anyProps.href) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
element.getAttribute('title') !==
(anyProps.title == null ? null : anyProps.title)
) {
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
// and title could vary for rel alternate
return true;
}
break;
}
case 'style': {
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
if (element.hasAttribute('data-precedence')) {
// This is a style resource
return true;
}
break;
}
case 'script': {
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
// if we learn it is problematic
const srcAttr = element.getAttribute('src');
if (
srcAttr &&
element.hasAttribute('async') &&
!element.hasAttribute('itemprop')
) {
// This is an async script resource
return true;
} else if (
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
element.getAttribute('type') !==
(anyProps.type == null ? null : anyProps.type) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
) {
// This script is for a different src
return true;
}
break;
}
}
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
// that should work in the vast majority of cases.
return false;
}
}
export function shouldSkipHydratableForTextInstance(
instance: HydratableInstance,
): boolean {
return instance.nodeType === ELEMENT_NODE;
}
export function shouldSkipHydratableForSuspenseInstance(
instance: HydratableInstance,
): boolean {
return instance.nodeType === ELEMENT_NODE;
}
export function canHydrateInstance(
instance: HydratableInstance,
type: string,
props: Props,
inRootOrSingleton: boolean,
): null | Instance {
if (
instance.nodeType !== ELEMENT_NODE ||
instance.nodeName.toLowerCase() !== type.toLowerCase()
) {
return null;
} else {
return ((instance: any): Instance);
while (instance.nodeType === ELEMENT_NODE) {
const element: Element = (instance: any);
const anyProps = (props: any);
if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
if (!inRootOrSingleton || !enableHostSingletons) {
// Usually we error for mismatched tags.
if (
enableFormActions &&
element.nodeName === 'INPUT' &&
(element: any).type === 'hidden'
) {
// If we have extra hidden inputs, we don't mismatch. This allows us to embed
// extra form data in the original form.
} else {
return null;
}
}
// In root or singleton parents we skip past mismatched instances.
} else if (!inRootOrSingleton || !enableHostSingletons) {
// Match
if (
enableFormActions &&
type === 'input' &&
(element: any).type === 'hidden' &&
anyProps.type !== 'hidden'
) {
// Skip past hidden inputs unless that's what we're looking for. This allows us
// embed extra form data in the original form.
} else {
return element;
}
} else if (isMarkedHoistable(element)) {
// We've already claimed this as a hoistable which isn't hydrated this way so we skip past it.
} else {
// We have an Element with the right type.
// We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
// we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
// using high entropy attributes for certain types. This technique will fail for strange insertions like
// extension prepending <div> in the <body> but that already breaks before and that is an edge case.
switch (type) {
// case 'title':
//We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
// and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
case 'meta': {
// The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
// not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
// are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
// implications are minimal
if (!element.hasAttribute('itemprop')) {
// This is a Hoistable
break;
}
return element;
}
case 'link': {
// Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
// and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
// matches.
const rel = element.getAttribute('rel');
if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
// This is a stylesheet resource
break;
} else if (
rel !== anyProps.rel ||
element.getAttribute('href') !==
(anyProps.href == null ? null : anyProps.href) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
element.getAttribute('title') !==
(anyProps.title == null ? null : anyProps.title)
) {
// rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
// and title could vary for rel alternate
break;
}
return element;
}
case 'style': {
// Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
// in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
if (element.hasAttribute('data-precedence')) {
// This is a style resource
break;
}
return element;
}
case 'script': {
// Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
// to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
// in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
// Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
// if we learn it is problematic
const srcAttr = element.getAttribute('src');
if (
srcAttr &&
element.hasAttribute('async') &&
!element.hasAttribute('itemprop')
) {
// This is an async script resource
break;
} else if (
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
element.getAttribute('type') !==
(anyProps.type == null ? null : anyProps.type) ||
element.getAttribute('crossorigin') !==
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
) {
// This script is for a different src
break;
}
return element;
}
default: {
// We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
// and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
// that should work in the vast majority of cases.
return element;
}
}
}
const nextInstance = getNextHydratableSibling(element);
if (nextInstance === null) {
break;
}
instance = nextInstance;
}
// This is a suspense boundary or Text node or we got the end.
// Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
// and this is a hydration error.
// Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
// but it seems reasonable and conservative to reject this as a hydration error as well
return null;
}
export function canHydrateTextInstance(
instance: HydratableInstance,
text: string,
inRootOrSingleton: boolean,
): null | TextInstance {
// Empty strings are not parsed by HTML so there won't be a correct match here.
if (text === '') return null;
if (instance.nodeType !== TEXT_NODE) {
// Empty strings are not parsed by HTML so there won't be a correct match here.
return null;
while (instance.nodeType !== TEXT_NODE) {
if (!inRootOrSingleton || !enableHostSingletons) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
}
// This has now been refined to a text node.
return ((instance: any): TextInstance);
@ -1189,9 +1204,17 @@ export function canHydrateTextInstance(
export function canHydrateSuspenseInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance {
if (instance.nodeType !== COMMENT_NODE) {
return null;
while (instance.nodeType !== COMMENT_NODE) {
if (!inRootOrSingleton || !enableHostSingletons) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
}
// This has now been refined to a suspense node.
return ((instance: any): SuspenseInstance);
@ -1416,12 +1439,14 @@ export function commitHydratedSuspenseInstance(
retryIfBlockedOn(suspenseInstance);
}
// @TODO remove this function once float lands and hydrated tail nodes
// are controlled by HostSingleton fibers
export function shouldDeleteUnhydratedTailInstances(
parentType: string,
): boolean {
return parentType !== 'head' && parentType !== 'body';
return (
(enableHostSingletons ||
(parentType !== 'head' && parentType !== 'body')) &&
(!enableFormActions || (parentType !== 'form' && parentType !== 'button'))
);
}
export function didNotMatchHydratedContainerTextInstance(

View File

@ -472,7 +472,6 @@ function addTrappedEventListener(
if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
const originalListener = listener;
// $FlowFixMe[missing-this-annot]
// $FlowFixMe[definition-cycle]
listener = function (...p) {
removeEventListener(
targetContainer,

View File

@ -46,7 +46,6 @@ if (document.body != null) {
}
});
// documentElement must already exist at this point
// $FlowFixMe[incompatible-call]
domBodyObserver.observe(document.documentElement, {childList: true});
}

View File

@ -7,7 +7,7 @@
* @flow
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes';
import {
checkHtmlStringCoercion,
@ -131,7 +131,7 @@ export type ResponseState = {
instructions: InstructionState,
// state for data streaming format
externalRuntimeConfig: BootstrapScriptDescriptor | null,
externalRuntimeScript: null | ExternalRuntimeScript,
// preamble and postamble chunks and state
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
@ -161,6 +161,7 @@ const endInlineScript = stringToPrecomputedChunk('</script>');
const startScriptSrc = stringToPrecomputedChunk('<script src="');
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
const scriptNonce = stringToPrecomputedChunk('" nonce="');
const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
@ -192,6 +193,10 @@ export type BootstrapScriptDescriptor = {
src: string,
integrity?: string,
};
export type ExternalRuntimeScript = {
src: string,
chunks: Array<Chunk | PrecomputedChunk>,
};
// Allows us to keep track of what we've already written so we can refer back to it.
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
// is set, the server will send instructions via data attributes (instead of inline scripts)
@ -211,7 +216,7 @@ export function createResponseState(
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);
const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
let externalRuntimeDesc = null;
let externalRuntimeScript: null | ExternalRuntimeScript = null;
let streamingFormat = ScriptStreamingFormat;
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(
@ -229,12 +234,27 @@ export function createResponseState(
if (externalRuntimeConfig !== undefined) {
streamingFormat = DataStreamingFormat;
if (typeof externalRuntimeConfig === 'string') {
externalRuntimeDesc = {
externalRuntimeScript = {
src: externalRuntimeConfig,
integrity: undefined,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig,
async: true,
integrity: undefined,
nonce: nonce,
});
} else {
externalRuntimeDesc = externalRuntimeConfig;
externalRuntimeScript = {
src: externalRuntimeConfig.src,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig.src,
async: true,
integrity: externalRuntimeConfig.integrity,
nonce: nonce,
});
}
}
}
@ -245,10 +265,17 @@ export function createResponseState(
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
const integrity =
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startScriptSrc,
stringToChunk(escapeTextForBrowser(src)),
);
if (nonce) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonce)),
);
}
if (integrity) {
bootstrapChunks.push(
scriptIntegirty,
@ -265,10 +292,18 @@ export function createResponseState(
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
const integrity =
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
bootstrapChunks.push(
startModuleSrc,
stringToChunk(escapeTextForBrowser(src)),
);
if (nonce) {
bootstrapChunks.push(
scriptNonce,
stringToChunk(escapeTextForBrowser(nonce)),
);
}
if (integrity) {
bootstrapChunks.push(
scriptIntegirty,
@ -288,7 +323,7 @@ export function createResponseState(
streamingFormat,
startInlineScript: inlineScriptWithNonce,
instructions: NothingSent,
externalRuntimeConfig: externalRuntimeDesc,
externalRuntimeScript,
htmlChunks: null,
headChunks: null,
hasBody: false,
@ -297,6 +332,7 @@ export function createResponseState(
preloadChunks: [],
hoistableChunks: [],
stylesToHoist: false,
nonce,
};
}
@ -632,6 +668,13 @@ function pushStringAttribute(
}
}
function makeFormFieldPrefix(responseState: ResponseState): string {
// I'm just reusing this counter. It's not really the same namespace as "name".
// It could just be its own counter.
const id = responseState.nextSuspenseID++;
return responseState.idPrefix + id;
}
// Since this will likely be repeated a lot in the HTML, we use a more concise message
// than on the client and hopefully it's googleable.
const actionJavaScriptURL = stringToPrecomputedChunk(
@ -641,6 +684,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
),
);
const startHiddenInputChunk = stringToPrecomputedChunk('<input type="hidden"');
function pushAdditionalFormField(
this: Array<Chunk | PrecomputedChunk>,
value: string | File,
key: string,
): void {
const target: Array<Chunk | PrecomputedChunk> = this;
target.push(startHiddenInputChunk);
if (typeof value !== 'string') {
throw new Error(
'File/Blob fields are not yet supported in progressive forms. ' +
'It probably means you are closing over binary data or FormData in a Server Action.',
);
}
pushStringAttribute(target, 'name', key);
pushStringAttribute(target, 'value', value);
target.push(endOfStartTagSelfClosing);
}
function pushAdditionalFormFields(
target: Array<Chunk | PrecomputedChunk>,
formData: null | FormData,
) {
if (formData !== null) {
// $FlowFixMe[prop-missing]: FormData has forEach.
formData.forEach(pushAdditionalFormField, target);
}
}
function pushFormActionAttribute(
target: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
@ -649,7 +722,8 @@ function pushFormActionAttribute(
formMethod: any,
formTarget: any,
name: any,
): void {
): null | FormData {
let formData = null;
if (enableFormActions && typeof formAction === 'function') {
// Function form actions cannot control the form properties
if (__DEV__) {
@ -678,37 +752,55 @@ function pushFormActionAttribute(
);
}
}
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
target.push(
attributeSeparator,
stringToChunk('formAction'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
injectFormReplayingRuntime(responseState);
} else {
// Plain form actions support all the properties, so we have to emit them.
if (name !== null) {
pushAttribute(target, 'name', name);
}
if (formAction !== null) {
pushAttribute(target, 'formAction', formAction);
}
if (formEncType !== null) {
pushAttribute(target, 'formEncType', formEncType);
}
if (formMethod !== null) {
pushAttribute(target, 'formMethod', formMethod);
}
if (formTarget !== null) {
pushAttribute(target, 'formTarget', formTarget);
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
if (typeof customAction === 'function') {
// This action has a custom progressive enhancement form that can submit the form
// back to the server if it's invoked before hydration. Such as a Server Action.
const prefix = makeFormFieldPrefix(responseState);
const customFields = formAction.$$FORM_ACTION(prefix);
name = customFields.name;
formAction = customFields.action || '';
formEncType = customFields.encType;
formMethod = customFields.method;
formTarget = customFields.target;
formData = customFields.data;
} else {
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
target.push(
attributeSeparator,
stringToChunk('formAction'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
name = null;
formAction = null;
formEncType = null;
formMethod = null;
formTarget = null;
injectFormReplayingRuntime(responseState);
}
}
if (name != null) {
pushAttribute(target, 'name', name);
}
if (formAction != null) {
pushAttribute(target, 'formAction', formAction);
}
if (formEncType != null) {
pushAttribute(target, 'formEncType', formEncType);
}
if (formMethod != null) {
pushAttribute(target, 'formMethod', formMethod);
}
if (formTarget != null) {
pushAttribute(target, 'formTarget', formTarget);
}
return formData;
}
function pushAttribute(
@ -1273,7 +1365,7 @@ function injectFormReplayingRuntime(responseState: ResponseState): void {
// to emit anything. It's always used.
if (
(responseState.instructions & SentFormReplayingRuntime) === NothingSent &&
(!enableFizzExternalRuntime || !responseState.externalRuntimeConfig)
(!enableFizzExternalRuntime || !responseState.externalRuntimeScript)
) {
responseState.instructions |= SentFormReplayingRuntime;
responseState.bootstrapChunks.unshift(
@ -1330,6 +1422,8 @@ function pushStartForm(
}
}
let formData = null;
let formActionName = null;
if (enableFormActions && typeof formAction === 'function') {
// Function form actions cannot control the form properties
if (__DEV__) {
@ -1352,36 +1446,60 @@ function pushStartForm(
);
}
}
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
target.push(
attributeSeparator,
stringToChunk('action'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
injectFormReplayingRuntime(responseState);
} else {
// Plain form actions support all the properties, so we have to emit them.
if (formAction !== null) {
pushAttribute(target, 'action', formAction);
}
if (formEncType !== null) {
pushAttribute(target, 'encType', formEncType);
}
if (formMethod !== null) {
pushAttribute(target, 'method', formMethod);
}
if (formTarget !== null) {
pushAttribute(target, 'target', formTarget);
const customAction: ReactCustomFormAction = formAction.$$FORM_ACTION;
if (typeof customAction === 'function') {
// This action has a custom progressive enhancement form that can submit the form
// back to the server if it's invoked before hydration. Such as a Server Action.
const prefix = makeFormFieldPrefix(responseState);
const customFields = formAction.$$FORM_ACTION(prefix);
formAction = customFields.action || '';
formEncType = customFields.encType;
formMethod = customFields.method;
formTarget = customFields.target;
formData = customFields.data;
formActionName = customFields.name;
} else {
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
// because we'll preventDefault in the Fizz runtime, but it can happen if a form is
// manually submitted or if someone calls stopPropagation before React gets the event.
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
// error message but the URL will be logged.
target.push(
attributeSeparator,
stringToChunk('action'),
attributeAssign,
actionJavaScriptURL,
attributeEnd,
);
formAction = null;
formEncType = null;
formMethod = null;
formTarget = null;
injectFormReplayingRuntime(responseState);
}
}
if (formAction != null) {
pushAttribute(target, 'action', formAction);
}
if (formEncType != null) {
pushAttribute(target, 'encType', formEncType);
}
if (formMethod != null) {
pushAttribute(target, 'method', formMethod);
}
if (formTarget != null) {
pushAttribute(target, 'target', formTarget);
}
target.push(endOfStartTag);
if (formActionName !== null) {
target.push(startHiddenInputChunk);
pushStringAttribute(target, 'name', formActionName);
target.push(endOfStartTagSelfClosing);
pushAdditionalFormFields(target, formData);
}
pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
@ -1474,7 +1592,7 @@ function pushInput(
}
}
pushFormActionAttribute(
const formData = pushFormActionAttribute(
target,
responseState,
formAction,
@ -1525,6 +1643,10 @@ function pushInput(
}
target.push(endOfStartTagSelfClosing);
// We place any additional hidden form fields after the input.
pushAdditionalFormFields(target, formData);
return null;
}
@ -1592,7 +1714,7 @@ function pushStartButton(
}
}
pushFormActionAttribute(
const formData = pushFormActionAttribute(
target,
responseState,
formAction,
@ -1603,6 +1725,10 @@ function pushStartButton(
);
target.push(endOfStartTag);
// We place any additional hidden form fields we need to include inside the button itself.
pushAdditionalFormFields(target, formData);
pushInnerHTML(target, innerHTML, children);
if (typeof children === 'string') {
// Special case children as a string to avoid the unnecessary comment.
@ -1610,6 +1736,7 @@ function pushStartButton(
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}
@ -4058,15 +4185,15 @@ export function writePreamble(
if (
enableFizzExternalRuntime &&
!willFlushAllSegments &&
responseState.externalRuntimeConfig
responseState.externalRuntimeScript
) {
// If the root segment is incomplete due to suspended tasks
// (e.g. willFlushAllSegments = false) and we are using data
// streaming format, ensure the external runtime is sent.
// (User code could choose to send this even earlier by calling
// preinit(...), if they know they will suspend).
const {src, integrity} = responseState.externalRuntimeConfig;
internalPreinitScript(resources, src, integrity);
const {src, chunks} = responseState.externalRuntimeScript;
internalPreinitScript(resources, src, chunks);
}
const htmlChunks = responseState.htmlChunks;
@ -5342,30 +5469,22 @@ function preinit(href: string, options: PreinitOptions): void {
}
}
// This method is trusted. It must only be called from within this codebase and it assumes the arguments
// conform to the types because no user input is being passed in. It also assumes that it is being called as
// part of a work or flush loop and therefore does not need to request Fizz to flush Resources.
function internalPreinitScript(
resources: Resources,
src: string,
integrity: ?string,
chunks: Array<Chunk | PrecomputedChunk>,
): void {
const key = getResourceKey('script', src);
let resource = resources.scriptsMap.get(key);
if (!resource) {
resource = {
type: 'script',
chunks: [],
chunks,
state: NoState,
props: null,
};
resources.scriptsMap.set(key, resource);
resources.scripts.add(resource);
pushScriptImpl(resource.chunks, {
async: true,
src,
integrity,
});
}
return;
}

View File

@ -9,6 +9,7 @@
import type {
BootstrapScriptDescriptor,
ExternalRuntimeScript,
FormatContext,
StreamingFormat,
InstructionState,
@ -48,7 +49,7 @@ export type ResponseState = {
streamingFormat: StreamingFormat,
startInlineScript: PrecomputedChunk,
instructions: InstructionState,
externalRuntimeConfig: BootstrapScriptDescriptor | null,
externalRuntimeScript: null | ExternalRuntimeScript,
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
headChunks: null | Array<Chunk | PrecomputedChunk>,
hasBody: boolean,
@ -85,7 +86,7 @@ export function createResponseState(
streamingFormat: responseState.streamingFormat,
startInlineScript: responseState.startInlineScript,
instructions: responseState.instructions,
externalRuntimeConfig: responseState.externalRuntimeConfig,
externalRuntimeScript: responseState.externalRuntimeScript,
htmlChunks: responseState.htmlChunks,
headChunks: responseState.headChunks,
hasBody: responseState.hasBody,

View File

@ -22,4 +22,5 @@ export {
preconnect,
preload,
preinit,
experimental_useFormStatus,
} from './src/server/ReactDOMServerRenderingStub';

View File

@ -22,6 +22,7 @@ let React;
let ReactDOMServer;
let ReactDOMClient;
let useFormStatus;
let useOptimistic;
describe('ReactDOMFizzForm', () => {
beforeEach(() => {
@ -30,6 +31,7 @@ describe('ReactDOMFizzForm', () => {
ReactDOMServer = require('react-dom/server.browser');
ReactDOMClient = require('react-dom/client');
useFormStatus = require('react-dom').experimental_useFormStatus;
useOptimistic = require('react').experimental_useOptimistic;
act = require('internal-test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
@ -181,7 +183,7 @@ describe('ReactDOMFizzForm', () => {
});
// @gate enableFormActions || !__DEV__
it('should warn when passing a string during SSR and function during hydration', async () => {
it('should ideally warn when passing a string during SSR and function during hydration', async () => {
function action(formData) {}
function App({isClient}) {
return (
@ -193,13 +195,10 @@ describe('ReactDOMFizzForm', () => {
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
await expect(async () => {
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
});
}).toErrorDev(
'Prop `action` did not match. Server: "action" Client: "function action(formData) {}"',
);
// This should ideally warn because only the client provides a function that doesn't line up.
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
});
});
// @gate enableFormActions || !__DEV__
@ -453,4 +452,150 @@ describe('ReactDOMFizzForm', () => {
expect(deletedTitle).toBe('Hello');
expect(rootActionCalled).toBe(false);
});
// @gate enableAsyncActions
it('useOptimistic returns passthrough value', async () => {
function App() {
const [optimisticState] = useOptimistic('hi');
return optimisticState;
}
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
expect(container.textContent).toBe('hi');
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
expect(container.textContent).toBe('hi');
});
// @gate enableFormActions
it('can provide a custom action on the server for actions', async () => {
const ref = React.createRef();
let foo;
function action(formData) {
foo = formData.get('foo');
}
action.$$FORM_ACTION = function (identifierPrefix) {
const extraFields = new FormData();
extraFields.append(identifierPrefix + 'hello', 'world');
return {
action: this.name,
name: identifierPrefix,
method: 'POST',
encType: 'multipart/form-data',
target: 'self',
data: extraFields,
};
};
function App() {
return (
<form action={action} ref={ref} method={null}>
<input type="text" name="foo" defaultValue="bar" />
</form>
);
}
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
const form = container.firstChild;
expect(form.getAttribute('action')).toBe('action');
expect(form.getAttribute('method')).toBe('POST');
expect(form.getAttribute('enctype')).toBe('multipart/form-data');
expect(form.getAttribute('target')).toBe('self');
const formActionName = form.firstChild.getAttribute('name');
expect(
container
.querySelector('input[name="' + formActionName + 'hello"]')
.getAttribute('value'),
).toBe('world');
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
submit(ref.current);
expect(foo).toBe('bar');
});
// @gate enableFormActions
it('can provide a custom action on buttons the server for actions', async () => {
const inputRef = React.createRef();
const buttonRef = React.createRef();
let foo;
function action(formData) {
foo = formData.get('foo');
}
action.$$FORM_ACTION = function (identifierPrefix) {
const extraFields = new FormData();
extraFields.append(identifierPrefix + 'hello', 'world');
return {
action: this.name,
name: identifierPrefix,
method: 'POST',
encType: 'multipart/form-data',
target: 'self',
data: extraFields,
};
};
function App() {
return (
<form>
<input type="hidden" name="foo" value="bar" />
<input
type="submit"
formAction={action}
method={null}
ref={inputRef}
/>
<button formAction={action} ref={buttonRef} target={null} />
</form>
);
}
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
const input = container.getElementsByTagName('input')[1];
const button = container.getElementsByTagName('button')[0];
expect(input.getAttribute('formaction')).toBe('action');
expect(input.getAttribute('formmethod')).toBe('POST');
expect(input.getAttribute('formenctype')).toBe('multipart/form-data');
expect(input.getAttribute('formtarget')).toBe('self');
expect(button.getAttribute('formaction')).toBe('action');
expect(button.getAttribute('formmethod')).toBe('POST');
expect(button.getAttribute('formenctype')).toBe('multipart/form-data');
expect(button.getAttribute('formtarget')).toBe('self');
const inputName = input.getAttribute('name');
const buttonName = button.getAttribute('name');
expect(
container
.querySelector('input[name="' + inputName + 'hello"]')
.getAttribute('value'),
).toBe('world');
expect(
container
.querySelector('input[name="' + buttonName + 'hello"]')
.getAttribute('value'),
).toBe('world');
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App />);
});
submit(inputRef.current);
expect(foo).toBe('bar');
foo = null;
submit(buttonRef.current);
expect(foo).toBe('bar');
});
});

View File

@ -20,6 +20,7 @@ let JSDOM;
let Stream;
let Scheduler;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let Suspense;
@ -73,6 +74,7 @@ describe('ReactDOMFizzServer', () => {
Scheduler = require('scheduler');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server');
Stream = require('stream');
@ -574,7 +576,7 @@ describe('ReactDOMFizzServer', () => {
);
});
it('should support nonce scripts', async () => {
it('should support nonce for bootstrap and runtime scripts', async () => {
CSPnonce = 'R4nd0m';
try {
let resolve;
@ -591,11 +593,26 @@ describe('ReactDOMFizzServer', () => {
<Lazy text="Hello" />
</Suspense>
</div>,
{nonce: 'R4nd0m'},
{
nonce: 'R4nd0m',
bootstrapScriptContent: 'function noop(){}',
bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
},
);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
// check that there are 4 scripts with a matching nonce:
// The runtime script, an inline bootstrap script, and two src scripts
expect(
Array.from(container.getElementsByTagName('script')).filter(
node => node.getAttribute('nonce') === CSPnonce,
).length,
).toEqual(4);
await act(() => {
resolve({default: Text});
});
@ -605,6 +622,53 @@ describe('ReactDOMFizzServer', () => {
}
});
it('should not automatically add nonce to rendered scripts', async () => {
CSPnonce = 'R4nd0m';
try {
await act(async () => {
const {pipe} = renderToPipeableStream(
<html>
<body>
<script nonce={CSPnonce}>{'try { foo() } catch (e) {} ;'}</script>
<script nonce={CSPnonce} src="foo" async={true} />
<script src="bar" />
<script src="baz" integrity="qux" async={true} />
<script type="module" src="quux" async={true} />
<script type="module" src="corge" async={true} />
<script
type="module"
src="grault"
integrity="garply"
async={true}
/>
</body>
</html>,
{
nonce: CSPnonce,
},
);
pipe(writable);
});
expect(
stripExternalRuntimeInNodes(
document.getElementsByTagName('script'),
renderOptions.unstable_externalRuntimeSrc,
).map(n => n.outerHTML),
).toEqual([
`<script nonce="${CSPnonce}" src="foo" async=""></script>`,
`<script src="baz" integrity="qux" async=""></script>`,
`<script type="module" src="quux" async=""></script>`,
`<script type="module" src="corge" async=""></script>`,
`<script type="module" src="grault" integrity="garply" async=""></script>`,
`<script nonce="${CSPnonce}">try { foo() } catch (e) {} ;</script>`,
`<script src="bar"></script>`,
]);
} finally {
CSPnonce = null;
}
});
it('should client render a boundary if a lazy component rejects', async () => {
let rejectComponent;
const LazyComponent = React.lazy(() => {
@ -2445,6 +2509,98 @@ describe('ReactDOMFizzServer', () => {
},
);
it('can hydrate uSES in StrictMode with different client and server snapshot (sync)', async () => {
function subscribe() {
return () => {};
}
function getClientSnapshot() {
return 'Yay!';
}
function getServerSnapshot() {
return 'Nay!';
}
function App() {
const value = useSyncExternalStore(
subscribe,
getClientSnapshot,
getServerSnapshot,
);
Scheduler.log(value);
return value;
}
const element = (
<React.StrictMode>
<App />
</React.StrictMode>
);
await act(async () => {
const {pipe} = renderToPipeableStream(element);
pipe(writable);
});
assertLog(['Nay!']);
expect(getVisibleChildren(container)).toEqual('Nay!');
await clientAct(() => {
ReactDOM.flushSync(() => {
ReactDOMClient.hydrateRoot(container, element);
});
});
expect(getVisibleChildren(container)).toEqual('Yay!');
assertLog(['Nay!', 'Yay!']);
});
it('can hydrate uSES in StrictMode with different client and server snapshot (concurrent)', async () => {
function subscribe() {
return () => {};
}
function getClientSnapshot() {
return 'Yay!';
}
function getServerSnapshot() {
return 'Nay!';
}
function App() {
const value = useSyncExternalStore(
subscribe,
getClientSnapshot,
getServerSnapshot,
);
Scheduler.log(value);
return value;
}
const element = (
<React.StrictMode>
<App />
</React.StrictMode>
);
await act(async () => {
const {pipe} = renderToPipeableStream(element);
pipe(writable);
});
assertLog(['Nay!']);
expect(getVisibleChildren(container)).toEqual('Nay!');
await clientAct(() => {
React.startTransition(() => {
ReactDOMClient.hydrateRoot(container, element);
});
});
expect(getVisibleChildren(container)).toEqual('Yay!');
assertLog(['Nay!', 'Yay!']);
});
it(
'errors during hydration force a client render at the nearest Suspense ' +
'boundary, and during the client render it recovers',
@ -3705,7 +3861,7 @@ describe('ReactDOMFizzServer', () => {
Array.from(document.head.getElementsByTagName('script')).map(
n => n.outerHTML,
),
).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
expect(getVisibleChildren(document)).toEqual(
<html>

View File

@ -486,4 +486,21 @@ describe('ReactDOMFizzServerBrowser', () => {
'<!DOCTYPE html><html><head><title>foo</title></head><body>bar</body></html>',
);
});
it('should support nonce attribute for bootstrap scripts', async () => {
const nonce = 'R4nd0m';
const stream = await ReactDOMFizzServer.renderToReadableStream(
<div>hello world</div>,
{
nonce,
bootstrapScriptContent: 'INIT();',
bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
},
);
const result = await readResult(stream);
expect(result).toMatchInlineSnapshot(
`"<div>hello world</div><script nonce="${nonce}">INIT();</script><script src="init.js" nonce="${nonce}" async=""></script><script type="module" src="init.mjs" nonce="${nonce}" async=""></script>"`,
);
});
});

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
* Copyright (c) Meta Platforms, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.

View File

@ -695,4 +695,41 @@ describe('ReactDOMServerHydration', () => {
);
}
});
// @gate enableFormActions
it('allows rendering extra hidden inputs in a form', async () => {
const element = document.createElement('div');
element.innerHTML =
'<form>' +
'<input type="hidden" /><input type="hidden" name="a" value="A" />' +
'<input type="hidden" /><input type="submit" name="b" value="B" />' +
'<input type="hidden" /><button name="c" value="C"></button>' +
'<input type="hidden" />' +
'</form>';
const form = element.firstChild;
const ref = React.createRef();
const a = React.createRef();
const b = React.createRef();
const c = React.createRef();
await act(async () => {
ReactDOMClient.hydrateRoot(
element,
<form ref={ref}>
<input type="hidden" name="a" value="A" ref={a} />
<input type="submit" name="b" value="B" ref={b} />
<button name="c" value="C" ref={c} />
</form>,
);
});
// The content should not have been client rendered.
expect(ref.current).toBe(form);
expect(a.current.name).toBe('a');
expect(a.current.value).toBe('A');
expect(b.current.name).toBe('b');
expect(b.current.value).toBe('B');
expect(c.current.name).toBe('c');
expect(c.current.value).toBe('C');
});
});

View File

@ -81,4 +81,16 @@ describe('react-dom-server-rendering-stub', () => {
);
expect(x).toBe(false);
});
// @gate enableFormActions
// @gate enableAsyncActions
it('exports experimental_useFormStatus', async () => {
function App() {
const {pending} = ReactDOM.experimental_useFormStatus();
return 'Pending: ' + pending;
}
const result = await ReactDOMFizzServer.renderToStaticMarkup(<App />);
expect(result).toEqual('Pending: false');
});
});

View File

@ -8,6 +8,7 @@
*/
export {preinit, preload, preconnect, prefetchDNS} from '../ReactDOMFloat';
export {useFormStatus as experimental_useFormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
export function createPortal() {
throw new Error(

View File

@ -103,6 +103,12 @@ async function executeScript(script: Element) {
} else {
const newScript = ownerDocument.createElement('script');
newScript.textContent = script.textContent;
// make sure to add nonce back to script if it exists
const scriptNonce = script.getAttribute('nonce');
if (scriptNonce) {
newScript.setAttribute('nonce', scriptNonce);
}
parent.insertBefore(newScript, script);
parent.removeChild(script);
}

View File

@ -8,9 +8,9 @@
"directory": "packages/react-native-renderer"
},
"dependencies": {
"scheduler": "^0.11.0"
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^17.0.0"
"react": "^18.0.0"
}
}

View File

@ -13,6 +13,7 @@ import {dispatchEvent} from './ReactFabricEventEmitter';
import {
DefaultEventPriority,
DiscreteEventPriority,
type EventPriority,
} from 'react-reconciler/src/ReactEventPriorities';
import {HostText} from 'react-reconciler/src/ReactWorkTags';
@ -317,7 +318,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
return false;
}
export function getCurrentEventPriority(): * {
export function getCurrentEventPriority(): EventPriority {
const currentEventPriority = fabricGetCurrentEventPriority
? fabricGetCurrentEventPriority()
: null;

View File

@ -24,7 +24,10 @@ import {
} from './ReactNativeComponentTree';
import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
import {DefaultEventPriority} from 'react-reconciler/src/ReactEventPriorities';
import {
DefaultEventPriority,
type EventPriority,
} from 'react-reconciler/src/ReactEventPriorities';
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
@ -217,9 +220,10 @@ export function getChildHostContext(
}
}
export function getPublicInstance(instance: Instance): * {
export function getPublicInstance(instance: Instance): PublicInstance {
// $FlowExpectedError[prop-missing] For compatibility with Fabric
if (instance.canonical != null && instance.canonical.publicInstance != null) {
// $FlowFixMe[incompatible-return]
return instance.canonical.publicInstance;
}
@ -262,7 +266,7 @@ export function shouldSetTextContent(type: string, props: Props): boolean {
return false;
}
export function getCurrentEventPriority(): * {
export function getCurrentEventPriority(): EventPriority {
return DefaultEventPriority;
}

View File

@ -1,359 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactNodeList} from 'shared/ReactTypes';
import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import {
writeChunk,
writeChunkAndReturn,
stringToChunk,
stringToPrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
export const isPrimaryRenderer = true;
// Every list of children or string is null terminated.
const END_TAG = 0;
// Tree node tags.
const INSTANCE_TAG = 1;
const PLACEHOLDER_TAG = 2;
const SUSPENSE_PENDING_TAG = 3;
const SUSPENSE_COMPLETE_TAG = 4;
const SUSPENSE_CLIENT_RENDER_TAG = 5;
// Command tags.
const SEGMENT_TAG = 1;
const SUSPENSE_UPDATE_TO_COMPLETE_TAG = 2;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG = 3;
const END = new Uint8Array(1);
END[0] = END_TAG;
const PLACEHOLDER = new Uint8Array(1);
PLACEHOLDER[0] = PLACEHOLDER_TAG;
const INSTANCE = new Uint8Array(1);
INSTANCE[0] = INSTANCE_TAG;
const SUSPENSE_PENDING = new Uint8Array(1);
SUSPENSE_PENDING[0] = SUSPENSE_PENDING_TAG;
const SUSPENSE_COMPLETE = new Uint8Array(1);
SUSPENSE_COMPLETE[0] = SUSPENSE_COMPLETE_TAG;
const SUSPENSE_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_CLIENT_RENDER[0] = SUSPENSE_CLIENT_RENDER_TAG;
const SEGMENT = new Uint8Array(1);
SEGMENT[0] = SEGMENT_TAG;
const SUSPENSE_UPDATE_TO_COMPLETE = new Uint8Array(1);
SUSPENSE_UPDATE_TO_COMPLETE[0] = SUSPENSE_UPDATE_TO_COMPLETE_TAG;
const SUSPENSE_UPDATE_TO_CLIENT_RENDER = new Uint8Array(1);
SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
export type Resources = void;
export type BoundaryResources = void;
// Per response,
export type ResponseState = {
nextSuspenseID: number,
};
// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(): ResponseState {
return {
nextSuspenseID: 0,
};
}
// isInAParentText
export type FormatContext = boolean;
export function createRootFormatContext(): FormatContext {
return false;
}
export function getChildFormatContext(
parentContext: FormatContext,
type: string,
props: Object,
): FormatContext {
const prevIsInAParentText = parentContext;
const isInAParentText =
type === 'AndroidTextInput' || // Android
type === 'RCTMultilineTextInputView' || // iOS
type === 'RCTSinglelineTextInputView' || // iOS
type === 'RCTText' ||
type === 'RCTVirtualText';
if (prevIsInAParentText !== isInAParentText) {
return isInAParentText;
} else {
return parentContext;
}
}
// This object is used to lazily reuse the ID of the first generated node, or assign one.
// This is very specific to DOM where we can't assign an ID to.
export type SuspenseBoundaryID = number;
export const UNINITIALIZED_SUSPENSE_BOUNDARY_ID = -1;
export function assignSuspenseBoundaryID(
responseState: ResponseState,
): SuspenseBoundaryID {
return responseState.nextSuspenseID++;
}
export function makeId(
responseState: ResponseState,
treeId: string,
localId: number,
): string {
throw new Error('Not implemented');
}
const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
export function pushTextInstance(
target: Array<Chunk | PrecomputedChunk>,
text: string,
responseState: ResponseState,
// This Renderer does not use this argument
textEmbedded: boolean,
): boolean {
target.push(
INSTANCE,
RAW_TEXT, // Type
END, // Null terminated type string
// TODO: props { text: text }
END, // End of children
);
return false;
}
export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
resources: Resources,
responseState: ResponseState,
formatContext: FormatContext,
textEmbedded: boolean,
): ReactNodeList {
target.push(
INSTANCE,
stringToChunk(type),
END, // Null terminated type string
// TODO: props
);
return props.children;
}
export function pushEndInstance(
target: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
formatContext: FormatContext,
): void {
target.push(END);
}
// In this Renderer this is a noop
export function pushSegmentFinale(
target: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
lastPushedText: boolean,
textEmbedded: boolean,
): void {}
export function writeCompletedRoot(
destination: Destination,
responseState: ResponseState,
): boolean {
return true;
}
// IDs are formatted as little endian Uint16
function formatID(id: number): Uint8Array {
if (id > 0xffff) {
throw new Error(
'More boundaries or placeholders than we expected to ever emit.',
);
}
const buffer = new Uint8Array(2);
buffer[0] = (id >>> 8) & 0xff;
buffer[1] = id & 0xff;
return buffer;
}
// Structural Nodes
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
// display. It's never visible to users.
export function writePlaceholder(
destination: Destination,
responseState: ResponseState,
id: number,
): boolean {
writeChunk(destination, PLACEHOLDER);
return writeChunkAndReturn(destination, formatID(id));
}
// Suspense boundaries are encoded as comments.
export function writeStartCompletedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
): boolean {
return writeChunkAndReturn(destination, SUSPENSE_COMPLETE);
}
export function pushStartCompletedSuspenseBoundary(
target: Array<Chunk | PrecomputedChunk>,
): void {
target.push(SUSPENSE_COMPLETE);
}
export function writeStartPendingSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
id: SuspenseBoundaryID,
): boolean {
writeChunk(destination, SUSPENSE_PENDING);
return writeChunkAndReturn(destination, formatID(id));
}
export function writeStartClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
// TODO: encode error for native
errorDigest: ?string,
errorMessage: ?string,
errorComponentStack: ?string,
): boolean {
return writeChunkAndReturn(destination, SUSPENSE_CLIENT_RENDER);
}
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
): boolean {
return writeChunkAndReturn(destination, END);
}
export function pushEndCompletedSuspenseBoundary(
target: Array<Chunk | PrecomputedChunk>,
): void {
target.push(END);
}
export function writeEndPendingSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
): boolean {
return writeChunkAndReturn(destination, END);
}
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
responseState: ResponseState,
): boolean {
return writeChunkAndReturn(destination, END);
}
export function writeStartSegment(
destination: Destination,
responseState: ResponseState,
formatContext: FormatContext,
id: number,
): boolean {
writeChunk(destination, SEGMENT);
return writeChunkAndReturn(destination, formatID(id));
}
export function writeEndSegment(
destination: Destination,
formatContext: FormatContext,
): boolean {
return writeChunkAndReturn(destination, END);
}
// Instruction Set
export function writeCompletedSegmentInstruction(
destination: Destination,
responseState: ResponseState,
contentSegmentID: number,
): boolean {
// We don't need to emit this. Instead the client will keep track of pending placeholders.
// TODO: Returning true here is not correct. Avoid having to call this function at all.
return true;
}
export function writeCompletedBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
contentSegmentID: number,
resources: BoundaryResources,
): boolean {
writeChunk(destination, SUSPENSE_UPDATE_TO_COMPLETE);
writeChunk(destination, formatID(boundaryID));
return writeChunkAndReturn(destination, formatID(contentSegmentID));
}
export function writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
// TODO: encode error for native
errorDigest: ?string,
errorMessage: ?string,
errorComponentStack: ?string,
): boolean {
writeChunk(destination, SUSPENSE_UPDATE_TO_CLIENT_RENDER);
return writeChunkAndReturn(destination, formatID(boundaryID));
}
export function writePreamble(
destination: Destination,
resources: Resources,
responseState: ResponseState,
willFlushAllSegments: boolean,
) {}
export function writeHoistables(
destination: Destination,
resources: Resources,
responseState: ResponseState,
) {}
export function writePostamble(
destination: Destination,
responseState: ResponseState,
) {}
export function hoistResources(
resources: Resources,
boundaryResources: BoundaryResources,
) {}
export function prepareHostDispatcher() {}
export function createResources() {}
export function createBoundaryResources() {}
export function setCurrentlyRenderingBoundaryResourcesTarget(
resources: Resources,
boundaryResources: ?BoundaryResources,
) {}
export function writeResourcesForBoundary(
destination: Destination,
boundaryResources: BoundaryResources,
responseState: ResponseState,
): boolean {
return true;
}
export type TransitionStatus = mixed;
export const NotPendingTransition: TransitionStatus = null;

View File

@ -1,19 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export const isPrimaryRenderer = true;
export type Hints = null;
export type HintModel = '';
export function createHints(): null {
return null;
}
export function prepareHostDispatcher() {}

View File

@ -55,6 +55,7 @@ import {
enableLegacyHidden,
enableHostSingletons,
diffInCommitPhase,
alwaysThrottleRetries,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@ -2905,17 +2906,35 @@ function commitMutationEffectsOnFiber(
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// TODO: We should mark a flag on the Suspense fiber itself, rather than
// relying on the Offscreen fiber having a flag also being marked. The
// reason is that this offscreen fiber might not be part of the work-in-
// progress tree! It could have been reused from a previous render. This
// doesn't lead to incorrect behavior because we don't rely on the flag
// check alone; we also compare the states explicitly below. But for
// modeling purposes, we _should_ be able to rely on the flag check alone.
// So this is a bit fragile.
//
// Also, all this logic could/should move to the passive phase so it
// doesn't block paint.
const offscreenFiber: Fiber = (finishedWork.child: any);
if (offscreenFiber.flags & Visibility) {
const newState: OffscreenState | null = offscreenFiber.memoizedState;
const isHidden = newState !== null;
if (isHidden) {
const wasHidden =
offscreenFiber.alternate !== null &&
offscreenFiber.alternate.memoizedState !== null;
if (!wasHidden) {
// TODO: Move to passive phase
// Throttle the appearance and disappearance of Suspense fallbacks.
const isShowingFallback =
(finishedWork.memoizedState: SuspenseState | null) !== null;
const wasShowingFallback =
current !== null &&
(current.memoizedState: SuspenseState | null) !== null;
if (alwaysThrottleRetries) {
if (isShowingFallback !== wasShowingFallback) {
// A fallback is either appearing or disappearing.
markCommitTimeOfFallback();
}
} else {
if (isShowingFallback && !wasShowingFallback) {
// Old behavior. Only mark when a fallback appears, not when
// it disappears.
markCommitTimeOfFallback();
}
}

View File

@ -31,9 +31,6 @@ export const getNextHydratableSibling = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;
export const getFirstHydratableChildWithinSuspenseInstance = shim;
export const shouldSkipHydratableForInstance = shim;
export const shouldSkipHydratableForTextInstance = shim;
export const shouldSkipHydratableForSuspenseInstance = shim;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateSuspenseInstance = shim;

View File

@ -149,11 +149,13 @@ import type {ThenableState} from './ReactFiberThenable';
import type {BatchConfigTransition} from './ReactFiberTracingMarkerComponent';
import {requestAsyncActionContext} from './ReactFiberAsyncAction';
import {HostTransitionContext} from './ReactFiberHostContext';
import {requestTransitionLane} from './ReactFiberRootScheduler';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
export type Update<S, A> = {
lane: Lane,
revertLane: Lane,
action: A,
hasEagerState: boolean,
eagerState: S | null,
@ -1136,6 +1138,14 @@ function updateReducer<S, I, A>(
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook();
return updateReducerImpl(hook, ((currentHook: any): Hook), reducer);
}
function updateReducerImpl<S, A>(
hook: Hook,
current: Hook,
reducer: (S, A) => S,
): [S, Dispatch<A>] {
const queue = hook.queue;
if (queue === null) {
@ -1146,10 +1156,8 @@ function updateReducer<S, I, A>(
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
let baseQueue = hook.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
@ -1180,7 +1188,7 @@ function updateReducer<S, I, A>(
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newState = hook.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
@ -1206,6 +1214,7 @@ function updateReducer<S, I, A>(
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
@ -1228,18 +1237,68 @@ function updateReducer<S, I, A>(
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
// Check if this is an optimistic update.
const revertLane = update.revertLane;
if (!enableAsyncActions || revertLane === NoLane) {
// This is not an optimistic update, and we're going to apply it now.
// But, if there were earlier updates that were skipped, we need to
// leave this update in the queue so it can be rebased later.
if (newBaseQueueLast !== null) {
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
revertLane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
} else {
// This is an optimistic update. If the "revert" priority is
// sufficient, don't apply the update. Otherwise, apply the update,
// but leave it in the queue so it can be either reverted or
// rebased in a subsequent render.
if (isSubsetOfLanes(renderLanes, revertLane)) {
// The transition that this optimistic update is associated with
// has finished. Pretend the update doesn't exist by skipping
// over it.
update = update.next;
continue;
} else {
const clone: Update<S, A> = {
// Once we commit an optimistic update, we shouldn't uncommit it
// until the transition it is associated with has finished
// (represented by revertLane). Using NoLane here works because 0
// is a subset of all bitmasks, so this will never be skipped by
// the check above.
lane: NoLane,
// Reuse the same revertLane so we know when the transition
// has finished.
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
revertLane,
);
markSkippedUpdateLanes(revertLane);
}
}
// Process this update.
@ -1717,8 +1776,6 @@ function mountSyncExternalStore<T>(
// clean-up function, and we track the deps correctly, we can call pushEffect
// directly, without storing any additional state. For the same reason, we
// don't need to set a static flag, either.
// TODO: We can move this to the passive phase once we add a pre-commit
// consistency check. See the next comment.
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
@ -1740,15 +1797,28 @@ function updateSyncExternalStore<T>(
// Read the current snapshot from the store on every render. This breaks the
// normal rules of React, and only works because store updates are
// always synchronous.
const nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
const cachedSnapshot = getSnapshot();
if (!is(nextSnapshot, cachedSnapshot)) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
// Needed for strict mode double render
if (getServerSnapshot === undefined) {
throw new Error(
'Missing getServerSnapshot, which is required for ' +
'server-rendered content. Will revert to client rendering.',
);
}
nextSnapshot = getServerSnapshot();
} else {
nextSnapshot = getSnapshot();
if (__DEV__) {
if (!didWarnUncachedGetSnapshot) {
const cachedSnapshot = getSnapshot();
if (!is(nextSnapshot, cachedSnapshot)) {
console.error(
'The result of getSnapshot should be cached to avoid an infinite loop',
);
didWarnUncachedGetSnapshot = true;
}
}
}
}
@ -1771,7 +1841,7 @@ function updateSyncExternalStore<T>(
if (
inst.getSnapshot !== getSnapshot ||
snapshotChanged ||
// Check if the susbcribe function changed. We can save some memory by
// Check if the subscribe function changed. We can save some memory by
// checking whether we scheduled a subscription effect above.
(workInProgressHook !== null &&
workInProgressHook.memoizedState.tag & HookHasEffect)
@ -1795,7 +1865,7 @@ function updateSyncExternalStore<T>(
);
}
if (!includesBlockingLane(root, renderLanes)) {
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
@ -1884,9 +1954,7 @@ function forceStoreRerender(fiber: Fiber) {
}
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
@ -1901,21 +1969,106 @@ function mountState<S>(
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchSetState.bind(null, currentlyRenderingFiber, queue): any));
return hook;
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountStateImpl(initialState);
const queue = hook.queue;
const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
): any);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
return updateReducer(basicStateReducer, initialState);
}
function rerenderState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return rerenderReducer(basicStateReducer, (initialState: any));
return rerenderReducer(basicStateReducer, initialState);
}
function mountOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
const hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = passthrough;
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
// Optimistic state does not use the eager update optimization.
lastRenderedReducer: null,
lastRenderedState: null,
};
hook.queue = queue;
// This is different than the normal setState function.
const dispatch: A => void = (dispatchOptimisticSetState.bind(
null,
currentlyRenderingFiber,
true,
queue,
): any);
queue.dispatch = dispatch;
return [passthrough, dispatch];
}
function updateOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
const hook = updateWorkInProgressHook();
// Optimistic updates are always rebased on top of the latest value passed in
// as an argument. It's called a passthrough because if there are no pending
// updates, it will be returned as-is.
//
// Reset the base state and memoized state to the passthrough. Future
// updates will be applied on top of this.
hook.baseState = hook.memoizedState = passthrough;
// If a reducer is not provided, default to the same one used by useState.
const resolvedReducer: (S, A) => S =
typeof reducer === 'function' ? reducer : (basicStateReducer: any);
return updateReducerImpl(hook, ((currentHook: any): Hook), resolvedReducer);
}
function rerenderOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
// Unlike useState, useOptimistic doesn't support render phase updates.
// Also unlike useState, we need to replay all pending updates again in case
// the passthrough value changed.
//
// So instead of a forked re-render implementation that knows how to handle
// render phase udpates, we can use the same implementation as during a
// regular mount or update.
if (currentHook !== null) {
// This is an update. Process the update queue.
return updateOptimistic(passthrough, reducer);
}
// This is a mount. No updates to process.
const hook = updateWorkInProgressHook();
// Reset the base state and memoized state to the passthrough. Future
// updates will be applied on top of this.
hook.baseState = hook.memoizedState = passthrough;
const dispatch = hook.queue.dispatch;
return [passthrough, dispatch];
}
function pushEffect(
@ -2075,7 +2228,8 @@ function updateEffectImpl(
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
// currentHook is null when rerendering after a render phase state update.
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
@ -2445,9 +2599,10 @@ function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
}
function startTransition<S>(
fiber: Fiber,
queue: UpdateQueue<S | Thenable<S>, BasicStateAction<S | Thenable<S>>>,
pendingState: S,
finishedState: S,
setPending: (Thenable<S> | S) => void,
callback: () => mixed,
options?: StartTransitionOptions,
): void {
@ -2457,8 +2612,20 @@ function startTransition<S>(
);
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
setPending(pendingState);
if (enableAsyncActions) {
// We don't really need to use an optimistic update here, because we
// schedule a second "revert" update below (which we use to suspend the
// transition until the async action scope has finished). But we'll use an
// optimistic update anyway to make it less likely the behavior accidentally
// diverges; for example, both an optimistic update and this one should
// share the same lane.
dispatchOptimisticSetState(fiber, false, queue, pendingState);
} else {
ReactCurrentBatchConfig.transition = null;
dispatchSetState(fiber, queue, pendingState);
}
const currentTransition = (ReactCurrentBatchConfig.transition =
({}: BatchConfigTransition));
@ -2485,10 +2652,10 @@ function startTransition<S>(
returnValue,
finishedState,
);
setPending(maybeThenable);
dispatchSetState(fiber, queue, maybeThenable);
} else {
// Async actions are not enabled.
setPending(finishedState);
dispatchSetState(fiber, queue, finishedState);
callback();
}
} catch (error) {
@ -2501,7 +2668,7 @@ function startTransition<S>(
status: 'rejected',
reason: error,
};
setPending(rejectedThenable);
dispatchSetState(fiber, queue, rejectedThenable);
} else {
// The error rethrowing behavior is only enabled when the async actions
// feature is on, even for sync actions.
@ -2553,7 +2720,10 @@ export function startHostTransition<F>(
);
}
let setPending;
let queue: UpdateQueue<
Thenable<TransitionStatus> | TransitionStatus,
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
>;
if (formFiber.memoizedState === null) {
// Upgrade this host component fiber to be stateful. We're going to pretend
// it was stateful all along so we can reuse most of the implementation
@ -2561,28 +2731,28 @@ export function startHostTransition<F>(
//
// Create the state hook used by TransitionAwareHostComponent. This is
// essentially an inlined version of mountState.
const queue: UpdateQueue<
Thenable<TransitionStatus> | TransitionStatus,
const newQueue: UpdateQueue<
Thenable<TransitionStatus> | TransitionStatus,
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
> = {
pending: null,
lanes: NoLanes,
dispatch: null,
// We're going to cheat and intentionally not create a bound dispatch
// method, because we can call it directly in startTransition.
dispatch: (null: any),
lastRenderedReducer: basicStateReducer,
lastRenderedState: NoPendingHostTransition,
};
queue = newQueue;
const stateHook: Hook = {
memoizedState: NoPendingHostTransition,
baseState: NoPendingHostTransition,
baseQueue: null,
queue: queue,
queue: newQueue,
next: null,
};
const dispatch: (Thenable<TransitionStatus> | TransitionStatus) => void =
(dispatchSetState.bind(null, formFiber, queue): any);
setPending = queue.dispatch = dispatch;
// Add the state hook to both fiber alternates. The idea is that the fiber
// had this hook all along.
formFiber.memoizedState = stateHook;
@ -2593,15 +2763,14 @@ export function startHostTransition<F>(
} else {
// This fiber was already upgraded to be stateful.
const stateHook: Hook = formFiber.memoizedState;
const dispatch: (Thenable<TransitionStatus> | TransitionStatus) => void =
stateHook.queue.dispatch;
setPending = dispatch;
queue = stateHook.queue;
}
startTransition(
formFiber,
queue,
pendingState,
NoPendingHostTransition,
setPending,
// TODO: We can avoid this extra wrapper, somehow. Figure out layering
// once more of this function is implemented.
() => callback(formData),
@ -2612,9 +2781,15 @@ function mountTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const [, setPending] = mountState((false: Thenable<boolean> | boolean));
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
// The `start` method never changes.
const start = startTransition.bind(null, true, false, setPending);
const start = startTransition.bind(
null,
currentlyRenderingFiber,
stateHook.queue,
true,
false,
);
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [false, start];
@ -2785,6 +2960,7 @@ function dispatchReducerAction<S, A>(
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
@ -2823,6 +2999,7 @@ function dispatchSetState<S, A>(
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
@ -2886,6 +3063,58 @@ function dispatchSetState<S, A>(
markUpdateInDevTools(fiber, lane, action);
}
function dispatchOptimisticSetState<S, A>(
fiber: Fiber,
throwIfDuringRender: boolean,
queue: UpdateQueue<S, A>,
action: A,
): void {
const update: Update<S, A> = {
// An optimistic update commits synchronously.
lane: SyncLane,
// After committing, the optimistic update is "reverted" using the same
// lane as the transition it's associated with.
//
// TODO: Warn if there's no transition/action associated with this
// optimistic update.
revertLane: requestTransitionLane(),
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
if (isRenderPhaseUpdate(fiber)) {
// When calling startTransition during render, this warns instead of
// throwing because throwing would be a breaking change. setOptimisticState
// is a new API so it's OK to throw.
if (throwIfDuringRender) {
throw new Error('Cannot update optimistic state while rendering.');
} else {
// startTransition was called during render. We don't need to do anything
// besides warn here because the render phase update would be overidden by
// the second update, anyway. We can remove this branch and make it throw
// in a future release.
if (__DEV__) {
console.error('Cannot call startTransition while rendering.');
}
}
} else {
const root = enqueueConcurrentHookUpdate(fiber, queue, update, SyncLane);
if (root !== null) {
// NOTE: The optimistic update implementation assumes that the transition
// will never be attempted before the optimistic update. This currently
// holds because the optimistic update is always synchronous. If we ever
// change that, we'll need to account for this.
scheduleUpdateOnFiber(root, fiber, SyncLane);
// Optimistic updates are always synchronous, so we don't need to call
// entangleTransitionUpdate here.
}
}
markUpdateInDevTools(fiber, SyncLane, action);
}
function isRenderPhaseUpdate(fiber: Fiber): boolean {
const alternate = fiber.alternate;
return (
@ -2989,6 +3218,9 @@ if (enableFormActions && enableAsyncActions) {
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
throwInvalidHookError;
}
if (enableAsyncActions) {
(ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@ -3024,6 +3256,10 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@ -3058,6 +3294,9 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@ -3093,6 +3332,9 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@ -3283,6 +3525,17 @@ if (__DEV__) {
(HooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnMountInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
mountHookTypesDev();
return mountOptimistic(passthrough, reducer);
};
}
HooksDispatcherOnMountWithHookTypesInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -3441,6 +3694,17 @@ if (__DEV__) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
updateHookTypesDev();
return mountOptimistic(passthrough, reducer);
};
}
HooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -3601,6 +3865,17 @@ if (__DEV__) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
updateHookTypesDev();
return updateOptimistic(passthrough, reducer);
};
}
HooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -3761,6 +4036,17 @@ if (__DEV__) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
updateHookTypesDev();
return rerenderOptimistic(passthrough, reducer);
};
}
InvalidNestedHooksDispatcherOnMountInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -3943,6 +4229,18 @@ if (__DEV__) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
warnInvalidHookAccess();
mountHookTypesDev();
return mountOptimistic(passthrough, reducer);
};
}
InvalidNestedHooksDispatcherOnUpdateInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -4128,6 +4426,18 @@ if (__DEV__) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
warnInvalidHookAccess();
updateHookTypesDev();
return updateOptimistic(passthrough, reducer);
};
}
InvalidNestedHooksDispatcherOnRerenderInDEV = {
readContext<T>(context: ReactContext<T>): T {
@ -4313,4 +4623,16 @@ if (__DEV__) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useOptimistic =
function useOptimistic<S, A>(
passthrough: S,
reducer: ?(S, A) => S,
): [S, (A) => void] {
currentHookNameInDev = 'useOptimistic';
warnInvalidHookAccess();
updateHookTypesDev();
return rerenderOptimistic(passthrough, reducer);
};
}
}

View File

@ -74,9 +74,6 @@ import {
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
resolveSingletonInstance,
shouldSkipHydratableForInstance,
shouldSkipHydratableForTextInstance,
shouldSkipHydratableForSuspenseInstance,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@ -355,6 +352,7 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
nextInstance,
fiber.type,
fiber.pendingProps,
rootOrSingletonContext,
);
if (instance !== null) {
fiber.stateNode = (instance: Instance);
@ -369,7 +367,11 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
function tryHydrateText(fiber: Fiber, nextInstance: any) {
// fiber is a HostText Fiber
const text = fiber.pendingProps;
const textInstance = canHydrateTextInstance(nextInstance, text);
const textInstance = canHydrateTextInstance(
nextInstance,
text,
rootOrSingletonContext,
);
if (textInstance !== null) {
fiber.stateNode = (textInstance: TextInstance);
hydrationParentFiber = fiber;
@ -382,7 +384,10 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
// fiber is a SuspenseComponent Fiber
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
const suspenseInstance = canHydrateSuspenseInstance(
nextInstance,
rootOrSingletonContext,
);
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
@ -441,44 +446,6 @@ function claimHydratableSingleton(fiber: Fiber): void {
}
}
function advanceToFirstAttemptableInstance(fiber: Fiber) {
// fiber is HostComponent Fiber
while (
nextHydratableInstance &&
shouldSkipHydratableForInstance(
nextHydratableInstance,
fiber.type,
fiber.pendingProps,
)
) {
// Flow doesn't understand that inside this block nextHydratableInstance is not null
const instance: HydratableInstance = (nextHydratableInstance: any);
nextHydratableInstance = getNextHydratableSibling(instance);
}
}
function advanceToFirstAttemptableTextInstance() {
while (
nextHydratableInstance &&
shouldSkipHydratableForTextInstance(nextHydratableInstance)
) {
// Flow doesn't understand that inside this block nextHydratableInstance is not null
const instance: HydratableInstance = (nextHydratableInstance: any);
nextHydratableInstance = getNextHydratableSibling(instance);
}
}
function advanceToFirstAttemptableSuspenseInstance() {
while (
nextHydratableInstance &&
shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
) {
// Flow doesn't understand that inside this block nextHydratableInstance is not null
const instance: HydratableInstance = (nextHydratableInstance: any);
nextHydratableInstance = getNextHydratableSibling(instance);
}
}
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
if (!isHydrating) {
return;
@ -493,10 +460,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
}
}
const initialInstance = nextHydratableInstance;
if (rootOrSingletonContext) {
// We may need to skip past certain nodes in these contexts
advanceToFirstAttemptableInstance(fiber);
}
const nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@ -521,10 +484,6 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
// might be flawed or unnecessary.
nextHydratableInstance = getNextHydratableSibling(nextInstance);
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
if (rootOrSingletonContext) {
// We may need to skip past certain nodes in these contexts
advanceToFirstAttemptableInstance(fiber);
}
if (
!nextHydratableInstance ||
!tryHydrateInstance(fiber, nextHydratableInstance)
@ -552,12 +511,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
const isHydratable = isHydratableText(text);
const initialInstance = nextHydratableInstance;
if (rootOrSingletonContext && isHydratable) {
// We may need to skip past certain nodes in these contexts.
// We don't skip if the text is not hydratable because we know no hydratables
// exist which could match this Fiber
advanceToFirstAttemptableTextInstance();
}
const nextInstance = nextHydratableInstance;
if (!nextInstance || !isHydratable) {
// We exclude non hydrabable text because we know there are no matching hydratables.
@ -585,11 +538,6 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
nextHydratableInstance = getNextHydratableSibling(nextInstance);
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
if (rootOrSingletonContext && isHydratable) {
// We may need to skip past certain nodes in these contexts
advanceToFirstAttemptableTextInstance();
}
if (
!nextHydratableInstance ||
!tryHydrateText(fiber, nextHydratableInstance)
@ -614,10 +562,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
return;
}
const initialInstance = nextHydratableInstance;
if (rootOrSingletonContext) {
// We may need to skip past certain nodes in these contexts
advanceToFirstAttemptableSuspenseInstance();
}
const nextInstance = nextHydratableInstance;
if (!nextInstance) {
if (shouldClientRenderOnMismatch(fiber)) {
@ -643,11 +587,6 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
nextHydratableInstance = getNextHydratableSibling(nextInstance);
const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
if (rootOrSingletonContext) {
// We may need to skip past certain nodes in these contexts
advanceToFirstAttemptableSuspenseInstance();
}
if (
!nextHydratableInstance ||
!tryHydrateSuspense(fiber, nextHydratableInstance)
@ -863,7 +802,8 @@ function popHydrationState(fiber: Fiber): boolean {
fiber.tag !== HostSingleton &&
!(
fiber.tag === HostComponent &&
shouldSetTextContent(fiber.type, fiber.memoizedProps)
(!shouldDeleteUnhydratedTailInstances(fiber.type) ||
shouldSetTextContent(fiber.type, fiber.memoizedProps))
)
) {
shouldClear = true;

View File

@ -151,17 +151,29 @@ export function flushSyncWorkOnLegacyRootsOnly() {
flushSyncWorkAcrossRoots_impl(true);
}
export function _doFlushWork(
firstRoot,
workInProgressRoot,
workInProgressRootRenderLanes,
onlyLegacy,
) {
let didPerformSomeWork = false;
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
if (isFlushingWork) {
// Prevent reentrancy.
// TODO: Is this overly defensive? The callers must check the execution
// context first regardless.
return;
}
if (!mightHavePendingSyncWork) {
// Fast path. There's no sync work to do.
return;
}
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
// There may or may not be synchronous work scheduled. Let's check.
let didPerformSomeWork;
let errors: Array<mixed> | null = null;
isFlushingWork = true;
do {
didPerformSomeWork = false;
let root = firstRoot;
let root = firstScheduledRoot;
while (root !== null) {
if (onlyLegacy && root.tag !== LegacyRoot) {
// Skip non-legacy roots.
@ -190,33 +202,6 @@ export function _doFlushWork(
root = root.next;
}
} while (didPerformSomeWork);
return errors;
}
function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
if (isFlushingWork) {
// Prevent reentrancy.
// TODO: Is this overly defensive? The callers must check the execution
// context first regardless.
return;
}
if (!mightHavePendingSyncWork) {
// Fast path. There's no sync work to do.
return;
}
const workInProgressRoot = getWorkInProgressRoot();
const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();
// There may or may not be synchronous work scheduled. Let's check.
isFlushingWork = true;
const errors = _doFlushWork(
firstScheduledRoot,
workInProgressRoot,
workInProgressRootRenderLanes,
onlyLegacy,
);
isFlushingWork = false;
// If any errors were thrown, rethrow them right before exiting.

View File

@ -370,10 +370,12 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
null;
// The most recent time we committed a fallback. This lets us ensure a train
// model where we don't commit new loading states in too quick succession.
// The most recent time we either committed a fallback, or when a fallback was
// filled in with the resolved UI. This lets us throttle the appearance of new
// content as it streams in, to minimize jank.
// TODO: Think of a better name for this variable?
let globalMostRecentFallbackTime: number = 0;
const FALLBACK_THROTTLE_MS: number = 500;
const FALLBACK_THROTTLE_MS: number = 300;
// The absolute time for when we should start giving up on rendering
// more and prefer CPU suspense heuristics instead.

View File

@ -57,7 +57,8 @@ export type HookType =
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useId'
| 'useCacheRefresh';
| 'useCacheRefresh'
| 'useOptimistic';
export type ContextDependency<T> = {
context: ReactContext<T>,
@ -423,6 +424,10 @@ export type Dispatcher = {
useCacheRefresh?: () => <T>(?() => T, ?T) => void,
useMemoCache?: (size: number) => Array<any>,
useHostTransitionStatus?: () => TransitionStatus,
useOptimistic?: <S, A>(
passthrough: S,
reducer: ?(S, A) => S,
) => [S, (A) => void],
};
export type CacheDispatcher = {

View File

@ -5,6 +5,7 @@ let act;
let assertLog;
let useTransition;
let useState;
let useOptimistic;
let textCache;
describe('ReactAsyncActions', () => {
@ -18,6 +19,7 @@ describe('ReactAsyncActions', () => {
assertLog = require('internal-test-utils').assertLog;
useTransition = React.useTransition;
useState = React.useState;
useOptimistic = React.experimental_useOptimistic;
textCache = new Map();
});
@ -644,4 +646,432 @@ describe('ReactAsyncActions', () => {
</>,
);
});
// @gate enableAsyncActions
test('useOptimistic can be used to implement a pending state', async () => {
const startTransition = React.startTransition;
let setIsPending;
function App({text}) {
const [isPending, _setIsPending] = useOptimistic(false);
setIsPending = _setIsPending;
return (
<>
<Text text={'Pending: ' + isPending} />
<AsyncText text={text} />
</>
);
}
// Initial render
const root = ReactNoop.createRoot();
resolveText('A');
await act(() => root.render(<App text="A" />));
assertLog(['Pending: false', 'A']);
expect(root).toMatchRenderedOutput('Pending: falseA');
// Start a transition
await act(() =>
startTransition(() => {
setIsPending(true);
root.render(<App text="B" />);
}),
);
assertLog([
// Render the pending state immediately
'Pending: true',
'A',
// Then attempt to render the transition. The pending state will be
// automatically reverted.
'Pending: false',
'Suspend! [B]',
]);
// Resolve the transition
await act(() => resolveText('B'));
assertLog([
// Render the pending state immediately
'Pending: false',
'B',
]);
});
// @gate enableAsyncActions
test('useOptimistic rebases pending updates on top of passthrough value', async () => {
let serverCart = ['A'];
async function submitNewItem(item) {
await getText('Adding item ' + item);
serverCart = [...serverCart, item];
React.startTransition(() => {
root.render(<App cart={serverCart} />);
});
}
let addItemToCart;
function App({cart}) {
const [isPending, startTransition] = useTransition();
const savedCartSize = cart.length;
const [optimisticCartSize, setOptimisticCartSize] =
useOptimistic(savedCartSize);
addItemToCart = item => {
startTransition(async () => {
setOptimisticCartSize(n => n + 1);
await submitNewItem(item);
});
};
return (
<>
<div>
<Text text={'Pending: ' + isPending} />
</div>
<div>
<Text text={'Items in cart: ' + optimisticCartSize} />
</div>
<ul>
{cart.map(item => (
<li key={item}>
<Text text={'Item ' + item} />
</li>
))}
</ul>
</>
);
}
// Initial render
const root = ReactNoop.createRoot();
await act(() => root.render(<App cart={serverCart} />));
assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: false</div>
<div>Items in cart: 1</div>
<ul>
<li>Item A</li>
</ul>
</>,
);
// The cart size is incremented even though B hasn't been added yet.
await act(() => addItemToCart('B'));
assertLog(['Pending: true', 'Items in cart: 2', 'Item A']);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: true</div>
<div>Items in cart: 2</div>
<ul>
<li>Item A</li>
</ul>
</>,
);
// While B is still pending, another item gets added to the cart
// out-of-band.
serverCart = [...serverCart, 'C'];
// NOTE: This is a synchronous update only because we don't yet support
// parallel transitions; all transitions are entangled together. Once we add
// support for parallel transitions, we can update this test.
ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
assertLog([
'Pending: true',
// Note that the optimistic cart size is still correct, because the
// pending update was rebased on top new value.
'Items in cart: 3',
'Item A',
'Item C',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: true</div>
<div>Items in cart: 3</div>
<ul>
<li>Item A</li>
<li>Item C</li>
</ul>
</>,
);
// Finish loading B. The optimistic state is reverted.
await act(() => resolveText('Adding item B'));
assertLog([
'Pending: false',
'Items in cart: 3',
'Item A',
'Item C',
'Item B',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: false</div>
<div>Items in cart: 3</div>
<ul>
<li>Item A</li>
<li>Item C</li>
<li>Item B</li>
</ul>
</>,
);
});
// @gate enableAsyncActions
test('useOptimistic accepts a custom reducer', async () => {
let serverCart = ['A'];
async function submitNewItem(item) {
await getText('Adding item ' + item);
serverCart = [...serverCart, item];
React.startTransition(() => {
root.render(<App cart={serverCart} />);
});
}
let addItemToCart;
function App({cart}) {
const [isPending, startTransition] = useTransition();
const savedCartSize = cart.length;
const [optimisticCartSize, addToOptimisticCart] = useOptimistic(
savedCartSize,
(prevSize, newItem) => {
Scheduler.log('Increment optimistic cart size for ' + newItem);
return prevSize + 1;
},
);
addItemToCart = item => {
startTransition(async () => {
addToOptimisticCart(item);
await submitNewItem(item);
});
};
return (
<>
<div>
<Text text={'Pending: ' + isPending} />
</div>
<div>
<Text text={'Items in cart: ' + optimisticCartSize} />
</div>
<ul>
{cart.map(item => (
<li key={item}>
<Text text={'Item ' + item} />
</li>
))}
</ul>
</>
);
}
// Initial render
const root = ReactNoop.createRoot();
await act(() => root.render(<App cart={serverCart} />));
assertLog(['Pending: false', 'Items in cart: 1', 'Item A']);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: false</div>
<div>Items in cart: 1</div>
<ul>
<li>Item A</li>
</ul>
</>,
);
// The cart size is incremented even though B hasn't been added yet.
await act(() => addItemToCart('B'));
assertLog([
'Increment optimistic cart size for B',
'Pending: true',
'Items in cart: 2',
'Item A',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: true</div>
<div>Items in cart: 2</div>
<ul>
<li>Item A</li>
</ul>
</>,
);
// While B is still pending, another item gets added to the cart
// out-of-band.
serverCart = [...serverCart, 'C'];
// NOTE: This is a synchronous update only because we don't yet support
// parallel transitions; all transitions are entangled together. Once we add
// support for parallel transitions, we can update this test.
ReactNoop.flushSync(() => root.render(<App cart={serverCart} />));
assertLog([
'Increment optimistic cart size for B',
'Pending: true',
// Note that the optimistic cart size is still correct, because the
// pending update was rebased on top new value.
'Items in cart: 3',
'Item A',
'Item C',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: true</div>
<div>Items in cart: 3</div>
<ul>
<li>Item A</li>
<li>Item C</li>
</ul>
</>,
);
// Finish loading B. The optimistic state is reverted.
await act(() => resolveText('Adding item B'));
assertLog([
'Pending: false',
'Items in cart: 3',
'Item A',
'Item C',
'Item B',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Pending: false</div>
<div>Items in cart: 3</div>
<ul>
<li>Item A</li>
<li>Item C</li>
<li>Item B</li>
</ul>
</>,
);
});
// @gate enableAsyncActions
test('useOptimistic rebases if the passthrough is updated during a render phase update', async () => {
// This is kind of an esoteric case where it's hard to come up with a
// realistic real-world scenario but it should still work.
let increment;
let setCount;
function App() {
const [isPending, startTransition] = useTransition(2);
const [count, _setCount] = useState(0);
setCount = _setCount;
const [optimisticCount, setOptimisticCount] = useOptimistic(
count,
prev => {
Scheduler.log('Increment optimistic count');
return prev + 1;
},
);
if (count === 1) {
Scheduler.log('Render phase update count from 1 to 2');
setCount(2);
}
increment = () =>
startTransition(async () => {
setOptimisticCount(n => n + 1);
await getText('Wait to increment');
React.startTransition(() => setCount(n => n + 1));
});
return (
<>
<div>
<Text text={'Count: ' + count} />
</div>
{isPending ? (
<div>
<Text text={'Optimistic count: ' + optimisticCount} />
</div>
) : null}
</>
);
}
const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog(['Count: 0']);
expect(root).toMatchRenderedOutput(<div>Count: 0</div>);
await act(() => increment());
assertLog([
'Increment optimistic count',
'Count: 0',
'Optimistic count: 1',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Count: 0</div>
<div>Optimistic count: 1</div>
</>,
);
await act(() => setCount(1));
assertLog([
'Increment optimistic count',
'Render phase update count from 1 to 2',
// The optimistic update is rebased on top of the new passthrough value.
'Increment optimistic count',
'Count: 2',
'Optimistic count: 3',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Count: 2</div>
<div>Optimistic count: 3</div>
</>,
);
// Finish the action
await act(() => resolveText('Wait to increment'));
assertLog(['Count: 3']);
expect(root).toMatchRenderedOutput(<div>Count: 3</div>);
});
// @gate enableAsyncActions
test('useOptimistic rebases if the passthrough is updated during a render phase update (initial mount)', async () => {
// This is kind of an esoteric case where it's hard to come up with a
// realistic real-world scenario but it should still work.
function App() {
const [count, setCount] = useState(0);
const [optimisticCount] = useOptimistic(count);
if (count === 0) {
Scheduler.log('Render phase update count from 1 to 2');
setCount(1);
}
return (
<>
<div>
<Text text={'Count: ' + count} />
</div>
<div>
<Text text={'Optimistic count: ' + optimisticCount} />
</div>
</>
);
}
const root = ReactNoop.createRoot();
await act(() => root.render(<App />));
assertLog([
'Render phase update count from 1 to 2',
'Count: 1',
'Optimistic count: 1',
]);
expect(root).toMatchRenderedOutput(
<>
<div>Count: 1</div>
<div>Optimistic count: 1</div>
</>,
);
});
});

View File

@ -1,63 +0,0 @@
let _doFlushWork;
const shimHostConfigPath = 'react-reconciler/src/ReactFiberConfig';
jest.mock(shimHostConfigPath, () => {
return jest.requireActual(
'react-dom-bindings/src/client/ReactFiberConfigDOM.js',
);
});
beforeAll(() => {
_doFlushWork = require('../ReactFiberRootScheduler')._doFlushWork;
});
test('does not hang', () => {
const root = {
tag: 1,
pendingChildren: null,
pingCache: {},
finishedWork: null,
timeoutHandle: -1,
cancelPendingCommit: null,
context: {},
pendingContext: null,
next: null,
callbackNode: null,
callbackPriority: 0,
expirationTimes: [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 278303.90000000596,
278417.1999999881, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1,
],
pendingLanes: 6176,
suspendedLanes: 0,
pingedLanes: 0,
expiredLanes: 0,
mutableReadLanes: 0,
finishedLanes: 0,
errorRecoveryDisabledLanes: 0,
entangledLanes: 6144,
entanglements: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6144, 6144, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
],
hiddenUpdates: [],
identifierPrefix: '',
pooledCache: null,
pooledCacheLanes: 0,
mutableSourceEagerHydrationData: null,
hydrationCallbacks: {
unstable_concurrentUpdatesByDefault: true,
unstable_strictMode: true,
},
incompleteTransitions: {},
effectDuration: 0,
passiveEffectDuration: 0,
memoizedUpdaters: {},
pendingUpdatersLaneMap: [],
_debugRootType: 'hydrateRoot()',
};
expect(() => {
_doFlushWork(root, root, 2, false);
}).not.toThrow();
});

View File

@ -1811,6 +1811,102 @@ describe('ReactSuspenseWithNoopRenderer', () => {
);
});
// @gate enableLegacyCache
it('throttles content from appearing if a fallback was filled in recently', async () => {
function Foo() {
Scheduler.log('Foo');
return (
<>
<Suspense fallback={<Text text="Loading A..." />}>
<AsyncText text="A" />
</Suspense>
<Suspense fallback={<Text text="Loading B..." />}>
<AsyncText text="B" />
</Suspense>
</>
);
}
ReactNoop.render(<Foo />);
// Start rendering
await waitForAll([
'Foo',
'Suspend! [A]',
'Loading A...',
'Suspend! [B]',
'Loading B...',
]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="Loading A..." />
<span prop="Loading B..." />
</>,
);
// Resolve only A. B will still be loading.
await act(async () => {
await resolveText('A');
// If we didn't advance the time here, A would not commit; it would
// be throttled because the fallback would have appeared too recently.
Scheduler.unstable_advanceTime(10000);
jest.advanceTimersByTime(10000);
await waitForPaint(['A']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="Loading B..." />
</>,
);
});
// Advance by a small amount of time. For testing purposes, this is meant
// to be just under the throttling interval. It's a heurstic, though, so
// if we adjust the heuristic we might have to update this test, too.
Scheduler.unstable_advanceTime(200);
jest.advanceTimersByTime(200);
// Now resolve B.
await act(async () => {
await resolveText('B');
await waitForPaint(['B']);
if (gate(flags => flags.alwaysThrottleRetries)) {
// B should not commit yet. Even though it's been a long time since its
// fallback was shown, it hasn't been long since A appeared. So B's
// appearance is throttled to reduce jank.
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="Loading B..." />
</>,
);
// Advance time a little bit more. Now it commits because enough time
// has passed.
Scheduler.unstable_advanceTime(100);
jest.advanceTimersByTime(100);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="B" />
</>,
);
} else {
// Old behavior, gated until this rolls out at Meta:
//
// B appears immediately, without being throttled.
expect(ReactNoop).toMatchRenderedOutput(
<>
<span prop="A" />
<span prop="B" />
</>,
);
}
});
});
// TODO: flip to "warns" when this is implemented again.
// @gate enableLegacyCache
it('does not warn when a low priority update suspends inside a high priority update for functional components', async () => {

View File

@ -149,12 +149,6 @@ export const getFirstHydratableChildWithinContainer =
$$$config.getFirstHydratableChildWithinContainer;
export const getFirstHydratableChildWithinSuspenseInstance =
$$$config.getFirstHydratableChildWithinSuspenseInstance;
export const shouldSkipHydratableForInstance =
$$$config.shouldSkipHydratableForInstance;
export const shouldSkipHydratableForTextInstance =
$$$config.shouldSkipHydratableForTextInstance;
export const shouldSkipHydratableForSuspenseInstance =
$$$config.shouldSkipHydratableForSuspenseInstance;
export const canHydrateInstance = $$$config.canHydrateInstance;
export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from 'react-native-renderer/src/ReactFiberConfigFabric';

View File

@ -657,7 +657,6 @@ export function createSignatureFunctionForTransform(): <T>(
// in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))).
if (!savedType) {
// We're in the innermost call, so this is the actual type.
// $FlowFixMe[escaped-generic] discovered when updating Flow
savedType = type;
hasCustomHooks = typeof getCustomHooks === 'function';
}

View File

@ -1,16 +1,17 @@
{
"name": "react-server-native-relay",
"name": "react-server-dom-fb",
"version": "0.1.0",
"private": true,
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-server-native-relay"
"directory": "packages/react-server-dom-fb"
},
"dependencies": {
"scheduler": "^0.11.0"
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^17.0.0"
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}

View File

@ -1,17 +0,0 @@
{
"name": "react-server-dom-relay",
"version": "0.1.0",
"private": true,
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"directory": "packages/react-server-dom-relay"
},
"dependencies": {
"scheduler": "^0.11.0"
},
"peerDependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0"
}
}

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export * from './src/ReactFlightDOMRelayServer';

View File

@ -1,97 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {JSONValue, ResponseBase} from 'react-client/src/ReactFlightClient';
import type {JSResourceReference} from 'JSResourceReference';
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
export type ClientReference<T> = JSResourceReference<T>;
import {
parseModelString,
parseModelTuple,
} from 'react-client/src/ReactFlightClient';
export {
preloadModule,
requireModule,
} from 'ReactFlightDOMRelayClientIntegration';
import {resolveClientReference as resolveClientReferenceImpl} from 'ReactFlightDOMRelayClientIntegration';
import isArray from 'shared/isArray';
export type {ClientReferenceMetadata} from 'ReactFlightDOMRelayClientIntegration';
export type SSRManifest = null;
export type ServerManifest = null;
export type ServerReferenceId = string;
export type UninitializedModel = JSONValue;
export type Response = ResponseBase;
export function resolveClientReference<T>(
bundlerConfig: SSRManifest,
metadata: ClientReferenceMetadata,
): ClientReference<T> {
return resolveClientReferenceImpl(metadata);
}
export function resolveServerReference<T>(
bundlerConfig: ServerManifest,
id: ServerReferenceId,
): ClientReference<T> {
throw new Error('Not implemented.');
}
function parseModelRecursively(
response: Response,
parentObj: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
key: string,
value: JSONValue,
): $FlowFixMe {
if (typeof value === 'string') {
return parseModelString(response, parentObj, key, value);
}
if (typeof value === 'object' && value !== null) {
if (isArray(value)) {
const parsedValue: Array<$FlowFixMe> = [];
for (let i = 0; i < value.length; i++) {
(parsedValue: any)[i] = parseModelRecursively(
response,
value,
'' + i,
value[i],
);
}
return parseModelTuple(response, parsedValue);
} else {
const parsedValue = {};
for (const innerKey in value) {
(parsedValue: any)[innerKey] = parseModelRecursively(
response,
value,
innerKey,
value[innerKey],
);
}
return parsedValue;
}
}
return value;
}
const dummy = {};
export function parseModel<T>(response: Response, json: UninitializedModel): T {
return (parseModelRecursively(response, dummy, '', json): any);
}

View File

@ -1,61 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {RowEncoding} from './ReactFlightDOMRelayProtocol';
import type {Response} from 'react-client/src/ReactFlightClient';
import {
createResponse,
resolveModel,
resolveModule,
resolveErrorDev,
resolveErrorProd,
resolveHint,
close,
getRoot,
} from 'react-client/src/ReactFlightClient';
export {createResponse, close, getRoot};
export function resolveRow(response: Response, chunk: RowEncoding): void {
if (chunk[0] === 'O') {
// $FlowFixMe[incompatible-call] unable to refine on array indices
resolveModel(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'I') {
// $FlowFixMe[incompatible-call] unable to refine on array indices
resolveModule(response, chunk[1], chunk[2]);
} else if (chunk[0] === 'H') {
// $FlowFixMe[incompatible-call] unable to refine on array indices
resolveHint(response, chunk[1], chunk[2]);
} else {
if (__DEV__) {
resolveErrorDev(
response,
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
chunk[1],
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-use]
chunk[2].digest,
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
// $FlowFixMe[incompatible-use]
chunk[2].message || '',
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
// $FlowFixMe[incompatible-use]
chunk[2].stack || '',
);
} else {
// $FlowFixMe[incompatible-call]: Flow doesn't support disjoint unions on tuples.
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-use]
resolveErrorProd(response, chunk[1], chunk[2].digest);
}
}
}

View File

@ -1,36 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
import type {ClientReferenceMetadata} from 'ReactFlightDOMRelayServerIntegration';
export type JSONValue =
| string
| number
| boolean
| null
| {+[key: string]: JSONValue}
| $ReadOnlyArray<JSONValue>;
export type RowEncoding =
| ['O', number, JSONValue]
| ['I', number, ClientReferenceMetadata]
| ['H', string, HintModel]
| ['P', number, string]
| ['S', number, string]
| [
'E',
number,
{
digest: string,
message?: string,
stack?: string,
...
},
];

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactClientValue} from 'react-server/src/ReactFlightServer';
import type {
ClientManifest,
Destination,
} from './ReactFlightServerConfigDOMRelay';
import {
createRequest,
startWork,
startFlowing,
} from 'react-server/src/ReactFlightServer';
type Options = {
onError?: (error: mixed) => void,
identifierPrefix?: string,
};
function render(
model: ReactClientValue,
destination: Destination,
config: ClientManifest,
options?: Options,
): void {
const request = createRequest(
model,
config,
options ? options.onError : undefined,
undefined, // not currently set up to supply context overrides
options ? options.identifierPrefix : undefined,
);
startWork(request);
startFlowing(request, destination);
}
export {render};

View File

@ -1,236 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
import type {RowEncoding, JSONValue} from './ReactFlightDOMRelayProtocol';
import type {
Request,
ReactClientValue,
} from 'react-server/src/ReactFlightServer';
import type {JSResourceReference} from 'JSResourceReference';
import JSResourceReferenceImpl from 'JSResourceReferenceImpl';
import hasOwnProperty from 'shared/hasOwnProperty';
import isArray from 'shared/isArray';
export type ClientReference<T> = JSResourceReference<T>;
export type ServerReference<T> = T;
export type ServerReferenceId = {};
import type {
Destination,
BundlerConfig as ClientManifest,
ClientReferenceMetadata,
} from 'ReactFlightDOMRelayServerIntegration';
import {resolveModelToJSON} from 'react-server/src/ReactFlightServer';
import {
emitRow,
resolveClientReferenceMetadata as resolveClientReferenceMetadataImpl,
close,
} from 'ReactFlightDOMRelayServerIntegration';
export type {
Destination,
BundlerConfig as ClientManifest,
ClientReferenceMetadata,
} from 'ReactFlightDOMRelayServerIntegration';
export function isClientReference(reference: Object): boolean {
return reference instanceof JSResourceReferenceImpl;
}
export function isServerReference(reference: Object): boolean {
return false;
}
export type ClientReferenceKey = ClientReference<any>;
export function getClientReferenceKey(
reference: ClientReference<any>,
): ClientReferenceKey {
// We use the reference object itself as the key because we assume the
// object will be cached by the bundler runtime.
return reference;
}
export function resolveClientReferenceMetadata<T>(
config: ClientManifest,
resource: ClientReference<T>,
): ClientReferenceMetadata {
return resolveClientReferenceMetadataImpl(config, resource);
}
export function getServerReferenceId<T>(
config: ClientManifest,
resource: ServerReference<T>,
): ServerReferenceId {
throw new Error('Not implemented.');
}
export function getServerReferenceBoundArguments<T>(
config: ClientManifest,
resource: ServerReference<T>,
): Array<ReactClientValue> {
throw new Error('Not implemented.');
}
export type Chunk = RowEncoding;
export function processErrorChunkProd(
request: Request,
id: number,
digest: string,
): Chunk {
if (__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'processErrorChunkProd should never be called while in development mode. Use processErrorChunkDev instead. This is a bug in React.',
);
}
return [
'E',
id,
{
digest,
},
];
}
export function processErrorChunkDev(
request: Request,
id: number,
digest: string,
message: string,
stack: string,
): Chunk {
if (!__DEV__) {
// These errors should never make it into a build so we don't need to encode them in codes.json
// eslint-disable-next-line react-internal/prod-error-codes
throw new Error(
'processErrorChunkDev should never be called while in production mode. Use processErrorChunkProd instead. This is a bug in React.',
);
}
return [
'E',
id,
{
digest,
message,
stack,
},
];
}
function convertModelToJSON(
request: Request,
parent: {+[key: string]: ReactClientValue} | $ReadOnlyArray<ReactClientValue>,
key: string,
model: ReactClientValue,
): JSONValue {
const json = resolveModelToJSON(request, parent, key, model);
if (typeof json === 'object' && json !== null) {
if (isArray(json)) {
const jsonArray: Array<JSONValue> = [];
for (let i = 0; i < json.length; i++) {
jsonArray[i] = convertModelToJSON(request, json, '' + i, json[i]);
}
return jsonArray;
} else {
const jsonObj: {[key: string]: JSONValue} = {};
for (const nextKey in json) {
if (hasOwnProperty.call(json, nextKey)) {
jsonObj[nextKey] = convertModelToJSON(
request,
json,
nextKey,
json[nextKey],
);
}
}
return jsonObj;
}
}
return json;
}
export function processModelChunk(
request: Request,
id: number,
model: ReactClientValue,
): Chunk {
const json = convertModelToJSON(request, {}, '', model);
return ['O', id, json];
}
export function processReferenceChunk(
request: Request,
id: number,
reference: string,
): Chunk {
return ['O', id, reference];
}
export function processImportChunk(
request: Request,
id: number,
clientReferenceMetadata: ClientReferenceMetadata,
): Chunk {
// The clientReferenceMetadata is already a JSON serializable value.
return ['I', id, clientReferenceMetadata];
}
export function processHintChunk(
request: Request,
id: number,
code: string,
model: HintModel,
): Chunk {
// The hint is already a JSON serializable value.
return ['H', code, model];
}
export function scheduleWork(callback: () => void) {
callback();
}
export function flushBuffered(destination: Destination) {}
export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<Request> = (null: any);
export function beginWriting(destination: Destination) {}
export function writeChunk(destination: Destination, chunk: Chunk): void {
// $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
emitRow(destination, chunk);
}
export function writeChunkAndReturn(
destination: Destination,
chunk: Chunk,
): boolean {
// $FlowFixMe[incompatible-call] `Chunk` doesn't flow into `JSONValue` because of the `E` row type.
emitRow(destination, chunk);
return true;
}
export function completeWriting(destination: Destination) {}
export {close};
export function closeWithError(destination: Destination, error: mixed): void {
close(destination);
}

Some files were not shown because too many files have changed in this diff Show More