diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 4d5204ab00220..0e2bb5b9f4606 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -43,9 +43,7 @@ import type {Postpone} from 'react/src/ReactPostpone'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; import { - enableBinaryFlight, enablePostpone, - enableFlightReadableStream, enableOwnerStacks, enableServerComponentLogs, enableProfilerTimer, @@ -407,14 +405,12 @@ function wakeChunkIfInitialized( function triggerErrorOnChunk(chunk: SomeChunk, error: mixed): void { if (chunk.status !== PENDING && chunk.status !== BLOCKED) { - if (enableFlightReadableStream) { - // If we get more data to an already resolved ID, we assume that it's - // a stream chunk since any other row shouldn't have more than one entry. - const streamChunk: InitializedStreamChunk = (chunk: any); - const controller = streamChunk.reason; - // $FlowFixMe[incompatible-call]: The error method should accept mixed. - controller.error(error); - } + // If we get more data to an already resolved ID, we assume that it's + // a stream chunk since any other row shouldn't have more than one entry. + const streamChunk: InitializedStreamChunk = (chunk: any); + const controller = streamChunk.reason; + // $FlowFixMe[incompatible-call]: The error method should accept mixed. + controller.error(error); return; } const listeners = chunk.reason; @@ -513,13 +509,11 @@ function resolveModelChunk( value: UninitializedModel, ): void { if (chunk.status !== PENDING) { - if (enableFlightReadableStream) { - // If we get more data to an already resolved ID, we assume that it's - // a stream chunk since any other row shouldn't have more than one entry. - const streamChunk: InitializedStreamChunk = (chunk: any); - const controller = streamChunk.reason; - controller.enqueueModel(value); - } + // If we get more data to an already resolved ID, we assume that it's + // a stream chunk since any other row shouldn't have more than one entry. + const streamChunk: InitializedStreamChunk = (chunk: any); + const controller = streamChunk.reason; + controller.enqueueModel(value); return; } const resolveListeners = chunk.value; @@ -1461,11 +1455,8 @@ function parseModelString( } case 'B': { // Blob - if (enableBinaryFlight) { - const ref = value.slice(2); - return getOutlinedModel(response, ref, parentObject, key, createBlob); - } - return undefined; + const ref = value.slice(2); + return getOutlinedModel(response, ref, parentObject, key, createBlob); } case 'K': { // FormData @@ -1722,16 +1713,14 @@ function resolveModel( function resolveText(response: Response, id: number, text: string): void { const chunks = response._chunks; - if (enableFlightReadableStream) { - const chunk = chunks.get(id); - if (chunk && chunk.status !== PENDING) { - // If we get more data to an already resolved ID, we assume that it's - // a stream chunk since any other row shouldn't have more than one entry. - const streamChunk: InitializedStreamChunk = (chunk: any); - const controller = streamChunk.reason; - controller.enqueueValue(text); - return; - } + const chunk = chunks.get(id); + if (chunk && chunk.status !== PENDING) { + // If we get more data to an already resolved ID, we assume that it's + // a stream chunk since any other row shouldn't have more than one entry. + const streamChunk: InitializedStreamChunk = (chunk: any); + const controller = streamChunk.reason; + controller.enqueueValue(text); + return; } chunks.set(id, createInitializedTextChunk(response, text)); } @@ -1742,16 +1731,14 @@ function resolveBuffer( buffer: $ArrayBufferView | ArrayBuffer, ): void { const chunks = response._chunks; - if (enableFlightReadableStream) { - const chunk = chunks.get(id); - if (chunk && chunk.status !== PENDING) { - // If we get more data to an already resolved ID, we assume that it's - // a stream chunk since any other row shouldn't have more than one entry. - const streamChunk: InitializedStreamChunk = (chunk: any); - const controller = streamChunk.reason; - controller.enqueueValue(buffer); - return; - } + const chunk = chunks.get(id); + if (chunk && chunk.status !== PENDING) { + // If we get more data to an already resolved ID, we assume that it's + // a stream chunk since any other row shouldn't have more than one entry. + const streamChunk: InitializedStreamChunk = (chunk: any); + const controller = streamChunk.reason; + controller.enqueueValue(buffer); + return; } chunks.set(id, createInitializedBufferChunk(response, buffer)); } @@ -2821,53 +2808,51 @@ function processFullBinaryRow( buffer: Array, chunk: Uint8Array, ): void { - if (enableBinaryFlight) { - switch (tag) { - case 65 /* "A" */: - // We must always clone to extract it into a separate buffer instead of just a view. - resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); - return; - case 79 /* "O" */: - resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); - return; - case 111 /* "o" */: - resolveBuffer( - response, - id, - buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), - ); - return; - case 85 /* "U" */: - resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); - return; - case 83 /* "S" */: - resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); - return; - case 115 /* "s" */: - resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); - return; - case 76 /* "L" */: - resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); - return; - case 108 /* "l" */: - resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); - return; - case 71 /* "G" */: - resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); - return; - case 103 /* "g" */: - resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); - return; - case 77 /* "M" */: - resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); - return; - case 109 /* "m" */: - resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); - return; - case 86 /* "V" */: - resolveTypedArray(response, id, buffer, chunk, DataView, 1); - return; - } + switch (tag) { + case 65 /* "A" */: + // We must always clone to extract it into a separate buffer instead of just a view. + resolveBuffer(response, id, mergeBuffer(buffer, chunk).buffer); + return; + case 79 /* "O" */: + resolveTypedArray(response, id, buffer, chunk, Int8Array, 1); + return; + case 111 /* "o" */: + resolveBuffer( + response, + id, + buffer.length === 0 ? chunk : mergeBuffer(buffer, chunk), + ); + return; + case 85 /* "U" */: + resolveTypedArray(response, id, buffer, chunk, Uint8ClampedArray, 1); + return; + case 83 /* "S" */: + resolveTypedArray(response, id, buffer, chunk, Int16Array, 2); + return; + case 115 /* "s" */: + resolveTypedArray(response, id, buffer, chunk, Uint16Array, 2); + return; + case 76 /* "L" */: + resolveTypedArray(response, id, buffer, chunk, Int32Array, 4); + return; + case 108 /* "l" */: + resolveTypedArray(response, id, buffer, chunk, Uint32Array, 4); + return; + case 71 /* "G" */: + resolveTypedArray(response, id, buffer, chunk, Float32Array, 4); + return; + case 103 /* "g" */: + resolveTypedArray(response, id, buffer, chunk, Float64Array, 8); + return; + case 77 /* "M" */: + resolveTypedArray(response, id, buffer, chunk, BigInt64Array, 8); + return; + case 109 /* "m" */: + resolveTypedArray(response, id, buffer, chunk, BigUint64Array, 8); + return; + case 86 /* "V" */: + resolveTypedArray(response, id, buffer, chunk, DataView, 1); + return; } const stringDecoder = response._stringDecoder; @@ -2973,38 +2958,28 @@ function processFullStringRow( ); } case 82 /* "R" */: { - if (enableFlightReadableStream) { - startReadableStream(response, id, undefined); - return; - } + startReadableStream(response, id, undefined); + return; } // Fallthrough case 114 /* "r" */: { - if (enableFlightReadableStream) { - startReadableStream(response, id, 'bytes'); - return; - } + startReadableStream(response, id, 'bytes'); + return; } // Fallthrough case 88 /* "X" */: { - if (enableFlightReadableStream) { - startAsyncIterable(response, id, false); - return; - } + startAsyncIterable(response, id, false); + return; } // Fallthrough case 120 /* "x" */: { - if (enableFlightReadableStream) { - startAsyncIterable(response, id, true); - return; - } + startAsyncIterable(response, id, true); + return; } // Fallthrough case 67 /* "C" */: { - if (enableFlightReadableStream) { - stopStream(response, id, row); - return; - } + stopStream(response, id, row); + return; } // Fallthrough case 80 /* "P" */: { @@ -3061,20 +3036,19 @@ export function processBinaryChunk( const resolvedRowTag = chunk[i]; if ( resolvedRowTag === 84 /* "T" */ || - (enableBinaryFlight && - (resolvedRowTag === 65 /* "A" */ || - resolvedRowTag === 79 /* "O" */ || - resolvedRowTag === 111 /* "o" */ || - resolvedRowTag === 85 /* "U" */ || - resolvedRowTag === 83 /* "S" */ || - resolvedRowTag === 115 /* "s" */ || - resolvedRowTag === 76 /* "L" */ || - resolvedRowTag === 108 /* "l" */ || - resolvedRowTag === 71 /* "G" */ || - resolvedRowTag === 103 /* "g" */ || - resolvedRowTag === 77 /* "M" */ || - resolvedRowTag === 109 /* "m" */ || - resolvedRowTag === 86)) /* "V" */ + resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86 /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; @@ -3187,20 +3161,19 @@ export function processStringChunk(response: Response, chunk: string): void { const resolvedRowTag = chunk.charCodeAt(i); if ( resolvedRowTag === 84 /* "T" */ || - (enableBinaryFlight && - (resolvedRowTag === 65 /* "A" */ || - resolvedRowTag === 79 /* "O" */ || - resolvedRowTag === 111 /* "o" */ || - resolvedRowTag === 85 /* "U" */ || - resolvedRowTag === 83 /* "S" */ || - resolvedRowTag === 115 /* "s" */ || - resolvedRowTag === 76 /* "L" */ || - resolvedRowTag === 108 /* "l" */ || - resolvedRowTag === 71 /* "G" */ || - resolvedRowTag === 103 /* "g" */ || - resolvedRowTag === 77 /* "M" */ || - resolvedRowTag === 109 /* "m" */ || - resolvedRowTag === 86)) /* "V" */ + resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86 /* "V" */ ) { rowTag = resolvedRowTag; rowState = ROW_LENGTH; diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index bfb2573ea35b0..65d1129b53830 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -18,11 +18,7 @@ import type { import type {LazyComponent} from 'react/src/ReactLazy'; import type {TemporaryReferenceSet} from './ReactFlightTemporaryReferences'; -import { - enableRenderableContext, - enableBinaryFlight, - enableFlightReadableStream, -} from 'shared/ReactFeatureFlags'; +import {enableRenderableContext} from 'shared/ReactFeatureFlags'; import { REACT_ELEMENT_TYPE, @@ -578,73 +574,71 @@ export function processReply( return serializeSetID(setId); } - if (enableBinaryFlight) { - if (value instanceof ArrayBuffer) { - const blob = new Blob([value]); - const blobId = nextPartId++; - if (formData === null) { - formData = new FormData(); - } - formData.append(formFieldPrefix + blobId, blob); - return '$' + 'A' + blobId.toString(16); - } - if (value instanceof Int8Array) { - // char - return serializeTypedArray('O', value); - } - if (value instanceof Uint8Array) { - // unsigned char - return serializeTypedArray('o', value); - } - if (value instanceof Uint8ClampedArray) { - // unsigned clamped char - return serializeTypedArray('U', value); - } - if (value instanceof Int16Array) { - // sort - return serializeTypedArray('S', value); - } - if (value instanceof Uint16Array) { - // unsigned short - return serializeTypedArray('s', value); - } - if (value instanceof Int32Array) { - // long - return serializeTypedArray('L', value); - } - if (value instanceof Uint32Array) { - // unsigned long - return serializeTypedArray('l', value); - } - if (value instanceof Float32Array) { - // float - return serializeTypedArray('G', value); - } - if (value instanceof Float64Array) { - // double - return serializeTypedArray('g', value); - } - if (value instanceof BigInt64Array) { - // number - return serializeTypedArray('M', value); - } - if (value instanceof BigUint64Array) { - // unsigned number - // We use "m" instead of "n" since JSON can start with "null" - return serializeTypedArray('m', value); - } - if (value instanceof DataView) { - return serializeTypedArray('V', value); + if (value instanceof ArrayBuffer) { + const blob = new Blob([value]); + const blobId = nextPartId++; + if (formData === null) { + formData = new FormData(); } - // TODO: Blob is not available in old Node/browsers. Remove the typeof check later. - if (typeof Blob === 'function' && value instanceof Blob) { - if (formData === null) { - formData = new FormData(); - } - const blobId = nextPartId++; - formData.append(formFieldPrefix + blobId, value); - return serializeBlobID(blobId); + formData.append(formFieldPrefix + blobId, blob); + return '$' + 'A' + blobId.toString(16); + } + if (value instanceof Int8Array) { + // char + return serializeTypedArray('O', value); + } + if (value instanceof Uint8Array) { + // unsigned char + return serializeTypedArray('o', value); + } + if (value instanceof Uint8ClampedArray) { + // unsigned clamped char + return serializeTypedArray('U', value); + } + if (value instanceof Int16Array) { + // sort + return serializeTypedArray('S', value); + } + if (value instanceof Uint16Array) { + // unsigned short + return serializeTypedArray('s', value); + } + if (value instanceof Int32Array) { + // long + return serializeTypedArray('L', value); + } + if (value instanceof Uint32Array) { + // unsigned long + return serializeTypedArray('l', value); + } + if (value instanceof Float32Array) { + // float + return serializeTypedArray('G', value); + } + if (value instanceof Float64Array) { + // double + return serializeTypedArray('g', value); + } + if (value instanceof BigInt64Array) { + // number + return serializeTypedArray('M', value); + } + if (value instanceof BigUint64Array) { + // unsigned number + // We use "m" instead of "n" since JSON can start with "null" + return serializeTypedArray('m', value); + } + if (value instanceof DataView) { + return serializeTypedArray('V', value); + } + // TODO: Blob is not available in old Node/browsers. Remove the typeof check later. + if (typeof Blob === 'function' && value instanceof Blob) { + if (formData === null) { + formData = new FormData(); } + const blobId = nextPartId++; + formData.append(formFieldPrefix + blobId, value); + return serializeBlobID(blobId); } const iteratorFn = getIteratorFn(value); @@ -666,23 +660,21 @@ export function processReply( return Array.from((iterator: any)); } - if (enableFlightReadableStream) { - // TODO: ReadableStream is not available in old Node. Remove the typeof check later. - if ( - typeof ReadableStream === 'function' && - value instanceof ReadableStream - ) { - return serializeReadableStream(value); - } - const getAsyncIterator: void | (() => $AsyncIterator) = - (value: any)[ASYNC_ITERATOR]; - if (typeof getAsyncIterator === 'function') { - // We treat AsyncIterables as a Fragment and as such we might need to key them. - return serializeAsyncIterable( - (value: any), - getAsyncIterator.call((value: any)), - ); - } + // TODO: ReadableStream is not available in old Node. Remove the typeof check later. + if ( + typeof ReadableStream === 'function' && + value instanceof ReadableStream + ) { + return serializeReadableStream(value); + } + const getAsyncIterator: void | (() => $AsyncIterator) = + (value: any)[ASYNC_ITERATOR]; + if (typeof getAsyncIterator === 'function') { + // We treat AsyncIterables as a Fragment and as such we might need to key them. + return serializeAsyncIterable( + (value: any), + getAsyncIterator.call((value: any)), + ); } // Verify that this is a simple plain object. diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 75220b762918e..b2489705394a1 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2059,7 +2059,7 @@ describe('ReactFlight', () => { expect(errors).toEqual(['Cannot pass a secret token to the client']); }); - // @gate enableTaint && enableBinaryFlight + // @gate enableTaint it('errors when a tainted binary value is serialized', async () => { function UserClient({user}) { return {user.name}; @@ -2623,7 +2623,7 @@ describe('ReactFlight', () => { ); }); - // @gate enableFlightReadableStream && enableAsyncIterableChildren + // @gate enableAsyncIterableChildren it('shares state when moving keyed Server Components that render async iterables', async () => { function StatefulClient({name, initial}) { const [state] = React.useState(initial); @@ -2814,7 +2814,7 @@ describe('ReactFlight', () => { ); }); - // @gate enableFlightReadableStream && enableAsyncIterableChildren + // @gate enableAsyncIterableChildren it('preserves debug info for server-to-server pass through of async iterables', async () => { let resolve; const iteratorPromise = new Promise(r => (resolve = r)); @@ -2849,10 +2849,9 @@ describe('ReactFlight', () => { }, ); - if (gate(flag => flag.enableFlightReadableStream)) { - // Wait for the iterator to finish - await iteratorPromise; - } + // Wait for the iterator to finish + await iteratorPromise; + await 0; // One more tick for the return value / closing. const transport = ReactNoopFlightServer.render( diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 2a0c57154e6f9..eccaa6a67b166 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -104,22 +104,14 @@ function getPrimitiveStackCache(): Map> { ); Dispatcher.useDeferredValue(null); Dispatcher.useMemo(() => null); + Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); + Dispatcher.useFormState((s: mixed, p: mixed) => s, null); + Dispatcher.useActionState((s: mixed, p: mixed) => s, null); + Dispatcher.useHostTransitionStatus(); if (typeof Dispatcher.useMemoCache === 'function') { // This type check is for Flow only. Dispatcher.useMemoCache(0); } - if (typeof Dispatcher.useOptimistic === 'function') { - // This type check is for Flow only. - Dispatcher.useOptimistic(null, (s: mixed, a: mixed) => s); - } - if (typeof Dispatcher.useFormState === 'function') { - // This type check is for Flow only. - Dispatcher.useFormState((s: mixed, p: mixed) => s, null); - } - if (typeof Dispatcher.useActionState === 'function') { - // This type check is for Flow only. - Dispatcher.useActionState((s: mixed, p: mixed) => s, null); - } if (typeof Dispatcher.use === 'function') { // This type check is for Flow only. Dispatcher.use( @@ -143,11 +135,6 @@ function getPrimitiveStackCache(): Map> { } Dispatcher.useId(); - - if (typeof Dispatcher.useHostTransitionStatus === 'function') { - // This type check is for Flow only. - Dispatcher.useHostTransitionStatus(); - } } finally { readHookLog = hookLog; hookLog = []; diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index ef9a7c1e6edc7..fcd84fedfed71 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -2581,7 +2581,6 @@ describe('ReactHooksInspectionIntegration', () => { `); }); - // @gate enableAsyncActions it('should support useOptimistic hook', async () => { const useOptimistic = React.useOptimistic; function Foo() { @@ -2647,7 +2646,6 @@ describe('ReactHooksInspectionIntegration', () => { `); }); - // @gate enableAsyncActions it('should support useActionState hook', async () => { function Foo() { const [value] = React.useActionState(function increment(n) { diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index ac95c91c596a8..05892c930e1ca 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -63,10 +63,7 @@ import {validateProperties as validateInputProperties} from '../shared/ReactDOMN import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; import sanitizeURL from '../shared/sanitizeURL'; -import { - enableTrustedTypesIntegration, - enableFilterEmptyStringAttributesDOM, -} from 'shared/ReactFeatureFlags'; +import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags'; import { mediaEventTypes, listenToNonDelegatedEvent, @@ -400,35 +397,33 @@ function setProp( // fallthrough case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && key === 'href') - ) { - if (__DEV__) { - if (key === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - key, - key, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && key === 'href') + ) { + if (__DEV__) { + if (key === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); } - domElement.removeAttribute(key); - break; } + domElement.removeAttribute(key); + break; } if ( value == null || @@ -2489,53 +2484,52 @@ function diffHydratedGenericElement( // fallthrough case 'src': case 'href': - if (enableFilterEmptyStringAttributesDOM) { - if ( - value === '' && - // is fine for "reload" links. - !(tag === 'a' && propKey === 'href') && - !(tag === 'object' && propKey === 'data') - ) { - if (__DEV__) { - if (propKey === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } + if ( + value === '' && + // is fine for "reload" links. + !(tag === 'a' && propKey === 'href') && + !(tag === 'object' && propKey === 'data') + ) { + if (__DEV__) { + if (propKey === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - null, - extraAttributes, - serverDifferences, - ); - continue; } + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + null, + extraAttributes, + serverDifferences, + ); + continue; + } else { + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + serverDifferences, + ); + continue; } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - value, - extraAttributes, - serverDifferences, - ); - continue; case 'action': case 'formAction': { const serverValue = domElement.getAttribute(propKey); diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 0933794c68cb6..8e94d48beeef2 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -91,7 +91,6 @@ import { enableCreateEventHandleAPI, enableScopeAPI, enableTrustedTypesIntegration, - enableAsyncActions, disableLegacyMode, enableMoveBefore, } from 'shared/ReactFeatureFlags'; @@ -1378,9 +1377,8 @@ function getNextHydratable(node: ?Node) { nodeData === SUSPENSE_START_DATA || nodeData === SUSPENSE_FALLBACK_START_DATA || nodeData === SUSPENSE_PENDING_START_DATA || - (enableAsyncActions && - (nodeData === FORM_STATE_IS_MATCHING || - nodeData === FORM_STATE_IS_NOT_MATCHING)) + nodeData === FORM_STATE_IS_MATCHING || + nodeData === FORM_STATE_IS_NOT_MATCHING ) { break; } diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 1351a28db3aab..fe2e713d8a3ba 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -27,10 +27,7 @@ import { import {Children} from 'react'; -import { - enableFilterEmptyStringAttributesDOM, - enableFizzExternalRuntime, -} from 'shared/ReactFeatureFlags'; +import {enableFizzExternalRuntime} from 'shared/ReactFeatureFlags'; import type { Destination, @@ -1210,30 +1207,28 @@ function pushAttribute( } case 'src': case 'href': { - if (enableFilterEmptyStringAttributesDOM) { - if (value === '') { - if (__DEV__) { - if (name === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } + if (value === '') { + if (__DEV__) { + if (name === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); } - return; } + return; } } // Fall through to the last case which shouldn't remove empty strings. @@ -1633,19 +1628,17 @@ function pushStartObject( checkAttributeStringCoercion(propValue, 'data'); } const sanitizedValue = sanitizeURL('' + propValue); - if (enableFilterEmptyStringAttributesDOM) { - if (sanitizedValue === '') { - if (__DEV__) { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - propKey, - propKey, - ); - } - break; + if (sanitizedValue === '') { + if (__DEV__) { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); } + break; } target.push( attributeSeparator, @@ -3615,11 +3608,7 @@ export function pushStartInstance( // Fast track very common tags break; case 'a': - if (enableFilterEmptyStringAttributesDOM) { - return pushStartAnchor(target, props); - } else { - break; - } + return pushStartAnchor(target, props); case 'g': case 'p': case 'li': diff --git a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js index ab36fc11199b1..6dd4e4da44479 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMFormActions.js @@ -10,7 +10,6 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type {Awaited} from 'shared/ReactTypes'; -import {enableAsyncActions} from 'shared/ReactFeatureFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals'; @@ -66,13 +65,8 @@ function resolveDispatcher() { } export function useFormStatus(): FormStatus { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } else { - const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] We know this exists because of the feature check above. - return dispatcher.useHostTransitionStatus(); - } + const dispatcher = resolveDispatcher(); + return dispatcher.useHostTransitionStatus(); } export function useFormState( @@ -80,13 +74,8 @@ export function useFormState( initialState: Awaited, permalink?: string, ): [Awaited, (P) => void, boolean] { - if (!enableAsyncActions) { - throw new Error('Not implemented.'); - } else { - const dispatcher = resolveDispatcher(); - // $FlowFixMe[not-a-function] This is unstable, thus optional - return dispatcher.useFormState(action, initialState, permalink); - } + const dispatcher = resolveDispatcher(); + return dispatcher.useFormState(action, initialState, permalink); } export function requestFormReset(form: HTMLFormElement) { diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index d37a4ecba6dc6..ce71a6334ee64 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -587,133 +587,131 @@ describe('ReactDOMComponent', () => { expect(node.hasAttribute('data-foo')).toBe(false); }); - if (ReactFeatureFlags.enableFilterEmptyStringAttributesDOM) { - it('should not add an empty src attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('src')).toBe(false); - + it('should not add an empty src attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('src')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('src')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the src attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to src instead of an empty string.', - ); - expect(node.hasAttribute('src')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('src')).toBe(true); - it('should not add an empty href attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - const node = container.firstChild; - expect(node.hasAttribute('href')).toBe(false); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev( + 'An empty string ("") was passed to the src attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to src instead of an empty string.', + ); + expect(node.hasAttribute('src')).toBe(false); + }); + it('should not add an empty href attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - expect(node.hasAttribute('href')).toBe(true); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + const node = container.firstChild; + expect(node.hasAttribute('href')).toBe(false); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'An empty string ("") was passed to the href attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to href instead of an empty string.', - ); - expect(node.hasAttribute('href')).toBe(false); + await act(() => { + root.render(); }); + expect(node.hasAttribute('href')).toBe(true); - it('should allow an empty href attribute on anchors', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + await expect(async () => { await act(() => { - root.render(); + root.render(); }); - const node = container.firstChild; - expect(node.getAttribute('href')).toBe(''); + }).toErrorDev( + 'An empty string ("") was passed to the href attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to href instead of an empty string.', + ); + expect(node.hasAttribute('href')).toBe(false); + }); + + it('should allow an empty href attribute on anchors', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); }); + const node = container.firstChild; + expect(node.getAttribute('href')).toBe(''); + }); - it('should allow an empty action attribute', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(
); - }); - const node = container.firstChild; - expect(node.getAttribute('action')).toBe(''); + it('should allow an empty action attribute', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + const node = container.firstChild; + expect(node.getAttribute('action')).toBe(''); - await act(() => { - root.render(); - }); - expect(node.hasAttribute('action')).toBe(true); + await act(() => { + root.render(); + }); + expect(node.hasAttribute('action')).toBe(true); - await act(() => { - root.render(); - }); - expect(node.getAttribute('action')).toBe(''); + await act(() => { + root.render(); }); + expect(node.getAttribute('action')).toBe(''); + }); - it('allows empty string of a formAction to override the default of a parent', async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - -