Skip to content

Commit 61c2448

Browse files
eps1longaearon
authored andcommitted
Restore old behavior for empty href props on anchor tags (#28124)
Treat `<a href="" />` the same with and without `enableFilterEmptyStringAttributesDOM` in #18513 we started to warn and ignore for empty `href` and `src` props since it usually hinted at a mistake. However, for anchor tags there's a valid use case since `<a href=""></a>` will by spec render a link to the current page. It could be used to reload the page without having to rely on browser affordances. The implementation for Fizz is in the spirit of #21153. I gated the fork behind the flag so that the fork is DCE'd when the flag is off.
1 parent 580e408 commit 61c2448

File tree

5 files changed

+108
-3
lines changed

5 files changed

+108
-3
lines changed

fixtures/attribute-behavior/AttributeTableSnapshot.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5077,7 +5077,7 @@
50775077
| Test Case | Flags | Result |
50785078
| --- | --- | --- |
50795079
| `href=(string)`| (changed)| `"https://reactjs.com/"` |
5080-
| `href=(empty string)`| (initial, warning)| `<empty string>` |
5080+
| `href=(empty string)`| (changed)| `"http://localhost:3000/"` |
50815081
| `href=(array with string)`| (changed)| `"https://reactjs.com/"` |
50825082
| `href=(empty array)`| (changed)| `"http://localhost:3000/"` |
50835083
| `href=(object)`| (changed)| `"http://localhost:3000/result%20of%20toString()"` |

packages/react-dom-bindings/src/client/ReactDOMComponent.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,11 @@ function setProp(
434434
case 'src':
435435
case 'href': {
436436
if (enableFilterEmptyStringAttributesDOM) {
437-
if (value === '') {
437+
if (
438+
value === '' &&
439+
// <a href=""> is fine for "reload" links.
440+
!(tag === 'a' && key === 'href')
441+
) {
438442
if (__DEV__) {
439443
if (key === 'src') {
440444
console.error(
@@ -2350,7 +2354,11 @@ function diffHydratedGenericElement(
23502354
case 'src':
23512355
case 'href':
23522356
if (enableFilterEmptyStringAttributesDOM) {
2353-
if (value === '') {
2357+
if (
2358+
value === '' &&
2359+
// <a href=""> is fine for "reload" links.
2360+
!(tag === 'a' && propKey === 'href')
2361+
) {
23542362
if (__DEV__) {
23552363
if (propKey === 'src') {
23562364
console.error(

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,54 @@ function checkSelectProp(props: any, propName: string) {
15031503
}
15041504
}
15051505

1506+
function pushStartAnchor(
1507+
target: Array<Chunk | PrecomputedChunk>,
1508+
props: Object,
1509+
): ReactNodeList {
1510+
target.push(startChunkForTag('a'));
1511+
1512+
let children = null;
1513+
let innerHTML = null;
1514+
for (const propKey in props) {
1515+
if (hasOwnProperty.call(props, propKey)) {
1516+
const propValue = props[propKey];
1517+
if (propValue == null) {
1518+
continue;
1519+
}
1520+
switch (propKey) {
1521+
case 'children':
1522+
children = propValue;
1523+
break;
1524+
case 'dangerouslySetInnerHTML':
1525+
innerHTML = propValue;
1526+
break;
1527+
case 'href':
1528+
if (propValue === '') {
1529+
// Empty `href` is special on anchors so we're short-circuiting here.
1530+
// On other tags it should trigger a warning
1531+
pushStringAttribute(target, 'href', '');
1532+
} else {
1533+
pushAttribute(target, propKey, propValue);
1534+
}
1535+
break;
1536+
default:
1537+
pushAttribute(target, propKey, propValue);
1538+
break;
1539+
}
1540+
}
1541+
}
1542+
1543+
target.push(endOfStartTag);
1544+
pushInnerHTML(target, innerHTML, children);
1545+
if (typeof children === 'string') {
1546+
// Special case children as a string to avoid the unnecessary comment.
1547+
// TODO: Remove this special case after the general optimization is in place.
1548+
target.push(stringToChunk(encodeHTMLTextNode(children)));
1549+
return null;
1550+
}
1551+
return children;
1552+
}
1553+
15061554
function pushStartSelect(
15071555
target: Array<Chunk | PrecomputedChunk>,
15081556
props: Object,
@@ -3535,7 +3583,14 @@ export function pushStartInstance(
35353583
case 'span':
35363584
case 'svg':
35373585
case 'path':
3586+
// Fast track very common tags
3587+
break;
35383588
case 'a':
3589+
if (enableFilterEmptyStringAttributesDOM) {
3590+
return pushStartAnchor(target, props);
3591+
} else {
3592+
break;
3593+
}
35393594
case 'g':
35403595
case 'p':
35413596
case 'li':

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,16 @@ describe('ReactDOMComponent', () => {
640640
expect(node.hasAttribute('href')).toBe(false);
641641
});
642642

643+
it('should allow an empty href attribute on anchors', async () => {
644+
const container = document.createElement('div');
645+
const root = ReactDOMClient.createRoot(container);
646+
await act(() => {
647+
root.render(<a href="" />);
648+
});
649+
const node = container.firstChild;
650+
expect(node.getAttribute('href')).toBe('');
651+
});
652+
643653
it('should allow an empty action attribute', async () => {
644654
const container = document.createElement('div');
645655
const root = ReactDOMClient.createRoot(container);

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ describe('ReactDOMServerIntegration', () => {
5454
expect(e.getAttribute('width')).toBe('30');
5555
});
5656

57+
itRenders('empty src on img', async render => {
58+
const e = await render(
59+
<img src="" />,
60+
ReactFeatureFlags.enableFilterEmptyStringAttributesDOM ? 1 : 0,
61+
);
62+
expect(e.getAttribute('src')).toBe(
63+
ReactFeatureFlags.enableFilterEmptyStringAttributesDOM ? null : '',
64+
);
65+
});
66+
67+
itRenders('empty href on anchor', async render => {
68+
const e = await render(<a href="" />);
69+
expect(e.getAttribute('href')).toBe('');
70+
});
71+
72+
itRenders('empty href on other tags', async render => {
73+
const e = await render(
74+
// <link href="" /> would be more sensible.
75+
// However, that results in a hydration warning as well.
76+
// Our test helpers do not support different error counts for initial
77+
// server render and hydration.
78+
// The number of errors on the server need to be equal to the number of
79+
// errors during hydration.
80+
// So we use a <div> instead.
81+
<div href="" />,
82+
ReactFeatureFlags.enableFilterEmptyStringAttributesDOM ? 1 : 0,
83+
);
84+
expect(e.getAttribute('href')).toBe(
85+
ReactFeatureFlags.enableFilterEmptyStringAttributesDOM ? null : '',
86+
);
87+
});
88+
5789
itRenders('no string prop with true value', async render => {
5890
const e = await render(<a href={true} />, 1);
5991
expect(e.hasAttribute('href')).toBe(false);

0 commit comments

Comments
 (0)