diff --git a/.eslintrc.js b/.eslintrc.js
index bcab1b2756c1c..53e01fe8c16a7 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -236,7 +236,14 @@ module.exports = {
'no-inner-declarations': [ERROR, 'functions'],
'no-multi-spaces': ERROR,
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
- 'no-restricted-syntax': [ERROR, 'WithStatement'],
+ 'no-restricted-syntax': [
+ ERROR,
+ 'WithStatement',
+ {
+ selector: 'MemberExpression[property.name=/^(?:substring|substr)$/]',
+ message: 'Prefer string.slice() over .substring() and .substr().',
+ },
+ ],
'no-shadow': ERROR,
'no-unused-vars': [ERROR, {args: 'none'}],
'no-use-before-define': OFF,
diff --git a/.github/workflows/commit_artifacts.yml b/.github/workflows/commit_artifacts.yml
index fe6da856587db..11676a0dc04b5 100644
--- a/.github/workflows/commit_artifacts.yml
+++ b/.github/workflows/commit_artifacts.yml
@@ -1,13 +1,22 @@
-name: Commit Artifacts for Facebook WWW and fbsource
+name: Commit Artifacts for Meta WWW and fbsource
on:
push:
- branches: [main]
+ branches: [main, meta-www, meta-fbsource]
jobs:
download_artifacts:
runs-on: ubuntu-latest
+ outputs:
+ www_branch_count: ${{ steps.check_branches.outputs.www_branch_count }}
+ fbsource_branch_count: ${{ steps.check_branches.outputs.fbsource_branch_count }}
steps:
+ - uses: actions/checkout@v3
+ - name: "Check branches"
+ id: check_branches
+ run: |
+ echo "www_branch_count=$(git ls-remote --heads origin "refs/heads/meta-www" | wc -l)" >> "$GITHUB_OUTPUT"
+ echo "fbsource_branch_count=$(git ls-remote --heads origin "refs/heads/meta-fbsource" | wc -l)" >> "$GITHUB_OUTPUT"
- name: Download and unzip artifacts
uses: actions/github-script@v6
env:
@@ -168,6 +177,7 @@ jobs:
commit_www_artifacts:
needs: download_artifacts
+ if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.www_branch_count == '0') || github.ref == 'refs/heads/meta-www' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -204,6 +214,7 @@ jobs:
commit_fbsource_artifacts:
needs: download_artifacts
runs-on: ubuntu-latest
+ if: ${{ (github.ref == 'refs/heads/main' && needs.download_artifacts.outputs.fbsource_branch_count == '0') || github.ref == 'refs/heads/meta-fbsource' }}
steps:
- uses: actions/checkout@v3
with:
diff --git a/README.md b/README.md
index 6cbc3730a0ebe..766c673306a86 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
+# [React](https://reactjs.org/) · [](https://github.com/facebook/react/blob/main/LICENSE) [](https://www.npmjs.com/package/react) [](https://circleci.com/gh/facebook/react) [](https://reactjs.org/docs/how-to-contribute.html#your-first-pull-request)
React is a JavaScript library for building user interfaces.
diff --git a/fixtures/concurrent/time-slicing/src/index.js b/fixtures/concurrent/time-slicing/src/index.js
index 2b99f803f55c5..6a880584f0b55 100644
--- a/fixtures/concurrent/time-slicing/src/index.js
+++ b/fixtures/concurrent/time-slicing/src/index.js
@@ -22,7 +22,7 @@ class App extends PureComponent {
}
const multiplier = input.length !== 0 ? input.length : 1;
const complexity =
- (parseInt(window.location.search.substring(1), 10) / 100) * 25 || 25;
+ (parseInt(window.location.search.slice(1), 10) / 100) * 25 || 25;
const data = _.range(5).map(t =>
_.range(complexity * multiplier).map((j, i) => {
return {
diff --git a/fixtures/dom/src/react-loader.js b/fixtures/dom/src/react-loader.js
index c2f7b108abb16..b2a37c49e5ae2 100644
--- a/fixtures/dom/src/react-loader.js
+++ b/fixtures/dom/src/react-loader.js
@@ -11,7 +11,7 @@ import semver from 'semver';
function parseQuery(qstr) {
var query = {};
- var a = qstr.substr(1).split('&');
+ var a = qstr.slice(1).split('&');
for (var i = 0; i < a.length; i++) {
var b = a[i].split('=');
diff --git a/fixtures/flight/src/App.js b/fixtures/flight/src/App.js
index e3ba9462d63f6..5e6fe4927d202 100644
--- a/fixtures/flight/src/App.js
+++ b/fixtures/flight/src/App.js
@@ -7,8 +7,9 @@ import {Counter as Counter2} from './Counter2.js';
import ShowMore from './ShowMore.js';
import Button from './Button.js';
+import Form from './Form.js';
-import {like} from './actions.js';
+import {like, greet} from './actions.js';
export default async function App() {
const res = await fetch('http://localhost:3001/todos');
@@ -33,6 +34,7 @@ export default async function App() {
Lorem ipsum
+
diff --git a/fixtures/flight/src/Button.js b/fixtures/flight/src/Button.js
index bbdaeba2bde9c..d4a3f8500eaff 100644
--- a/fixtures/flight/src/Button.js
+++ b/fixtures/flight/src/Button.js
@@ -1,25 +1,30 @@
'use client';
import * as React from 'react';
+import {flushSync} from 'react-dom';
+import ErrorBoundary from './ErrorBoundary.js';
export default function Button({action, children}) {
const [isPending, setIsPending] = React.useState(false);
return (
-
+
+
+
);
}
diff --git a/fixtures/flight/src/ErrorBoundary.js b/fixtures/flight/src/ErrorBoundary.js
new file mode 100644
index 0000000000000..44dc0965142e2
--- /dev/null
+++ b/fixtures/flight/src/ErrorBoundary.js
@@ -0,0 +1,16 @@
+'use client';
+
+import * as React from 'react';
+
+export default class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ if (this.state.error) {
+ return
Caught an error: {this.state.error.message}
;
+ }
+ return this.props.children;
+ }
+}
diff --git a/fixtures/flight/src/Form.js b/fixtures/flight/src/Form.js
new file mode 100644
index 0000000000000..a4b92366d45aa
--- /dev/null
+++ b/fixtures/flight/src/Form.js
@@ -0,0 +1,34 @@
+'use client';
+
+import * as React from 'react';
+import {flushSync} from 'react-dom';
+import ErrorBoundary from './ErrorBoundary.js';
+
+export default function Form({action, children}) {
+ const [isPending, setIsPending] = React.useState(false);
+
+ return (
+
+
+
+ );
+}
diff --git a/fixtures/flight/src/actions.js b/fixtures/flight/src/actions.js
index 687f3f39da0dc..87cba005e0b72 100644
--- a/fixtures/flight/src/actions.js
+++ b/fixtures/flight/src/actions.js
@@ -3,3 +3,14 @@
export async function like() {
return new Promise((resolve, reject) => resolve('Liked'));
}
+
+export async function greet(formData) {
+ const name = formData.get('name') || 'you';
+ const file = formData.get('file');
+ if (file) {
+ return `Ok, ${name}, here is ${file.name}:
+ ${(await file.text()).toUpperCase()}
+ `;
+ }
+ return 'Hi ' + name + '!';
+}
diff --git a/fixtures/flight/src/index.js b/fixtures/flight/src/index.js
index 77461a258a0e3..3e8b7e5bcce28 100644
--- a/fixtures/flight/src/index.js
+++ b/fixtures/flight/src/index.js
@@ -1,5 +1,5 @@
import * as React from 'react';
-import {Suspense} from 'react';
+import {use, Suspense} from 'react';
import ReactDOM from 'react-dom/client';
import {createFromFetch, encodeReply} from 'react-server-dom-webpack/client';
@@ -27,7 +27,8 @@ let data = createFromFetch(
}
);
-// TODO: This transition shouldn't really be necessary but it is for now.
-React.startTransition(() => {
- ReactDOM.hydrateRoot(document, data);
-});
+function Shell({data}) {
+ return use(data);
+}
+
+ReactDOM.hydrateRoot(document, );
diff --git a/package.json b/package.json
index c805e3400568b..c1fcd28927bb5 100644
--- a/package.json
+++ b/package.json
@@ -82,7 +82,6 @@
"minimist": "^1.2.3",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
- "pacote": "^10.3.0",
"prettier": "2.8.3",
"pretty-format": "^29.4.1",
"prop-types": "^15.6.2",
@@ -103,7 +102,7 @@
"yargs": "^15.3.1"
},
"devEngines": {
- "node": "16.x || 18.x || 19.x"
+ "node": "16.x || 18.x || 19.x || 20.x"
},
"jest": {
"testRegex": "/scripts/jest/dont-run-jest-directly\\.js$"
@@ -118,7 +117,7 @@
"lint": "node ./scripts/tasks/eslint.js",
"lint-build": "node ./scripts/rollup/validate/index.js",
"extract-errors": "node scripts/error-codes/extract-errors.js",
- "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js && node ./scripts/yarn/downloadReactIsForPrettyFormat.js",
+ "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json && node ./scripts/flow/createFlowConfigs.js",
"debug-test": "yarn test --deprecated 'yarn test --debug'",
"test": "node ./scripts/jest/jest-cli.js",
"test-stable": "node ./scripts/jest/jest-cli.js --release-channel=stable",
diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
index 706613e401b28..a7b2abbe80d0b 100644
--- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
@@ -18,7 +18,7 @@ const ReactHooksESLintRule = ReactHooksESLintPlugin.rules['exhaustive-deps'];
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
- return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
+ return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
// ***************************************************
diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
index aa2bcf9846d31..7b90afb75a742 100644
--- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
+++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js
@@ -26,7 +26,7 @@ ESLintTester.setDefaultConfig({
function normalizeIndent(strings) {
const codeLines = strings[0].split('\n');
const leftPadding = codeLines[1].match(/\s+/)[0];
- return codeLines.map(line => line.substr(leftPadding.length)).join('\n');
+ return codeLines.map(line => line.slice(leftPadding.length)).join('\n');
}
// ***************************************************
diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
index da1e3e754e1d8..0b8b61b14fa54 100644
--- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
+++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js
@@ -1103,7 +1103,7 @@ export default {
extraWarning =
` You can also do a functional update '${
setStateRecommendation.setter
- }(${setStateRecommendation.missingDep.substring(
+ }(${setStateRecommendation.missingDep.slice(
0,
1,
)} => ...)' if you only need '${
diff --git a/packages/react-art/src/ReactFiberConfigART.js b/packages/react-art/src/ReactFiberConfigART.js
index bcf52149fd661..ace8b81a6e313 100644
--- a/packages/react-art/src/ReactFiberConfigART.js
+++ b/packages/react-art/src/ReactFiberConfigART.js
@@ -479,11 +479,3 @@ export function suspendInstance(type, props) {}
export function waitForCommitToBeReady() {
return null;
}
-// eslint-disable-next-line no-undef
-export function prepareRendererToRender(container: Container): void {
- // noop
-}
-
-export function resetRendererAfterRender(): void {
- // noop
-}
diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js
index 05ecf17268419..e8034d257e790 100644
--- a/packages/react-client/src/ReactFlightClient.js
+++ b/packages/react-client/src/ReactFlightClient.js
@@ -18,11 +18,14 @@ import type {
SSRManifest,
} from './ReactFlightClientConfig';
+import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
+
import {
resolveClientReference,
preloadModule,
requireModule,
parseModel,
+ dispatchHint,
} from './ReactFlightClientConfig';
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -515,11 +518,11 @@ export function parseModelString(
switch (value[1]) {
case '$': {
// This was an escaped string value.
- return value.substring(1);
+ return value.slice(1);
}
case 'L': {
// Lazy node
- const id = parseInt(value.substring(2), 16);
+ const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
@@ -527,21 +530,21 @@ export function parseModelString(
}
case '@': {
// Promise
- const id = parseInt(value.substring(2), 16);
+ const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
return chunk;
}
case 'S': {
// Symbol
- return Symbol.for(value.substring(2));
+ return Symbol.for(value.slice(2));
}
case 'P': {
// Server Context Provider
- return getOrCreateServerContext(value.substring(2)).Provider;
+ return getOrCreateServerContext(value.slice(2)).Provider;
}
case 'F': {
// Server Reference
- const id = parseInt(value.substring(2), 16);
+ const id = parseInt(value.slice(2), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@@ -580,13 +583,17 @@ export function parseModelString(
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
return undefined;
}
+ case 'D': {
+ // Date
+ return new Date(Date.parse(value.slice(2)));
+ }
case 'n': {
// BigInt
- return BigInt(value.substring(2));
+ return BigInt(value.slice(2));
}
default: {
// We assume that anything else is a reference ID.
- const id = parseInt(value.substring(1), 16);
+ const id = parseInt(value.slice(1), 16);
const chunk = getChunk(response, id);
switch (chunk.status) {
case RESOLVED_MODEL:
@@ -774,6 +781,15 @@ export function resolveErrorDev(
}
}
+export function resolveHint(
+ response: Response,
+ code: string,
+ model: UninitializedModel,
+): void {
+ const hintModel = parseModel(response, model);
+ dispatchHint(code, hintModel);
+}
+
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.
diff --git a/packages/react-client/src/ReactFlightClientStream.js b/packages/react-client/src/ReactFlightClientStream.js
index 81633e696696d..2c4c29919db6c 100644
--- a/packages/react-client/src/ReactFlightClientStream.js
+++ b/packages/react-client/src/ReactFlightClientStream.js
@@ -16,6 +16,7 @@ import {
resolveModel,
resolveErrorProd,
resolveErrorDev,
+ resolveHint,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
@@ -35,7 +36,7 @@ function processFullRow(response: Response, row: string): void {
return;
}
const colon = row.indexOf(':', 0);
- const id = parseInt(row.substring(0, colon), 16);
+ 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.
@@ -43,11 +44,16 @@ function processFullRow(response: Response, row: string): void {
// }
switch (tag) {
case 'I': {
- resolveModule(response, id, row.substring(colon + 2));
+ 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.substring(colon + 2));
+ const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
resolveErrorDev(
response,
@@ -63,7 +69,7 @@ function processFullRow(response: Response, row: string): void {
}
default: {
// We assume anything else is JSON.
- resolveModel(response, id, row.substring(colon + 1));
+ resolveModel(response, id, row.slice(colon + 1));
return;
}
}
@@ -76,13 +82,13 @@ export function processStringChunk(
): void {
let linebreak = chunk.indexOf('\n', offset);
while (linebreak > -1) {
- const fullrow = response._partialRow + chunk.substring(offset, linebreak);
+ const fullrow = response._partialRow + chunk.slice(offset, linebreak);
processFullRow(response, fullrow);
response._partialRow = '';
offset = linebreak + 1;
linebreak = chunk.indexOf('\n', offset);
}
- response._partialRow += chunk.substring(offset);
+ response._partialRow += chunk.slice(offset);
}
export function processBinaryChunk(
diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js
index 18fc2834e1da7..224af305d64b2 100644
--- a/packages/react-client/src/ReactFlightReplyClient.js
+++ b/packages/react-client/src/ReactFlightReplyClient.js
@@ -74,6 +74,11 @@ function serializeSymbolReference(name: string): string {
return '$S' + name;
}
+function serializeFormDataReference(id: number): string {
+ // Why K? F is "Function". D is "Date". What else?
+ return '$K' + id.toString(16);
+}
+
function serializeNumber(number: number): string | number {
if (Number.isFinite(number)) {
if (number === 0 && 1 / number === -Infinity) {
@@ -96,6 +101,12 @@ function serializeUndefined(): string {
return '$undefined';
}
+function serializeDateFromDateJSON(dateJSON: string): string {
+ // JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
+ // We need only tack on a $D prefix.
+ return '$D' + dateJSON;
+}
+
function serializeBigInt(n: bigint): string {
return '$n' + n.toString(10);
}
@@ -112,6 +123,7 @@ function escapeStringValue(value: string): string {
export function processReply(
root: ReactServerValue,
+ formFieldPrefix: string,
resolve: (string | FormData) => void,
reject: (error: mixed) => void,
): void {
@@ -127,10 +139,16 @@ export function processReply(
value: ReactServerValue,
): ReactJSONValue {
const parent = this;
+
+ // Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
if (__DEV__) {
// $FlowFixMe[incompatible-use]
- const originalValue = this[key];
- if (typeof originalValue === 'object' && originalValue !== value) {
+ const originalValue = parent[key];
+ if (
+ typeof originalValue === 'object' &&
+ originalValue !== value &&
+ !(originalValue instanceof Date)
+ ) {
if (objectName(originalValue) !== 'Object') {
console.error(
'Only plain objects can be passed to Server Functions from the Client. ' +
@@ -171,7 +189,7 @@ export function processReply(
// $FlowFixMe[incompatible-type] We know it's not null because we assigned it above.
const data: FormData = formData;
// eslint-disable-next-line react-internal/safe-string-coercion
- data.append('' + promiseId, partJSON);
+ data.append(formFieldPrefix + promiseId, partJSON);
pendingParts--;
if (pendingParts === 0) {
resolve(data);
@@ -185,6 +203,24 @@ export function processReply(
);
return serializePromiseID(promiseId);
}
+ // TODO: Should we the Object.prototype.toString.call() to test for cross-realm objects?
+ if (value instanceof FormData) {
+ if (formData === null) {
+ // Upgrade to use FormData to allow us to use rich objects as its values.
+ formData = new FormData();
+ }
+ const data: FormData = formData;
+ const refId = nextPartId++;
+ // Copy all the form fields with a prefix for this reference.
+ // These must come first in the form order because we assume that all the
+ // fields are available before this is referenced.
+ const prefix = formFieldPrefix + refId + '_';
+ // $FlowFixMe[prop-missing]: FormData has forEach.
+ value.forEach((originalValue: string | File, originalKey: string) => {
+ data.append(prefix + originalKey, originalValue);
+ });
+ return serializeFormDataReference(refId);
+ }
if (!isArray(value)) {
const iteratorFn = getIteratorFn(value);
if (iteratorFn) {
@@ -242,6 +278,17 @@ export function processReply(
}
if (typeof value === 'string') {
+ // TODO: Maybe too clever. If we support URL there's no similar trick.
+ if (value[value.length - 1] === 'Z') {
+ // 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);
+ }
+ }
+
return escapeStringValue(value);
}
@@ -268,7 +315,7 @@ export function processReply(
// The reference to this function came from the same client so we can pass it back.
const refId = nextPartId++;
// eslint-disable-next-line react-internal/safe-string-coercion
- formData.set('' + refId, metaDataJSON);
+ formData.set(formFieldPrefix + refId, metaDataJSON);
return serializeServerReferenceID(refId);
}
throw new Error(
@@ -308,7 +355,7 @@ export function processReply(
resolve(json);
} else {
// Otherwise, we use FormData to let us stream in the result.
- formData.set('0', json);
+ formData.set(formFieldPrefix + '0', json);
if (pendingParts === 0) {
// $FlowFixMe[incompatible-call] this has already been refined.
resolve(formData);
diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js
index 9fc2da89a0180..23a44a41ed803 100644
--- a/packages/react-client/src/__tests__/ReactFlight-test.js
+++ b/packages/react-client/src/__tests__/ReactFlight-test.js
@@ -306,6 +306,23 @@ describe('ReactFlight', () => {
);
});
+ it('can transport Date', async () => {
+ function ComponentClient({prop}) {
+ return `prop: ${prop.toISOString()}`;
+ }
+ const Component = clientReference(ComponentClient);
+
+ const model = ;
+
+ const transport = ReactNoopFlightServer.render(model);
+
+ await act(async () => {
+ ReactNoop.render(await ReactNoopFlightClient.read(transport));
+ });
+
+ expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
+ });
+
it('can render a lazy component as a shared component on the server', async () => {
function SharedComponent({text}) {
return (
@@ -513,7 +530,6 @@ describe('ReactFlight', () => {
expect(ReactNoop).toMatchRenderedOutput(
I am client
);
});
- // @gate enableUseHook
it('should error if a non-serializable value is passed to a host component', async () => {
function ClientImpl({children}) {
return children;
@@ -624,7 +640,6 @@ describe('ReactFlight', () => {
});
});
- // @gate enableUseHook
it('should trigger the inner most error boundary inside a Client Component', async () => {
function ServerComponent() {
throw new Error('This was thrown in the Server Component.');
@@ -675,28 +690,39 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
expect(() => {
- const transport = ReactNoopFlightServer.render(
- ,
- );
+ const transport = ReactNoopFlightServer.render();
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
- 'Date objects are not supported.',
+ 'Objects with toJSON methods are not supported. ' +
+ 'Convert it manually to a simple value before passing it to props.\n' +
+ ' \n' +
+ ' ^^^^^^^^^^^^^^^^^^^^',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
+ class MyError extends Error {
+ toJSON() {
+ return 123;
+ }
+ }
expect(() => {
const transport = ReactNoopFlightServer.render(
-
Current date: {new Date()}
,
+
Womp womp: {new MyError('spaghetti')}
,
);
ReactNoopFlightClient.read(transport);
}).toErrorDev(
- 'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
- '
Current date: {Date}
\n' +
- ' ^^^^^^',
+ 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
+ '
Womp womp: {Error}
\n' +
+ ' ^^^^^^^',
{withoutStack: true},
);
});
@@ -728,37 +754,46 @@ describe('ReactFlight', () => {
});
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
function ClientImpl({value}) {
return
{value}
;
}
const Client = clientReference(ClientImpl);
expect(() => {
- const transport = ReactNoopFlightServer.render(
- ,
- );
+ const transport = ReactNoopFlightServer.render();
ReactNoopFlightClient.read(transport);
}).toErrorDev(
'Only plain objects can be passed to Client Components from Server Components. ' +
- 'Date objects are not supported.',
+ 'Objects with toJSON methods are not supported.',
{withoutStack: true},
);
});
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
+ const obj = {
+ toJSON() {
+ return 123;
+ },
+ };
function ClientImpl({children}) {
return