Skip to content

Commit 06d07c0

Browse files
author
Brian Vaughn
committed
Filter certain DOM attributes (e.g. src, href) if their values are empty strings
This prevents e.g. <img src=""> from making an unnecessar HTTP request for certain browsers.
1 parent c781156 commit 06d07c0

12 files changed

Lines changed: 122 additions & 1 deletion

packages/react-dom/src/__tests__/ReactDOMComponent-test.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,60 @@ describe('ReactDOMComponent', () => {
445445
expect(node.hasAttribute('data-foo')).toBe(false);
446446
});
447447

448+
if (ReactFeatureFlags.enableAttributeEmptyStringFilter) {
449+
it('should not add an empty src attribute', () => {
450+
const container = document.createElement('div');
451+
ReactDOM.render(<img src="" />, container);
452+
const node = container.firstChild;
453+
expect(node.hasAttribute('src')).toBe(false);
454+
455+
ReactDOM.render(<img src="abc" />, container);
456+
expect(node.hasAttribute('src')).toBe(true);
457+
458+
ReactDOM.render(<img src="" />, container);
459+
expect(node.hasAttribute('src')).toBe(false);
460+
});
461+
462+
it('should not add an empty href attribute', () => {
463+
const container = document.createElement('div');
464+
ReactDOM.render(<link href="" />, container);
465+
const node = container.firstChild;
466+
expect(node.hasAttribute('href')).toBe(false);
467+
468+
ReactDOM.render(<link href="abc" />, container);
469+
expect(node.hasAttribute('href')).toBe(true);
470+
471+
ReactDOM.render(<link href="" />, container);
472+
expect(node.hasAttribute('href')).toBe(false);
473+
});
474+
475+
it('should not add an empty action attribute', () => {
476+
const container = document.createElement('div');
477+
ReactDOM.render(<form action="" />, container);
478+
const node = container.firstChild;
479+
expect(node.hasAttribute('action')).toBe(false);
480+
481+
ReactDOM.render(<form action="abc" />, container);
482+
expect(node.hasAttribute('action')).toBe(true);
483+
484+
ReactDOM.render(<form action="" />, container);
485+
expect(node.hasAttribute('action')).toBe(false);
486+
});
487+
488+
it('should not add an empty formAction attribute', () => {
489+
const container = document.createElement('div');
490+
ReactDOM.render(<button formAction="" />, container);
491+
const node = container.firstChild;
492+
expect(node.hasAttribute('formAction')).toBe(false);
493+
494+
ReactDOM.render(<button formAction="abc" />, container);
495+
expect(node.hasAttribute('formAction')).toBe(true);
496+
497+
ReactDOM.render(<button formAction="" />, container);
498+
expect(node.hasAttribute('formAction')).toBe(false);
499+
});
500+
}
501+
448502
it('should apply React-specific aliases to HTML elements', () => {
449503
const container = document.createElement('div');
450504
ReactDOM.render(<form acceptCharset="foo" />, container);

packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'use strict';
1111

1212
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13+
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
1314

1415
let React;
1516
let ReactDOM;
@@ -331,6 +332,30 @@ describe('ReactDOMServerIntegration', () => {
331332
});
332333
});
333334

335+
if (ReactFeatureFlags.enableAttributeEmptyStringFilter) {
336+
describe('special empty string property', function() {
337+
itRenders('action prop', async render => {
338+
const e = await render(<form action="" />);
339+
expect(e.hasAttribute('action')).toBe(false);
340+
});
341+
342+
itRenders('formAction prop', async render => {
343+
const e = await render(<button formAction="" />);
344+
expect(e.hasAttribute('formAction')).toBe(false);
345+
});
346+
347+
itRenders('href prop', async render => {
348+
const e = await render(<link href="" />);
349+
expect(e.hasAttribute('href')).toBe(false);
350+
});
351+
352+
itRenders('src prop', async render => {
353+
const e = await render(<img src="" />);
354+
expect(e.hasAttribute('src')).toBe(false);
355+
});
356+
});
357+
}
358+
334359
describe('props with special meaning in React', function() {
335360
itRenders('no ref attribute', async render => {
336361
class RefComponent extends React.Component {

packages/react-dom/src/shared/DOMProperty.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
* @flow
88
*/
99

10-
import {enableDeprecatedFlareAPI} from 'shared/ReactFeatureFlags';
10+
import {
11+
enableDeprecatedFlareAPI,
12+
enableFilterEmptyStringAttributesDOM,
13+
} from 'shared/ReactFeatureFlags';
1114

1215
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
1316

@@ -52,6 +55,7 @@ export type PropertyInfo = {|
5255
+propertyName: string,
5356
+type: PropertyType,
5457
+sanitizeURL: boolean,
58+
+removeEmptyString: boolean,
5559
|};
5660

5761
/* eslint-disable max-len */
@@ -146,6 +150,15 @@ export function shouldRemoveAttribute(
146150
propertyInfo: PropertyInfo | null,
147151
isCustomComponentTag: boolean,
148152
): boolean {
153+
if (enableFilterEmptyStringAttributesDOM) {
154+
if (
155+
propertyInfo !== null &&
156+
propertyInfo.removeEmptyString === true &&
157+
value === ''
158+
) {
159+
return true;
160+
}
161+
}
149162
if (value === null || typeof value === 'undefined') {
150163
return true;
151164
}
@@ -188,6 +201,7 @@ function PropertyInfoRecord(
188201
attributeName: string,
189202
attributeNamespace: string | null,
190203
sanitizeURL: boolean,
204+
removeEmptyString: boolean,
191205
) {
192206
this.acceptsBooleans =
193207
type === BOOLEANISH_STRING ||
@@ -199,6 +213,7 @@ function PropertyInfoRecord(
199213
this.propertyName = name;
200214
this.type = type;
201215
this.sanitizeURL = sanitizeURL;
216+
this.removeEmptyString = removeEmptyString;
202217
}
203218

204219
// When adding attributes to this list, be sure to also add them to
@@ -232,6 +247,7 @@ reservedProps.forEach(name => {
232247
name, // attributeName
233248
null, // attributeNamespace
234249
false, // sanitizeURL
250+
false, // removeEmptyString
235251
);
236252
});
237253

@@ -250,6 +266,7 @@ reservedProps.forEach(name => {
250266
attributeName, // attributeName
251267
null, // attributeNamespace
252268
false, // sanitizeURL
269+
false, // removeEmptyString
253270
);
254271
});
255272

@@ -264,6 +281,7 @@ reservedProps.forEach(name => {
264281
name.toLowerCase(), // attributeName
265282
null, // attributeNamespace
266283
false, // sanitizeURL
284+
false, // removeEmptyString
267285
);
268286
});
269287

@@ -284,6 +302,7 @@ reservedProps.forEach(name => {
284302
name, // attributeName
285303
null, // attributeNamespace
286304
false, // sanitizeURL
305+
false, // removeEmptyString
287306
);
288307
});
289308

@@ -322,6 +341,7 @@ reservedProps.forEach(name => {
322341
name.toLowerCase(), // attributeName
323342
null, // attributeNamespace
324343
false, // sanitizeURL
344+
false, // removeEmptyString
325345
);
326346
});
327347

@@ -346,6 +366,7 @@ reservedProps.forEach(name => {
346366
name, // attributeName
347367
null, // attributeNamespace
348368
false, // sanitizeURL
369+
false, // removeEmptyString
349370
);
350371
});
351372

@@ -366,6 +387,7 @@ reservedProps.forEach(name => {
366387
name, // attributeName
367388
null, // attributeNamespace
368389
false, // sanitizeURL
390+
false, // removeEmptyString
369391
);
370392
});
371393

@@ -387,6 +409,7 @@ reservedProps.forEach(name => {
387409
name, // attributeName
388410
null, // attributeNamespace
389411
false, // sanitizeURL
412+
false, // removeEmptyString
390413
);
391414
});
392415

@@ -399,6 +422,7 @@ reservedProps.forEach(name => {
399422
name.toLowerCase(), // attributeName
400423
null, // attributeNamespace
401424
false, // sanitizeURL
425+
false, // removeEmptyString
402426
);
403427
});
404428

@@ -497,6 +521,7 @@ const capitalize = token => token[1].toUpperCase();
497521
attributeName,
498522
null, // attributeNamespace
499523
false, // sanitizeURL
524+
false, // removeEmptyString
500525
);
501526
});
502527

@@ -521,6 +546,7 @@ const capitalize = token => token[1].toUpperCase();
521546
attributeName,
522547
'http://www.w3.org/1999/xlink',
523548
false, // sanitizeURL
549+
false, // removeEmptyString
524550
);
525551
});
526552

@@ -542,6 +568,7 @@ const capitalize = token => token[1].toUpperCase();
542568
attributeName,
543569
'http://www.w3.org/XML/1998/namespace',
544570
false, // sanitizeURL
571+
false, // removeEmptyString
545572
);
546573
});
547574

@@ -556,6 +583,7 @@ const capitalize = token => token[1].toUpperCase();
556583
attributeName.toLowerCase(), // attributeName
557584
null, // attributeNamespace
558585
false, // sanitizeURL
586+
false, // removeEmptyString
559587
);
560588
});
561589

@@ -569,6 +597,7 @@ properties[xlinkHref] = new PropertyInfoRecord(
569597
'xlink:href',
570598
'http://www.w3.org/1999/xlink',
571599
true, // sanitizeURL
600+
false, // removeEmptyString
572601
);
573602

574603
['src', 'href', 'action', 'formAction'].forEach(attributeName => {
@@ -579,5 +608,6 @@ properties[xlinkHref] = new PropertyInfoRecord(
579608
attributeName.toLowerCase(), // attributeName
580609
null, // attributeNamespace
581610
true, // sanitizeURL
611+
true, // removeEmptyString
582612
);
583613
});

packages/shared/ReactFeatureFlags.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* @flow strict
88
*/
99

10+
// Filter certain DOM attributes (e.g. src, href) if their values are empty strings.
11+
// This prevents e.g. <img src=""> from making an unnecessar HTTP request for certain browsers.
12+
export const enableFilterEmptyStringAttributesDOM = false;
13+
1014
// Helps identify side effects in render-phase lifecycle hooks and setState
1115
// reducers by double invoking them in Strict Mode.
1216
export const debugRenderPhaseSideEffectsForStrictMode = __DEV__;

packages/shared/forks/ReactFeatureFlags.native-fb.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = false;
4343
export const enableModernEventSystem = false;
4444
export const warnAboutSpreadingKeyToJSX = false;
4545
export const enableLegacyFBSupport = false;
46+
export const enableFilterEmptyStringAttributesDOM = false;
4647

4748
// Internal-only attempt to debug a React Native issue. See D20130868.
4849
export const throwEarlyForMysteriousError = true;

packages/shared/forks/ReactFeatureFlags.native-oss.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = false;
4242
export const enableModernEventSystem = false;
4343
export const warnAboutSpreadingKeyToJSX = false;
4444
export const enableLegacyFBSupport = false;
45+
export const enableFilterEmptyStringAttributesDOM = false;
4546

4647
// Internal-only attempt to debug a React Native issue. See D20130868.
4748
export const throwEarlyForMysteriousError = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = false;
4242
export const enableModernEventSystem = false;
4343
export const warnAboutSpreadingKeyToJSX = false;
4444
export const enableLegacyFBSupport = false;
45+
export const enableFilterEmptyStringAttributesDOM = false;
4546

4647
// Internal-only attempt to debug a React Native issue. See D20130868.
4748
export const throwEarlyForMysteriousError = false;

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = true;
4242
export const enableModernEventSystem = false;
4343
export const warnAboutSpreadingKeyToJSX = false;
4444
export const enableLegacyFBSupport = false;
45+
export const enableFilterEmptyStringAttributesDOM = false;
4546

4647
// Internal-only attempt to debug a React Native issue. See D20130868.
4748
export const throwEarlyForMysteriousError = false;

packages/shared/forks/ReactFeatureFlags.testing.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = false;
4242
export const enableModernEventSystem = false;
4343
export const warnAboutSpreadingKeyToJSX = false;
4444
export const enableLegacyFBSupport = false;
45+
export const enableFilterEmptyStringAttributesDOM = false;
4546

4647
// Internal-only attempt to debug a React Native issue. See D20130868.
4748
export const throwEarlyForMysteriousError = false;

packages/shared/forks/ReactFeatureFlags.testing.www.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const runAllPassiveEffectDestroysBeforeCreates = true;
4242
export const enableModernEventSystem = false;
4343
export const warnAboutSpreadingKeyToJSX = false;
4444
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
45+
export const enableFilterEmptyStringAttributesDOM = false;
4546

4647
// Internal-only attempt to debug a React Native issue. See D20130868.
4748
export const throwEarlyForMysteriousError = false;

0 commit comments

Comments
 (0)