Compare commits

..

1 Commits

Author SHA1 Message Date
Rick Hanlon ccdb07eea5 Failing unit test for infinite render issue 2023-04-29 13:24:07 -04:00
179 changed files with 3465 additions and 3157 deletions

View File

@ -426,6 +426,7 @@ jobs:
scripts/release/publish.js --ci --tags << parameters.dist_tag >>
workflows:
version: 2
build_and_test:
unless: << pipeline.parameters.prerelease_commit_sha >>
@ -604,20 +605,10 @@ workflows:
when: << pipeline.parameters.prerelease_commit_sha >>
jobs:
- publish_prerelease:
name: Publish to Canary channel
name: Publish to Next channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: stable
# 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"
dist_tag: "next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -625,7 +616,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 Canary channel
- Publish to Next channel
commit_sha: << pipeline.parameters.prerelease_commit_sha >>
release_channel: experimental
dist_tag: experimental
@ -643,10 +634,10 @@ workflows:
- main
jobs:
- publish_prerelease:
name: Publish to Canary channel
name: Publish to Next channel
commit_sha: << pipeline.git.revision >>
release_channel: stable
dist_tag: "canary,next"
dist_tag: "next"
- publish_prerelease:
name: Publish to Experimental channel
requires:
@ -654,7 +645,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 Canary channel
- Publish to Next channel
commit_sha: << pipeline.git.revision >>
release_channel: experimental
dist_tag: experimental

View File

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

View File

@ -228,16 +228,7 @@ 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.3.0
// 18.0.0
//
// The @canary channel appends additional information, with the scheme
// The @next channel appends additional information, with the scheme
// <version>-<label>-<commit_sha>, e.g.:
//
// 18.3.0-canary-a1c2d3e4
// 18.0.0-alpha-a1c2d3e4
//
// The @experimental channel doesn't include a version, only a date and a sha, e.g.:
//
@ -20,13 +20,9 @@
const ReactVersion = '18.3.0';
// 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';
// The label used by the @next channel. Represents the upcoming release's
// stability. Could be "alpha", "beta", "rc", etc.
const nextChannelLabel = 'next';
const stablePackages = {
'eslint-plugin-react-hooks': '5.0.0',
@ -44,14 +40,14 @@ const stablePackages = {
scheduler: '0.24.0',
};
// These packages do not exist in the @canary or @latest channel, only
// These packages do not exist in the @next 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,
canaryChannelLabel,
nextChannelLabel,
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@canary/umd/react-cache.development.js"></script>
<script src="https://unpkg.com/react-cache@next/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@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>
<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>
<!-- 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,8 +95,6 @@ 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,7 +36,6 @@ const bodyParser = require('body-parser');
const busboy = require('busboy');
const app = express();
const compress = require('compression');
const {Readable} = require('node:stream');
app.use(compress());
@ -46,7 +45,7 @@ const {readFile} = require('fs').promises;
const React = require('react');
async function renderApp(res, returnValue) {
app.get('/', async function (req, res) {
const {renderToPipeableStream} = await import(
'react-server-dom-webpack/server'
);
@ -92,74 +91,37 @@ async function renderApp(res, returnValue) {
),
React.createElement(App),
];
// 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);
const {pipe} = renderToPipeableStream(root, 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,
decodeAction,
} = await import('react-server-dom-webpack/server');
const {renderToPipeableStream, decodeReply, decodeReplyFromBusboy} =
await import('react-server-dom-webpack/server');
const serverReference = req.get('rsc-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;
} 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 {
// 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 [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;
} else {
args = await decodeReply(req.body);
}
const result = action.apply(null, args);
const {pipe} = renderToPipeableStream(result, {});
pipe(res);
});
app.get('/todos', function (req, res) {

View File

@ -11,8 +11,6 @@ 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();
@ -25,7 +23,7 @@ export default async function App() {
</head>
<body>
<Container>
<h1>{getServerState()}</h1>
<h1>Hello, world</h1>
<Counter />
<Counter2 />
<ul>

View File

@ -1,25 +1,29 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import {flushSync} from 'react-dom';
import ErrorBoundary from './ErrorBoundary.js';
function ButtonDisabledWhilePending({action, children}) {
const {pending} = useFormStatus();
return (
<button disabled={pending} formAction={action}>
{children}
</button>
);
}
export default function Button({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
<ErrorBoundary>
<form>
<ButtonDisabledWhilePending action={action}>
<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));
}
}}>
{children}
</ButtonDisabledWhilePending>
</button>
</form>
</ErrorBoundary>
);

View File

@ -1,20 +1,25 @@
'use client';
import * as React from 'react';
import {experimental_useFormStatus as useFormStatus} from 'react-dom';
import {flushSync} 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={action}>
<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));
}
}}>
<label>
Name: <input name="name" />
</label>
@ -22,7 +27,7 @@ export default function Form({action, children}) {
File: <input type="file" name="file" />
</label>
<button>Say Hi</button>
<Status />
{isPending ? 'Saving...' : null}
</form>
</ErrorBoundary>
);

View File

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

View File

@ -1,15 +1,11 @@
'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,29 +1,11 @@
import * as React from 'react';
import {use, Suspense, useState, startTransition} from 'react';
import {use, Suspense} 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: {
@ -31,14 +13,22 @@ let data = createFromFetch(
},
}),
{
callServer,
async callServer(id, args) {
const response = fetch('/', {
method: 'POST',
headers: {
Accept: 'text/x-component',
'rsc-action': id,
},
body: await encodeReply(args),
});
return createFromFetch(response);
},
}
);
function Shell({data}) {
const [root, setRoot] = useState(use(data));
updateRoot = setRoot;
return root;
return use(data);
}
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.205.1",
"flow-remove-types": "^2.205.1",
"flow-bin": "^0.202.0",
"flow-remove-types": "^2.202.0",
"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/ReactFlightClient';
export * from './src/ReactFlightClientStream';

View File

@ -13,37 +13,28 @@ 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 {
encodeFormAction,
knownServerReferences,
} from './ReactFlightReplyClient';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
import {REACT_LAZY_TYPE, REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import {getOrCreateServerContext} from 'shared/ReactServerContextRegistry';
export type {CallServerCallback};
type UninitializedModel = string;
export type CallServerCallback = <A, T>(id: any, args: A) => Promise<T>;
export type JSONValue =
| number
@ -162,15 +153,15 @@ Chunk.prototype.then = function <T>(
}
};
export type Response = {
export type ResponseBase = {
_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.
@ -509,14 +500,11 @@ 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;
}
function parseModelString(
export function parseModelString(
response: Response,
parentObject: Object,
key: string,
@ -636,7 +624,7 @@ function parseModelString(
return value;
}
function parseModelTuple(
export function parseModelTuple(
response: Response,
value: {+[key: string]: JSONValue} | $ReadOnlyArray<JSONValue>,
): any {
@ -660,25 +648,17 @@ function missingCall() {
export function createResponse(
bundlerConfig: SSRManifest,
callServer: void | CallServerCallback,
): Response {
): ResponseBase {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
const 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;
}
function resolveModel(
export function resolveModel(
response: Response,
id: number,
model: UninitializedModel,
@ -692,7 +672,7 @@ function resolveModel(
}
}
function resolveModule(
export function resolveModule(
response: Response,
id: number,
model: UninitializedModel,
@ -741,7 +721,7 @@ function resolveModule(
}
type ErrorWithDigest = Error & {digest?: string};
function resolveErrorProd(
export function resolveErrorProd(
response: Response,
id: number,
digest: string,
@ -770,7 +750,7 @@ function resolveErrorProd(
}
}
function resolveErrorDev(
export function resolveErrorDev(
response: Response,
id: number,
digest: string,
@ -801,7 +781,7 @@ function resolveErrorDev(
}
}
function resolveHint(
export function resolveHint(
response: Response,
code: string,
model: UninitializedModel,
@ -810,105 +790,6 @@ 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

@ -0,0 +1,33 @@
/**
* 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

@ -0,0 +1,23 @@
/**
* 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

@ -0,0 +1,146 @@
/**
* 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,7 +7,12 @@
* @flow
*/
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes';
import type {Thenable} from 'shared/ReactTypes';
import {
knownServerReferences,
createServerReference,
} from './ReactFlightServerReferenceRegistry';
import {
REACT_ELEMENT_TYPE,
@ -23,10 +28,6 @@ import {
} from 'shared/ReactSerializationErrors';
import isArray from 'shared/isArray';
import type {
FulfilledThenable,
RejectedThenable,
} from '../../shared/ReactTypes';
type ReactJSONValue =
| string
@ -38,15 +39,6 @@ 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
@ -291,6 +283,7 @@ 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);
}
@ -370,104 +363,4 @@ export function processReply(
}
}
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;
}
export {createServerReference};

View File

@ -0,0 +1,32 @@
/**
* 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,6 +25,7 @@
declare var $$$config: any;
export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
@ -38,6 +39,9 @@ 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,5 +8,6 @@
*/
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,7 @@
*/
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,5 +8,6 @@
*/
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,5 +8,6 @@
*/
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,5 +8,6 @@
*/
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,5 +8,6 @@
*/
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

@ -0,0 +1,12 @@
/**
* 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

@ -0,0 +1,11 @@
/**
* 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.8",
"version": "4.27.6",
"description": "Use react-devtools outside of the browser",
"license": "MIT",
"main": "./dist/backend.js",

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) Meta Platforms, Inc. and its affiliates.
* Copyright (c) Facebook, 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,6 +167,7 @@ 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.8",
"version_name": "4.27.8",
"version": "4.27.6",
"version_name": "4.27.6",
"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.8",
"version_name": "4.27.8",
"version": "4.27.6",
"version_name": "4.27.6",
"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.8",
"version": "4.27.6",
"applications": {
"gecko": {
"id": "@react-devtools",

View File

@ -11,7 +11,7 @@
/* eslint-disable no-unused-vars */
type JestMockFn<TArguments: $ReadOnlyArray<any>, TReturn> = {
type JestMockFn<TArguments: $ReadOnlyArray<*>, 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<any>): void;
toBeInstanceOf(cls: Class<*>): 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<any>, TReturn>(
fn<TArguments: $ReadOnlyArray<*>, TReturn>(
implementation?: (...args: TArguments) => TReturn
): JestMockFn<TArguments, TReturn>,
/**

View File

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

View File

@ -16,6 +16,7 @@ import {COMPACT_VERSION_NAME} from './utils';
let welcomeHasInitialized = false;
// $FlowFixMe[missing-local-annot]
function welcome(event: $FlowFixMe) {
if (
event.source !== window ||
@ -58,20 +59,13 @@ function setup(hook: ?DevToolsHook) {
// register renderers that have already injected themselves.
hook.renderers.forEach(renderer => {
registerRenderer(renderer, hook);
registerRenderer(renderer);
});
// 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, hook);
registerRenderer(renderer);
updateRequiredBackends();
});
@ -84,16 +78,12 @@ function setup(hook: ?DevToolsHook) {
const requiredBackends = new Set<string>();
function registerRenderer(renderer: ReactRenderer, hook: DevToolsHook) {
function registerRenderer(renderer: ReactRenderer) {
let version = renderer.reconcilerVersion || renderer.version;
if (!hasAssignedBackend(version)) {
version = COMPACT_VERSION_NAME;
}
// Check if required backend is already activated, no need to require again
if (!hook.backends.has(version)) {
requiredBackends.add(version);
}
requiredBackends.add(version);
}
function activateBackend(version: string, hook: DevToolsHook) {
@ -101,7 +91,6 @@ 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) {
@ -162,10 +151,6 @@ 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,48 +6,38 @@ import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from './utils';
const ports = {};
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();
// 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);
}
},
);
}
chrome.runtime.onConnect.addListener(function (port) {

View File

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

View File

@ -1243,9 +1243,10 @@ describe('Timeline profiler', () => {
function Example() {
const setHigh = React.useState(0)[1];
const setLow = React.useState(0)[1];
const startTransition = React.useTransition()[1];
updaterFn = () => {
React.startTransition(() => {
startTransition(() => {
setLow(prevLow => prevLow + 1);
});
setHigh(prevHigh => prevHigh + 1);
@ -1264,6 +1265,24 @@ 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 compatible with all older React versions
// this is the backend that is compactible with all older React versions
function isMatchingRender(version: string): boolean {
return !hasAssignedBackend(version);
}
@ -31,7 +31,6 @@ export function initBackend(
// DevTools didn't get injected into this page (maybe b'c of the contentType).
return () => {};
}
const subs = [
hook.sub(
'renderer-attached',
@ -65,6 +64,10 @@ 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;
@ -99,6 +102,7 @@ export function initBackend(
} else {
hook.emit('unsupported-renderer-version', id);
}
renderer.attached = true;
};
// Connect renderers that have already injected themselves.

View File

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

View File

@ -54,10 +54,7 @@ let supportsUserTiming =
let supportsUserTimingV3 = false;
if (supportsUserTiming) {
const CHECK_V3_MARK = '__v3';
const markOptions: {
detail?: mixed,
startTime?: number,
} = {};
const markOptions = ({}: {startTime?: number});
Object.defineProperty(markOptions, 'startTime', {
get: function () {
supportsUserTimingV3 = true;
@ -67,6 +64,7 @@ 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,6 +171,8 @@ 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 configuration option when constructing the Store.
// These options may be initially set by a confiugraiton 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.has(id);
return this._idToElement.get(id) != null;
}
getElementAtIndex(index: number): Element | null {
@ -539,13 +539,13 @@ export default class Store extends EventEmitter<{
}
getElementIDAtIndex(index: number): number | null {
const element = this.getElementAtIndex(index);
const element: Element | null = this.getElementAtIndex(index);
return element === null ? null : element.id;
}
getElementByID(id: number): Element | null {
const element = this._idToElement.get(id);
if (element === undefined) {
if (element == null) {
console.warn(`No element found with id "${id}"`);
return null;
}
@ -607,10 +607,7 @@ export default class Store extends EventEmitter<{
let currentID = element.parentID;
let index = 0;
while (true) {
const current = this._idToElement.get(currentID);
if (current === undefined) {
return null;
}
const current = ((this._idToElement.get(currentID): any): Element);
const {children} = current;
for (let i = 0; i < children.length; i++) {
@ -618,12 +615,7 @@ export default class Store extends EventEmitter<{
if (childID === previousID) {
break;
}
const child = this._idToElement.get(childID);
if (child === undefined) {
return null;
}
const child = ((this._idToElement.get(childID): any): Element);
index += child.isCollapsed ? 1 : child.weight;
}
@ -645,12 +637,7 @@ export default class Store extends EventEmitter<{
if (rootID === currentID) {
break;
}
const root = this._idToElement.get(rootID);
if (root === undefined) {
return null;
}
const root = ((this._idToElement.get(rootID): any): Element);
index += root.weight;
}
@ -660,7 +647,7 @@ export default class Store extends EventEmitter<{
getOwnersListForElement(ownerID: number): Array<Element> {
const list: Array<Element> = [];
const element = this._idToElement.get(ownerID);
if (element !== undefined) {
if (element != null) {
list.push({
...element,
depth: 0,
@ -678,8 +665,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) || 0) -
(this.getIndexOfElementID(idB) || 0),
((this.getIndexOfElementID(idA): any): number) -
((this.getIndexOfElementID(idB): any): number),
);
// Next we need to determine the appropriate depth for each element in the list.
@ -690,7 +677,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 !== undefined) {
if (innerElement != null) {
let parentID = innerElement.parentID;
let depth = 0;
@ -702,7 +689,7 @@ export default class Store extends EventEmitter<{
break;
}
const parent = this._idToElement.get(parentID);
if (parent === undefined) {
if (parent == null) {
break;
}
parentID = parent.parentID;
@ -723,7 +710,7 @@ export default class Store extends EventEmitter<{
getRendererIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current !== undefined) {
while (current != null) {
if (current.parentID === 0) {
const rendererID = this._rootIDToRendererID.get(current.id);
return rendererID == null ? null : rendererID;
@ -736,7 +723,7 @@ export default class Store extends EventEmitter<{
getRootIDForElement(id: number): number | null {
let current = this._idToElement.get(id);
while (current !== undefined) {
while (current != null) {
if (current.parentID === 0) {
return current.id;
} else {
@ -778,8 +765,10 @@ export default class Store extends EventEmitter<{
const weightDelta = 1 - element.weight;
let parentElement = this._idToElement.get(element.parentID);
while (parentElement !== undefined) {
let parentElement: void | Element = ((this._idToElement.get(
element.parentID,
): any): Element);
while (parentElement != null) {
// 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;
@ -787,7 +776,7 @@ export default class Store extends EventEmitter<{
}
}
} else {
let currentElement: ?Element = element;
let currentElement = element;
while (currentElement != null) {
const oldWeight = currentElement.isCollapsed
? 1
@ -802,8 +791,10 @@ export default class Store extends EventEmitter<{
: currentElement.weight;
const weightDelta = newWeight - oldWeight;
let parentElement = this._idToElement.get(currentElement.parentID);
while (parentElement !== undefined) {
let parentElement: void | Element = ((this._idToElement.get(
currentElement.parentID,
): any): Element);
while (parentElement != null) {
parentElement.weight += weightDelta;
if (parentElement.isCollapsed) {
// It's important to break on a collapsed parent when expanding nodes.
@ -817,8 +808,10 @@ export default class Store extends EventEmitter<{
currentElement =
currentElement.parentID !== 0
? this.getElementByID(currentElement.parentID)
: null;
? // $FlowFixMe[incompatible-type] found when upgrading Flow
this.getElementByID(currentElement.parentID)
: // $FlowFixMe[incompatible-type] found when upgrading Flow
null;
}
}
@ -840,7 +833,7 @@ export default class Store extends EventEmitter<{
}
_adjustParentTreeWeight: (
parentElement: ?Element,
parentElement: Element | null,
weightDelta: number,
) => void = (parentElement, weightDelta) => {
let isInsideCollapsedSubTree = false;
@ -855,7 +848,9 @@ export default class Store extends EventEmitter<{
break;
}
parentElement = this._idToElement.get(parentElement.parentID);
parentElement = ((this._idToElement.get(
parentElement.parentID,
): any): Element);
}
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
@ -911,16 +906,13 @@ export default class Store extends EventEmitter<{
const stringTable: Array<string | null> = [
null, // ID = 0 corresponds to the null string.
];
const stringTableSize = operations[i];
i++;
const stringTableSize = operations[i++];
const stringTableEnd = i + stringTableSize;
while (i < stringTableEnd) {
const nextLength = operations[i];
i++;
const nextString = utfDecodeString(operations.slice(i, i + nextLength));
const nextLength = operations[i++];
const nextString = utfDecodeString(
(operations.slice(i, i + nextLength): any),
);
stringTable.push(nextString);
i += nextLength;
}
@ -929,7 +921,7 @@ export default class Store extends EventEmitter<{
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD: {
const id = operations[i + 1];
const id = ((operations[i + 1]: any): number);
const type = ((operations[i + 2]: any): ElementType);
i += 3;
@ -942,6 +934,8 @@ 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}`);
@ -1003,10 +997,10 @@ export default class Store extends EventEmitter<{
haveRootsChanged = true;
} else {
const parentID = operations[i];
parentID = ((operations[i]: any): number);
i++;
const ownerID = operations[i];
ownerID = ((operations[i]: any): number);
i++;
const displayNameStringID = operations[i];
@ -1024,17 +1018,17 @@ export default class Store extends EventEmitter<{
);
}
const parentElement = this._idToElement.get(parentID);
if (parentElement === undefined) {
if (!this._idToElement.has(parentID)) {
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] =
@ -1071,25 +1065,23 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REMOVE: {
const removeLength = operations[i + 1];
const removeLength = ((operations[i + 1]: any): number);
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = operations[i];
const element = this._idToElement.get(id);
const id = ((operations[i]: any): number);
if (element === undefined) {
if (!this._idToElement.has(id)) {
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(
@ -1099,7 +1091,7 @@ export default class Store extends EventEmitter<{
this._idToElement.delete(id);
let parentElement: ?Element = null;
let parentElement = null;
if (parentID === 0) {
if (__DEBUG__) {
debug('Remove', `node ${id} root`);
@ -1114,18 +1106,14 @@ export default class Store extends EventEmitter<{
if (__DEBUG__) {
debug('Remove', `node ${id} from parent ${parentID}`);
}
parentElement = this._idToElement.get(parentID);
parentElement = ((this._idToElement.get(parentID): any): Element);
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);
}
@ -1179,21 +1167,19 @@ export default class Store extends EventEmitter<{
break;
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = operations[i + 1];
const numChildren = operations[i + 2];
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
i += 3;
const element = this._idToElement.get(id);
if (element === undefined) {
if (!this._idToElement.has(id)) {
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://react.dev/reference/react/StrictMode"
href="https://fb.me/devtools-strict-mode"
rel="noopener noreferrer"
target="_blank"
title="This component is not running in StrictMode. Click to learn more.">

View File

@ -45,6 +45,7 @@ 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) Meta Platforms, Inc. and its affiliates.
* Copyright (c) Facebook, 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) Meta Platforms, Inc. and its affiliates.
* Copyright (c) Facebook, 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.8",
"version": "4.27.6",
"license": "MIT",
"dependencies": {
"@elg/speedscope": "1.9.0-a6f84db",

View File

@ -4,23 +4,6 @@
---
### 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.8",
"version": "4.27.6",
"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.8",
"react-devtools-core": "4.27.6",
"update-notifier": "^2.1.0"
}
}

View File

@ -2791,6 +2791,7 @@ 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.
@ -2805,14 +2806,13 @@ function diffHydratedGenericElement(
extraAttributes.delete('method');
extraAttributes.delete('target');
}
// 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.
if (hasFormActionURL) {
// Expected
continue;
}
warnForPropDifference(propKey, serverValue, value);
continue;
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
} else if (hasFormActionURL) {
extraAttributes.delete(propKey.toLowerCase());
warnForPropDifference(propKey, 'function', value);
continue;

View File

@ -89,7 +89,6 @@ import {
enableHostSingletons,
enableTrustedTypesIntegration,
diffInCommitPhase,
enableFormActions,
} from 'shared/ReactFeatureFlags';
import {
HostComponent,
@ -1039,164 +1038,150 @@ 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 {
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;
if (
instance.nodeType !== ELEMENT_NODE ||
instance.nodeName.toLowerCase() !== type.toLowerCase()
) {
return null;
} else {
return ((instance: any): Instance);
}
// 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;
while (instance.nodeType !== TEXT_NODE) {
if (!inRootOrSingleton || !enableHostSingletons) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
if (instance.nodeType !== TEXT_NODE) {
// Empty strings are not parsed by HTML so there won't be a correct match here.
return null;
}
// This has now been refined to a text node.
return ((instance: any): TextInstance);
@ -1204,17 +1189,9 @@ export function canHydrateTextInstance(
export function canHydrateSuspenseInstance(
instance: HydratableInstance,
inRootOrSingleton: boolean,
): null | SuspenseInstance {
while (instance.nodeType !== COMMENT_NODE) {
if (!inRootOrSingleton || !enableHostSingletons) {
return null;
}
const nextInstance = getNextHydratableSibling(instance);
if (nextInstance === null) {
return null;
}
instance = nextInstance;
if (instance.nodeType !== COMMENT_NODE) {
return null;
}
// This has now been refined to a suspense node.
return ((instance: any): SuspenseInstance);
@ -1439,14 +1416,12 @@ 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 (
(enableHostSingletons ||
(parentType !== 'head' && parentType !== 'body')) &&
(!enableFormActions || (parentType !== 'form' && parentType !== 'button'))
);
return parentType !== 'head' && parentType !== 'body';
}
export function didNotMatchHydratedContainerTextInstance(

View File

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

View File

@ -46,6 +46,7 @@ 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, ReactCustomFormAction} from 'shared/ReactTypes';
import type {ReactNodeList} from 'shared/ReactTypes';
import {
checkHtmlStringCoercion,
@ -131,7 +131,7 @@ export type ResponseState = {
instructions: InstructionState,
// state for data streaming format
externalRuntimeScript: null | ExternalRuntimeScript,
externalRuntimeConfig: BootstrapScriptDescriptor | null,
// preamble and postamble chunks and state
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
@ -161,7 +161,6 @@ 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>');
@ -193,10 +192,6 @@ 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)
@ -216,7 +211,7 @@ export function createResponseState(
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);
const bootstrapChunks: Array<Chunk | PrecomputedChunk> = [];
let externalRuntimeScript: null | ExternalRuntimeScript = null;
let externalRuntimeDesc = null;
let streamingFormat = ScriptStreamingFormat;
if (bootstrapScriptContent !== undefined) {
bootstrapChunks.push(
@ -234,27 +229,12 @@ export function createResponseState(
if (externalRuntimeConfig !== undefined) {
streamingFormat = DataStreamingFormat;
if (typeof externalRuntimeConfig === 'string') {
externalRuntimeScript = {
externalRuntimeDesc = {
src: externalRuntimeConfig,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig,
async: true,
integrity: undefined,
nonce: nonce,
});
} else {
externalRuntimeScript = {
src: externalRuntimeConfig.src,
chunks: [],
};
pushScriptImpl(externalRuntimeScript.chunks, {
src: externalRuntimeConfig.src,
async: true,
integrity: externalRuntimeConfig.integrity,
nonce: nonce,
});
} else {
externalRuntimeDesc = externalRuntimeConfig;
}
}
}
@ -265,17 +245,10 @@ 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,
@ -292,18 +265,10 @@ 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,
@ -323,7 +288,7 @@ export function createResponseState(
streamingFormat,
startInlineScript: inlineScriptWithNonce,
instructions: NothingSent,
externalRuntimeScript,
externalRuntimeConfig: externalRuntimeDesc,
htmlChunks: null,
headChunks: null,
hasBody: false,
@ -332,7 +297,6 @@ export function createResponseState(
preloadChunks: [],
hoistableChunks: [],
stylesToHoist: false,
nonce,
};
}
@ -668,13 +632,6 @@ 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(
@ -684,36 +641,6 @@ 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,
@ -722,8 +649,7 @@ function pushFormActionAttribute(
formMethod: any,
formTarget: any,
name: any,
): null | FormData {
let formData = null;
): void {
if (enableFormActions && typeof formAction === 'function') {
// Function form actions cannot control the form properties
if (__DEV__) {
@ -752,55 +678,37 @@ function pushFormActionAttribute(
);
}
}
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);
// 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);
}
}
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(
@ -1365,7 +1273,7 @@ function injectFormReplayingRuntime(responseState: ResponseState): void {
// to emit anything. It's always used.
if (
(responseState.instructions & SentFormReplayingRuntime) === NothingSent &&
(!enableFizzExternalRuntime || !responseState.externalRuntimeScript)
(!enableFizzExternalRuntime || !responseState.externalRuntimeConfig)
) {
responseState.instructions |= SentFormReplayingRuntime;
responseState.bootstrapChunks.unshift(
@ -1422,8 +1330,6 @@ function pushStartForm(
}
}
let formData = null;
let formActionName = null;
if (enableFormActions && typeof formAction === 'function') {
// Function form actions cannot control the form properties
if (__DEV__) {
@ -1446,60 +1352,36 @@ function pushStartForm(
);
}
}
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);
// 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);
}
}
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.
@ -1592,7 +1474,7 @@ function pushInput(
}
}
const formData = pushFormActionAttribute(
pushFormActionAttribute(
target,
responseState,
formAction,
@ -1643,10 +1525,6 @@ function pushInput(
}
target.push(endOfStartTagSelfClosing);
// We place any additional hidden form fields after the input.
pushAdditionalFormFields(target, formData);
return null;
}
@ -1714,7 +1592,7 @@ function pushStartButton(
}
}
const formData = pushFormActionAttribute(
pushFormActionAttribute(
target,
responseState,
formAction,
@ -1725,10 +1603,6 @@ 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.
@ -1736,7 +1610,6 @@ function pushStartButton(
target.push(stringToChunk(encodeHTMLTextNode(children)));
return null;
}
return children;
}
@ -4185,15 +4058,15 @@ export function writePreamble(
if (
enableFizzExternalRuntime &&
!willFlushAllSegments &&
responseState.externalRuntimeScript
responseState.externalRuntimeConfig
) {
// 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, chunks} = responseState.externalRuntimeScript;
internalPreinitScript(resources, src, chunks);
const {src, integrity} = responseState.externalRuntimeConfig;
internalPreinitScript(resources, src, integrity);
}
const htmlChunks = responseState.htmlChunks;
@ -5469,22 +5342,30 @@ 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,
chunks: Array<Chunk | PrecomputedChunk>,
integrity: ?string,
): 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,7 +9,6 @@
import type {
BootstrapScriptDescriptor,
ExternalRuntimeScript,
FormatContext,
StreamingFormat,
InstructionState,
@ -49,7 +48,7 @@ export type ResponseState = {
streamingFormat: StreamingFormat,
startInlineScript: PrecomputedChunk,
instructions: InstructionState,
externalRuntimeScript: null | ExternalRuntimeScript,
externalRuntimeConfig: BootstrapScriptDescriptor | null,
htmlChunks: null | Array<Chunk | PrecomputedChunk>,
headChunks: null | Array<Chunk | PrecomputedChunk>,
hasBody: boolean,
@ -86,7 +85,7 @@ export function createResponseState(
streamingFormat: responseState.streamingFormat,
startInlineScript: responseState.startInlineScript,
instructions: responseState.instructions,
externalRuntimeScript: responseState.externalRuntimeScript,
externalRuntimeConfig: responseState.externalRuntimeConfig,
htmlChunks: responseState.htmlChunks,
headChunks: responseState.headChunks,
hasBody: responseState.hasBody,

View File

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

View File

@ -22,7 +22,6 @@ let React;
let ReactDOMServer;
let ReactDOMClient;
let useFormStatus;
let useOptimistic;
describe('ReactDOMFizzForm', () => {
beforeEach(() => {
@ -31,7 +30,6 @@ 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);
@ -183,7 +181,7 @@ describe('ReactDOMFizzForm', () => {
});
// @gate enableFormActions || !__DEV__
it('should ideally warn when passing a string during SSR and function during hydration', async () => {
it('should warn when passing a string during SSR and function during hydration', async () => {
function action(formData) {}
function App({isClient}) {
return (
@ -195,10 +193,13 @@ describe('ReactDOMFizzForm', () => {
const stream = await ReactDOMServer.renderToReadableStream(<App />);
await readIntoContainer(stream);
// 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} />);
});
await expect(async () => {
await act(async () => {
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
});
}).toErrorDev(
'Prop `action` did not match. Server: "action" Client: "function action(formData) {}"',
);
});
// @gate enableFormActions || !__DEV__
@ -452,150 +453,4 @@ 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,7 +20,6 @@ let JSDOM;
let Stream;
let Scheduler;
let React;
let ReactDOM;
let ReactDOMClient;
let ReactDOMFizzServer;
let Suspense;
@ -74,7 +73,6 @@ 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');
@ -576,7 +574,7 @@ describe('ReactDOMFizzServer', () => {
);
});
it('should support nonce for bootstrap and runtime scripts', async () => {
it('should support nonce scripts', async () => {
CSPnonce = 'R4nd0m';
try {
let resolve;
@ -593,26 +591,11 @@ describe('ReactDOMFizzServer', () => {
<Lazy text="Hello" />
</Suspense>
</div>,
{
nonce: 'R4nd0m',
bootstrapScriptContent: 'function noop(){}',
bootstrapScripts: ['init.js'],
bootstrapModules: ['init.mjs'],
},
{nonce: 'R4nd0m'},
);
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});
});
@ -622,53 +605,6 @@ 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(() => {
@ -2509,98 +2445,6 @@ 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',
@ -3861,7 +3705,7 @@ describe('ReactDOMFizzServer', () => {
Array.from(document.head.getElementsByTagName('script')).map(
n => n.outerHTML,
),
).toEqual(['<script src="src-of-external-runtime" async=""></script>']);
).toEqual(['<script async="" src="src-of-external-runtime"></script>']);
expect(getVisibleChildren(document)).toEqual(
<html>

View File

@ -486,21 +486,4 @@ 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) Meta Platforms, Inc. and its affiliates.
* Copyright (c) Facebook, 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,41 +695,4 @@ 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,16 +81,4 @@ 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,7 +8,6 @@
*/
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,12 +103,6 @@ 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.23.0"
"scheduler": "^0.11.0"
},
"peerDependencies": {
"react": "^18.0.0"
"react": "^17.0.0"
}
}

View File

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

View File

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

View File

@ -0,0 +1,359 @@
/**
* 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

@ -0,0 +1,19 @@
/**
* 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,7 +55,6 @@ import {
enableLegacyHidden,
enableHostSingletons,
diffInCommitPhase,
alwaysThrottleRetries,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@ -2906,35 +2905,17 @@ 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) {
// 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.
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
markCommitTimeOfFallback();
}
}

View File

@ -31,6 +31,9 @@ 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,13 +149,11 @@ 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,
@ -1138,14 +1136,6 @@ 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) {
@ -1156,8 +1146,10 @@ function updateReducerImpl<S, A>(
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = hook.baseQueue;
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
const pendingQueue = queue.pending;
@ -1188,7 +1180,7 @@ function updateReducerImpl<S, A>(
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = hook.baseState;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
@ -1214,7 +1206,6 @@ function updateReducerImpl<S, A>(
// update/state.
const clone: Update<S, A> = {
lane: updateLane,
revertLane: update.revertLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
@ -1237,68 +1228,18 @@ function updateReducerImpl<S, A>(
} else {
// This update does have sufficient priority.
// 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);
}
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;
}
// Process this update.
@ -1776,6 +1717,8 @@ 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,
@ -1797,28 +1740,15 @@ 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.
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;
}
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;
}
}
}
@ -1841,7 +1771,7 @@ function updateSyncExternalStore<T>(
if (
inst.getSnapshot !== getSnapshot ||
snapshotChanged ||
// Check if the subscribe function changed. We can save some memory by
// Check if the susbcribe function changed. We can save some memory by
// checking whether we scheduled a subscription effect above.
(workInProgressHook !== null &&
workInProgressHook.memoizedState.tag & HookHasEffect)
@ -1865,7 +1795,7 @@ function updateSyncExternalStore<T>(
);
}
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
if (!includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
@ -1954,7 +1884,9 @@ function forceStoreRerender(fiber: Fiber) {
}
}
function mountStateImpl<S>(initialState: (() => S) | S): Hook {
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
@ -1969,106 +1901,21 @@ function mountStateImpl<S>(initialState: (() => S) | S): Hook {
lastRenderedState: (initialState: any),
};
hook.queue = queue;
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;
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch =
(dispatchSetState.bind(null, currentlyRenderingFiber, queue): any));
return [hook.memoizedState, dispatch];
}
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, initialState);
return updateReducer(basicStateReducer, (initialState: any));
}
function rerenderState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
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];
return rerenderReducer(basicStateReducer, (initialState: any));
}
function pushEffect(
@ -2228,8 +2075,7 @@ function updateEffectImpl(
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
// currentHook is null when rerendering after a render phase state update.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
@ -2599,10 +2445,9 @@ 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 {
@ -2612,20 +2457,8 @@ function startTransition<S>(
);
const prevTransition = ReactCurrentBatchConfig.transition;
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);
}
ReactCurrentBatchConfig.transition = null;
setPending(pendingState);
const currentTransition = (ReactCurrentBatchConfig.transition =
({}: BatchConfigTransition));
@ -2652,10 +2485,10 @@ function startTransition<S>(
returnValue,
finishedState,
);
dispatchSetState(fiber, queue, maybeThenable);
setPending(maybeThenable);
} else {
// Async actions are not enabled.
dispatchSetState(fiber, queue, finishedState);
setPending(finishedState);
callback();
}
} catch (error) {
@ -2668,7 +2501,7 @@ function startTransition<S>(
status: 'rejected',
reason: error,
};
dispatchSetState(fiber, queue, rejectedThenable);
setPending(rejectedThenable);
} else {
// The error rethrowing behavior is only enabled when the async actions
// feature is on, even for sync actions.
@ -2720,10 +2553,7 @@ export function startHostTransition<F>(
);
}
let queue: UpdateQueue<
Thenable<TransitionStatus> | TransitionStatus,
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
>;
let setPending;
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
@ -2731,28 +2561,28 @@ export function startHostTransition<F>(
//
// Create the state hook used by TransitionAwareHostComponent. This is
// essentially an inlined version of mountState.
const newQueue: UpdateQueue<
const queue: UpdateQueue<
Thenable<TransitionStatus> | TransitionStatus,
Thenable<TransitionStatus> | TransitionStatus,
BasicStateAction<Thenable<TransitionStatus> | TransitionStatus>,
> = {
pending: null,
lanes: NoLanes,
// We're going to cheat and intentionally not create a bound dispatch
// method, because we can call it directly in startTransition.
dispatch: (null: any),
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: NoPendingHostTransition,
};
queue = newQueue;
const stateHook: Hook = {
memoizedState: NoPendingHostTransition,
baseState: NoPendingHostTransition,
baseQueue: null,
queue: newQueue,
queue: queue,
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;
@ -2763,14 +2593,15 @@ export function startHostTransition<F>(
} else {
// This fiber was already upgraded to be stateful.
const stateHook: Hook = formFiber.memoizedState;
queue = stateHook.queue;
const dispatch: (Thenable<TransitionStatus> | TransitionStatus) => void =
stateHook.queue.dispatch;
setPending = dispatch;
}
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),
@ -2781,15 +2612,9 @@ function mountTransition(): [
boolean,
(callback: () => void, options?: StartTransitionOptions) => void,
] {
const stateHook = mountStateImpl((false: Thenable<boolean> | boolean));
const [, setPending] = mountState((false: Thenable<boolean> | boolean));
// The `start` method never changes.
const start = startTransition.bind(
null,
currentlyRenderingFiber,
stateHook.queue,
true,
false,
);
const start = startTransition.bind(null, true, false, setPending);
const hook = mountWorkInProgressHook();
hook.memoizedState = start;
return [false, start];
@ -2960,7 +2785,6 @@ function dispatchReducerAction<S, A>(
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
@ -2999,7 +2823,6 @@ function dispatchSetState<S, A>(
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
@ -3063,58 +2886,6 @@ 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 (
@ -3218,9 +2989,6 @@ if (enableFormActions && enableAsyncActions) {
(ContextOnlyDispatcher: Dispatcher).useHostTransitionStatus =
throwInvalidHookError;
}
if (enableAsyncActions) {
(ContextOnlyDispatcher: Dispatcher).useOptimistic = throwInvalidHookError;
}
const HooksDispatcherOnMount: Dispatcher = {
readContext,
@ -3256,10 +3024,6 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnMount: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnMount: Dispatcher).useOptimistic = mountOptimistic;
}
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
@ -3294,9 +3058,6 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnUpdate: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnUpdate: Dispatcher).useOptimistic = updateOptimistic;
}
const HooksDispatcherOnRerender: Dispatcher = {
readContext,
@ -3332,9 +3093,6 @@ if (enableFormActions && enableAsyncActions) {
(HooksDispatcherOnRerender: Dispatcher).useHostTransitionStatus =
useHostTransitionStatus;
}
if (enableAsyncActions) {
(HooksDispatcherOnRerender: Dispatcher).useOptimistic = rerenderOptimistic;
}
let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@ -3525,17 +3283,6 @@ 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 {
@ -3694,17 +3441,6 @@ 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 {
@ -3865,17 +3601,6 @@ 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 {
@ -4036,17 +3761,6 @@ 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 {
@ -4229,18 +3943,6 @@ 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 {
@ -4426,18 +4128,6 @@ 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 {
@ -4623,16 +4313,4 @@ 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,6 +74,9 @@ import {
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
resolveSingletonInstance,
shouldSkipHydratableForInstance,
shouldSkipHydratableForTextInstance,
shouldSkipHydratableForSuspenseInstance,
canHydrateInstance,
canHydrateTextInstance,
canHydrateSuspenseInstance,
@ -352,7 +355,6 @@ function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
nextInstance,
fiber.type,
fiber.pendingProps,
rootOrSingletonContext,
);
if (instance !== null) {
fiber.stateNode = (instance: Instance);
@ -367,11 +369,7 @@ 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,
rootOrSingletonContext,
);
const textInstance = canHydrateTextInstance(nextInstance, text);
if (textInstance !== null) {
fiber.stateNode = (textInstance: TextInstance);
hydrationParentFiber = fiber;
@ -384,10 +382,7 @@ function tryHydrateText(fiber: Fiber, nextInstance: any) {
function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
// fiber is a SuspenseComponent Fiber
const suspenseInstance = canHydrateSuspenseInstance(
nextInstance,
rootOrSingletonContext,
);
const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
if (suspenseInstance !== null) {
const suspenseState: SuspenseState = {
dehydrated: suspenseInstance,
@ -446,6 +441,44 @@ 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;
@ -460,6 +493,10 @@ 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)) {
@ -484,6 +521,10 @@ 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)
@ -511,6 +552,12 @@ 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.
@ -538,6 +585,11 @@ 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)
@ -562,6 +614,10 @@ 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)) {
@ -587,6 +643,11 @@ 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)
@ -802,8 +863,7 @@ function popHydrationState(fiber: Fiber): boolean {
fiber.tag !== HostSingleton &&
!(
fiber.tag === HostComponent &&
(!shouldDeleteUnhydratedTailInstances(fiber.type) ||
shouldSetTextContent(fiber.type, fiber.memoizedProps))
shouldSetTextContent(fiber.type, fiber.memoizedProps)
)
) {
shouldClear = true;

View File

@ -151,29 +151,17 @@ export function flushSyncWorkOnLegacyRootsOnly() {
flushSyncWorkAcrossRoots_impl(true);
}
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;
export function _doFlushWork(
firstRoot,
workInProgressRoot,
workInProgressRootRenderLanes,
onlyLegacy,
) {
let didPerformSomeWork = false;
let errors: Array<mixed> | null = null;
isFlushingWork = true;
do {
didPerformSomeWork = false;
let root = firstScheduledRoot;
let root = firstRoot;
while (root !== null) {
if (onlyLegacy && root.tag !== LegacyRoot) {
// Skip non-legacy roots.
@ -202,6 +190,33 @@ function flushSyncWorkAcrossRoots_impl(onlyLegacy: boolean) {
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,12 +370,10 @@ let workInProgressRootConcurrentErrors: Array<CapturedValue<mixed>> | null =
let workInProgressRootRecoverableErrors: Array<CapturedValue<mixed>> | null =
null;
// 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?
// 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.
let globalMostRecentFallbackTime: number = 0;
const FALLBACK_THROTTLE_MS: number = 300;
const FALLBACK_THROTTLE_MS: number = 500;
// The absolute time for when we should start giving up on rendering
// more and prefer CPU suspense heuristics instead.

View File

@ -57,8 +57,7 @@ export type HookType =
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useId'
| 'useCacheRefresh'
| 'useOptimistic';
| 'useCacheRefresh';
export type ContextDependency<T> = {
context: ReactContext<T>,
@ -424,10 +423,6 @@ 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,7 +5,6 @@ let act;
let assertLog;
let useTransition;
let useState;
let useOptimistic;
let textCache;
describe('ReactAsyncActions', () => {
@ -19,7 +18,6 @@ describe('ReactAsyncActions', () => {
assertLog = require('internal-test-utils').assertLog;
useTransition = React.useTransition;
useState = React.useState;
useOptimistic = React.experimental_useOptimistic;
textCache = new Map();
});
@ -646,432 +644,4 @@ 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

@ -0,0 +1,63 @@
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,102 +1811,6 @@ 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,6 +149,12 @@ 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

@ -7,4 +7,4 @@
* @flow
*/
export * from '../../../react-server-dom-fb/src/ReactServerStreamConfigFB';
export * from 'react-native-renderer/src/ReactFiberConfigFabric';

View File

@ -657,6 +657,7 @@ 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

@ -7,6 +7,4 @@
* @flow
*/
'use strict';
export * from './src/forks/SchedulerNative';
export * from './src/ReactFlightDOMRelayClient';

View File

@ -0,0 +1,17 @@
{
"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

@ -0,0 +1,10 @@
/**
* 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

@ -0,0 +1,97 @@
/**
* 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

@ -0,0 +1,61 @@
/**
* 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

@ -0,0 +1,36 @@
/**
* 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

@ -0,0 +1,44 @@
/**
* 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

@ -0,0 +1,236 @@
/**
* 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);
}

View File

@ -0,0 +1,19 @@
/**
* 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.
*/
'use strict';
class JSResourceReferenceImpl {
constructor(moduleId) {
this._moduleId = moduleId;
}
getModuleId() {
return this._moduleId;
}
}
module.exports = JSResourceReferenceImpl;

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