diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
index a25d8f9eff4c..5df5d8b294a0 100644
--- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js
@@ -445,6 +445,111 @@ describe('ReactDOMComponent', () => {
expect(node.hasAttribute('data-foo')).toBe(false);
});
+ if (ReactFeatureFlags.enableFilterEmptyStringAttributesDOM) {
+ it('should not add an empty src attribute', () => {
+ const container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).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);
+
+ ReactDOM.render(, container);
+ expect(node.hasAttribute('src')).toBe(true);
+
+ expect(() => ReactDOM.render(, container)).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', () => {
+ const container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).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);
+
+ ReactDOM.render(, container);
+ expect(node.hasAttribute('href')).toBe(true);
+
+ expect(() => ReactDOM.render(, container)).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 not add an empty action attribute', () => {
+ const container = document.createElement('div');
+ expect(() => ReactDOM.render(
, container)).toErrorDev(
+ 'An empty string ("") was passed to the action attribute. ' +
+ 'To fix this, either do not render the element at all ' +
+ 'or pass null to action instead of an empty string.',
+ );
+ const node = container.firstChild;
+ expect(node.hasAttribute('action')).toBe(false);
+
+ ReactDOM.render(, container);
+ expect(node.hasAttribute('action')).toBe(true);
+
+ expect(() => ReactDOM.render(, container)).toErrorDev(
+ 'An empty string ("") was passed to the action attribute. ' +
+ 'To fix this, either do not render the element at all ' +
+ 'or pass null to action instead of an empty string.',
+ );
+ expect(node.hasAttribute('action')).toBe(false);
+ });
+
+ it('should not add an empty formAction attribute', () => {
+ const container = document.createElement('div');
+ expect(() =>
+ ReactDOM.render(, container),
+ ).toErrorDev(
+ 'An empty string ("") was passed to the formAction attribute. ' +
+ 'To fix this, either do not render the element at all ' +
+ 'or pass null to formAction instead of an empty string.',
+ );
+ const node = container.firstChild;
+ expect(node.hasAttribute('formAction')).toBe(false);
+
+ ReactDOM.render(, container);
+ expect(node.hasAttribute('formAction')).toBe(true);
+
+ expect(() =>
+ ReactDOM.render(, container),
+ ).toErrorDev(
+ 'An empty string ("") was passed to the formAction attribute. ' +
+ 'To fix this, either do not render the element at all ' +
+ 'or pass null to formAction instead of an empty string.',
+ );
+ expect(node.hasAttribute('formAction')).toBe(false);
+ });
+
+ it('should not filter attributes for custom elements', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+ ,
+ container,
+ );
+ const node = container.firstChild;
+ expect(node.hasAttribute('action')).toBe(true);
+ expect(node.hasAttribute('formAction')).toBe(true);
+ expect(node.hasAttribute('href')).toBe(true);
+ expect(node.hasAttribute('src')).toBe(true);
+ });
+ }
+
it('should apply React-specific aliases to HTML elements', () => {
const container = document.createElement('div');
ReactDOM.render(, container);
diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js
index 1eb36ead8416..68a2188b9850 100644
--- a/packages/react-dom/src/shared/DOMProperty.js
+++ b/packages/react-dom/src/shared/DOMProperty.js
@@ -7,7 +7,10 @@
* @flow
*/
-import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
+import {
+ enableDeprecatedFlareAPI,
+ enableFilterEmptyStringAttributesDOM,
+} from 'shared/ReactFeatureFlags';
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -52,6 +55,7 @@ export type PropertyInfo = {|
+propertyName: string,
+type: PropertyType,
+sanitizeURL: boolean,
+ +removeEmptyString: boolean,
|};
/* eslint-disable max-len */
@@ -163,6 +167,32 @@ export function shouldRemoveAttribute(
return false;
}
if (propertyInfo !== null) {
+ if (enableFilterEmptyStringAttributesDOM) {
+ if (propertyInfo.removeEmptyString && 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 true;
+ }
+ }
+
switch (propertyInfo.type) {
case BOOLEAN:
return !value;
@@ -188,6 +218,7 @@ function PropertyInfoRecord(
attributeName: string,
attributeNamespace: string | null,
sanitizeURL: boolean,
+ removeEmptyString: boolean,
) {
this.acceptsBooleans =
type === BOOLEANISH_STRING ||
@@ -199,6 +230,7 @@ function PropertyInfoRecord(
this.propertyName = name;
this.type = type;
this.sanitizeURL = sanitizeURL;
+ this.removeEmptyString = removeEmptyString;
}
// When adding attributes to this list, be sure to also add them to
@@ -232,6 +264,7 @@ reservedProps.forEach(name => {
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -250,6 +283,7 @@ reservedProps.forEach(name => {
attributeName, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -264,6 +298,7 @@ reservedProps.forEach(name => {
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -284,6 +319,7 @@ reservedProps.forEach(name => {
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -322,6 +358,7 @@ reservedProps.forEach(name => {
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -346,6 +383,7 @@ reservedProps.forEach(name => {
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -366,6 +404,7 @@ reservedProps.forEach(name => {
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -387,6 +426,7 @@ reservedProps.forEach(name => {
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -399,6 +439,7 @@ reservedProps.forEach(name => {
name.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -497,6 +538,7 @@ const capitalize = token => token[1].toUpperCase();
attributeName,
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -521,6 +563,7 @@ const capitalize = token => token[1].toUpperCase();
attributeName,
'http://www.w3.org/1999/xlink',
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -542,6 +585,7 @@ const capitalize = token => token[1].toUpperCase();
attributeName,
'http://www.w3.org/XML/1998/namespace',
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -556,6 +600,7 @@ const capitalize = token => token[1].toUpperCase();
attributeName.toLowerCase(), // attributeName
null, // attributeNamespace
false, // sanitizeURL
+ false, // removeEmptyString
);
});
@@ -569,6 +614,7 @@ properties[xlinkHref] = new PropertyInfoRecord(
'xlink:href',
'http://www.w3.org/1999/xlink',
true, // sanitizeURL
+ false, // removeEmptyString
);
['src', 'href', 'action', 'formAction'].forEach(attributeName => {
@@ -579,5 +625,6 @@ properties[xlinkHref] = new PropertyInfoRecord(
attributeName.toLowerCase(), // attributeName
null, // attributeNamespace
true, // sanitizeURL
+ true, // removeEmptyString
);
});
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index b25726f4a8c6..529965c9db6f 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -7,6 +7,10 @@
* @flow strict
*/
+// Filter certain DOM attributes (e.g. src, href) if their values are empty strings.
+// This prevents e.g. from making an unnecessar HTTP request for certain browsers.
+export const enableFilterEmptyStringAttributesDOM = false;
+
// Helps identify side effects in render-phase lifecycle hooks and setState
// reducers by double invoking them in Strict Mode.
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 1c3813dee899..ce023f9841e6 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -44,6 +44,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 233f230a8d25..b05a3f6c2c26 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -43,6 +43,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 707488ccd209..23f92629eee9 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -43,6 +43,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index 89a6e38c7eba..fb4001be9ed3 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -43,6 +43,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index da8719641dc5..e0b82d94373f 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -43,6 +43,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index 27d821bc6aa3..f49e7ed3367d 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -43,6 +43,7 @@ export const enableModernEventSystem = false;
export const warnAboutSpreadingKeyToJSX = false;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
+export const enableFilterEmptyStringAttributesDOM = false;
// Internal-only attempt to debug a React Native issue. See D20130868.
export const throwEarlyForMysteriousError = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index d67bf6be88bb..b7e987cbd513 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -17,6 +17,7 @@ export const warnAboutSpreadingKeyToJSX = __VARIANT__;
export const enableComponentStackLocations = __VARIANT__;
export const disableModulePatternComponents = __VARIANT__;
export const disableInputAttributeSyncing = __VARIANT__;
+export const enableFilterEmptyStringAttributesDOM = __VARIANT__;
// These are already tested in both modes using the build type dimension,
// so we don't need to use __VARIANT__ to get extra coverage.
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index f2f53c6c3ba3..47e006820eff 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -24,6 +24,7 @@ export const {
enableComponentStackLocations,
replayFailedUnitOfWorkWithInvokeGuardedCallback,
enableModernEventSystem,
+ enableFilterEmptyStringAttributesDOM,
} = dynamicFeatureFlags;
// On WWW, __EXPERIMENTAL__ is used for a new modern build.