@samitouri / QOS-React-2 / commits / 520f7f3ed4

Refactor ReactDOMComponent to use flatter property operations (#26433)

This is in line with the refactor I already did on Fizz earlier and brings Fiber up to a similar structure. We end up with a lot of extra checks due the extra abstractions we use to check the various properties. This uses a flatter and more inline model which makes it easier to see what each property does. The tradeoff is that a change might need changes in more places. The general structure is that there's a switch for tag first, then a switch for each attribute special case, then a switch for the value. So it's easy to follow where each scenario will end up and there shouldn't be any unnecessary code executed along the way. My goal is to eventually get rid of the meta-programming in DOMProperty and CSSProperty but I'm leaving that in for now - in line with Fizz. My next step is moving around things a bit in the diff/commit phases. This is the first step to more refactors for perf and size, but also because I'm adding more special cases so I need to have a flatter structure that I can reason about for those special cases.

Sebastian Markbåge committed Mar 20, 2023 at 20:56 UTC 520f7f3ed4d01e9e50a73d7d04ff138e3c71ac86
12 files changed +1177 -890
packages/react-dom-bindings/src/client/CSSPropertyOperations.js
+62 -26
@@ -7,9 +7,10 @@
7
8 import {shorthandToLonghand} from './CSSShorthandProperty';
9
10 -import dangerousStyleValue from './dangerousStyleValue';
10 import hyphenateStyleName from '../shared/hyphenateStyleName';
11 import warnValidStyle from '../shared/warnValidStyle';
12 +import {isUnitlessNumber} from '../shared/CSSProperty';
13 +import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
14
15 /**
16 * Operations for dealing with CSS properties.
@@ -29,19 +30,36 @@ export function createDangerousStringForStyles(styles) {
30 if (!styles.hasOwnProperty(styleName)) {
31 continue;
32 }
32 - const styleValue = styles[styleName];
33 - if (styleValue != null) {
33 + const value = styles[styleName];
34 + if (value != null && typeof value !== 'boolean' && value !== '') {
35 const isCustomProperty = styleName.indexOf('--') === 0;
35 - serialized +=
36 - delimiter +
37 - (isCustomProperty ? styleName : hyphenateStyleName(styleName)) +
38 - ':';
39 - serialized += dangerousStyleValue(
40 - styleName,
41 - styleValue,
42 - isCustomProperty,
43 - );
44 -
36 + if (isCustomProperty) {
37 + if (__DEV__) {
38 + checkCSSPropertyStringCoercion(value, styleName);
39 + }
40 + serialized += delimiter + styleName + ':' + ('' + value).trim();
41 + } else {
42 + if (
43 + typeof value === 'number' &&
44 + value !== 0 &&
45 + !(
46 + isUnitlessNumber.hasOwnProperty(styleName) &&
47 + isUnitlessNumber[styleName]
48 + )
49 + ) {
50 + serialized +=
51 + delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
52 + } else {
53 + if (__DEV__) {
54 + checkCSSPropertyStringCoercion(value, styleName);
55 + }
56 + serialized +=
57 + delimiter +
58 + hyphenateStyleName(styleName) +
59 + ':' +
60 + ('' + value).trim();
61 + }
62 + }
63 delimiter = ';';
64 }
65 }
@@ -58,28 +76,46 @@ export function createDangerousStringForStyles(styles) {
76 */
77 export function setValueForStyles(node, styles) {
78 const style = node.style;
61 - for (let styleName in styles) {
79 + for (const styleName in styles) {
80 if (!styles.hasOwnProperty(styleName)) {
81 continue;
82 }
83 + const value = styles[styleName];
84 const isCustomProperty = styleName.indexOf('--') === 0;
85 if (__DEV__) {
86 if (!isCustomProperty) {
68 - warnValidStyle(styleName, styles[styleName]);
87 + warnValidStyle(styleName, value);
88 }
89 }
71 - const styleValue = dangerousStyleValue(
72 - styleName,
73 - styles[styleName],
74 - isCustomProperty,
75 - );
76 - if (styleName === 'float') {
77 - styleName = 'cssFloat';
78 - }
79 - if (isCustomProperty) {
80 - style.setProperty(styleName, styleValue);
90 +
91 + if (value == null || typeof value === 'boolean' || value === '') {
92 + if (isCustomProperty) {
93 + style.setProperty(styleName, '');
94 + } else if (styleName === 'float') {
95 + style.cssFloat = '';
96 + } else {
97 + style[styleName] = '';
98 + }
99 + } else if (isCustomProperty) {
100 + style.setProperty(styleName, value);
101 + } else if (
102 + typeof value === 'number' &&
103 + value !== 0 &&
104 + !(
105 + isUnitlessNumber.hasOwnProperty(styleName) &&
106 + isUnitlessNumber[styleName]
107 + )
108 + ) {
109 + style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
110 } else {
82 - style[styleName] = styleValue;
111 + if (styleName === 'float') {
112 + style.cssFloat = value;
113 + } else {
114 + if (__DEV__) {
115 + checkCSSPropertyStringCoercion(value, styleName);
116 + }
117 + style[styleName] = ('' + value).trim();
118 + }
119 }
120 }
121 }
packages/react-dom-bindings/src/client/DOMPropertyOperations.js
+379 -130
@@ -9,17 +9,18 @@
9
10 import {
11 getPropertyInfo,
12 - shouldIgnoreAttribute,
13 - shouldRemoveAttribute,
12 isAttributeNameSafe,
13 BOOLEAN,
14 OVERLOADED_BOOLEAN,
15 + NUMERIC,
16 + POSITIVE_NUMERIC,
17 } from '../shared/DOMProperty';
18 import sanitizeURL from '../shared/sanitizeURL';
19 import {
20 disableJavaScriptURLs,
21 enableTrustedTypesIntegration,
22 enableCustomElementPropertySupport,
23 + enableFilterEmptyStringAttributesDOM,
24 } from 'shared/ReactFeatureFlags';
25 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
26 import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
@@ -41,68 +42,143 @@ export function getValueForProperty(
42 if (propertyInfo.mustUseProperty) {
43 const {propertyName} = propertyInfo;
44 return (node: any)[propertyName];
44 - } else {
45 - // This check protects multiple uses of `expected`, which is why the
46 - // react-internal/safe-string-coercion rule is disabled in several spots
47 - // below.
45 + }
46 + if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
47 + // If we haven't fully disabled javascript: URLs, and if
48 + // the hydration is successful of a javascript: URL, we
49 + // still want to warn on the client.
50 if (__DEV__) {
51 checkAttributeStringCoercion(expected, name);
52 }
53 + sanitizeURL('' + (expected: any));
54 + }
55
52 - if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
53 - // If we haven't fully disabled javascript: URLs, and if
54 - // the hydration is successful of a javascript: URL, we
55 - // still want to warn on the client.
56 - // eslint-disable-next-line react-internal/safe-string-coercion
57 - sanitizeURL('' + (expected: any));
58 - }
59 -
60 - const attributeName = propertyInfo.attributeName;
61 -
62 - let stringValue = null;
56 + const attributeName = propertyInfo.attributeName;
57
64 - if (propertyInfo.type === OVERLOADED_BOOLEAN) {
65 - if (node.hasAttribute(attributeName)) {
66 - const value = node.getAttribute(attributeName);
67 - if (value === '') {
68 - return true;
58 + if (!node.hasAttribute(attributeName)) {
59 + // shouldRemoveAttribute
60 + switch (typeof expected) {
61 + case 'function':
62 + case 'symbol': // eslint-disable-line
63 + return expected;
64 + case 'boolean': {
65 + if (!propertyInfo.acceptsBooleans) {
66 + return expected;
67 }
70 - if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
71 - return value;
68 + }
69 + }
70 + switch (propertyInfo.type) {
71 + case BOOLEAN: {
72 + if (!expected) {
73 + return expected;
74 }
73 - // eslint-disable-next-line react-internal/safe-string-coercion
74 - if (value === '' + (expected: any)) {
75 + break;
76 + }
77 + case OVERLOADED_BOOLEAN: {
78 + if (expected === false) {
79 return expected;
80 }
77 - return value;
81 + break;
82 }
79 - } else if (node.hasAttribute(attributeName)) {
80 - if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
81 - // We had an attribute but shouldn't have had one, so read it
82 - // for the error message.
83 - return node.getAttribute(attributeName);
83 + case NUMERIC: {
84 + if (isNaN(expected)) {
85 + return expected;
86 + }
87 + break;
88 + }
89 + case POSITIVE_NUMERIC: {
90 + if (isNaN(expected) || (expected: any) < 1) {
91 + return expected;
92 + }
93 + break;
94 + }
95 + }
96 + if (enableFilterEmptyStringAttributesDOM) {
97 + if (propertyInfo.removeEmptyString && expected === '') {
98 + if (__DEV__) {
99 + if (name === 'src') {
100 + console.error(
101 + 'An empty string ("") was passed to the %s attribute. ' +
102 + 'This may cause the browser to download the whole page again over the network. ' +
103 + 'To fix this, either do not render the element at all ' +
104 + 'or pass null to %s instead of an empty string.',
105 + name,
106 + name,
107 + );
108 + } else {
109 + console.error(
110 + 'An empty string ("") was passed to the %s attribute. ' +
111 + 'To fix this, either do not render the element at all ' +
112 + 'or pass null to %s instead of an empty string.',
113 + name,
114 + name,
115 + );
116 + }
117 + }
118 + return expected;
119 }
85 - if (propertyInfo.type === BOOLEAN) {
120 + }
121 + return expected === undefined ? undefined : null;
122 + }
123 +
124 + // Even if this property uses a namespace we use getAttribute
125 + // because we assume its namespaced name is the same as our config.
126 + // To use getAttributeNS we need the local name which we don't have
127 + // in our config atm.
128 + const value = node.getAttribute(attributeName);
129 +
130 + if (expected == null) {
131 + // We had an attribute but shouldn't have had one, so read it
132 + // for the error message.
133 + return value;
134 + }
135 +
136 + // shouldRemoveAttribute
137 + switch (propertyInfo.type) {
138 + case BOOLEAN: {
139 + if (expected) {
140 // If this was a boolean, it doesn't matter what the value is
141 // the fact that we have it is the same as the expected.
142 + // As long as it's positive.
143 return expected;
144 }
90 - // Even if this property uses a namespace we use getAttribute
91 - // because we assume its namespaced name is the same as our config.
92 - // To use getAttributeNS we need the local name which we don't have
93 - // in our config atm.
94 - stringValue = node.getAttribute(attributeName);
145 + return value;
146 }
96 -
97 - if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
98 - return stringValue === null ? expected : stringValue;
99 - // eslint-disable-next-line react-internal/safe-string-coercion
100 - } else if (stringValue === '' + (expected: any)) {
101 - return expected;
102 - } else {
103 - return stringValue;
147 + case OVERLOADED_BOOLEAN: {
148 + if (value === '') {
149 + return true;
150 + }
151 + if (expected === false) {
152 + // We had an attribute but shouldn't have had one, so read it
153 + // for the error message.
154 + return value;
155 + }
156 + break;
157 + }
158 + case NUMERIC: {
159 + if (isNaN(expected)) {
160 + // We had an attribute but shouldn't have had one, so read it
161 + // for the error message.
162 + return value;
163 + }
164 + break;
165 + }
166 + case POSITIVE_NUMERIC: {
167 + if (isNaN(expected) || (expected: any) < 1) {
168 + // We had an attribute but shouldn't have had one, so read it
169 + // for the error message.
170 + return value;
171 + }
172 + break;
173 }
174 }
175 + if (__DEV__) {
176 + checkAttributeStringCoercion(expected, name);
177 + }
178 + if (value === '' + (expected: any)) {
179 + return expected;
180 + }
181 + return value;
182 }
183 }
184
@@ -115,19 +191,76 @@ export function getValueForAttribute(
191 node: Element,
192 name: string,
193 expected: mixed,
118 - isCustomComponentTag: boolean,
194 ): mixed {
195 if (__DEV__) {
196 if (!isAttributeNameSafe(name)) {
197 return;
198 }
199 if (!node.hasAttribute(name)) {
200 + // shouldRemoveAttribute
201 + switch (typeof expected) {
202 + case 'function':
203 + case 'symbol': // eslint-disable-line
204 + return expected;
205 + case 'boolean': {
206 + const prefix = name.toLowerCase().slice(0, 5);
207 + if (prefix !== 'data-' && prefix !== 'aria-') {
208 + return expected;
209 + }
210 + }
211 + }
212 return expected === undefined ? undefined : null;
213 }
214 const value = node.getAttribute(name);
215 + if (__DEV__) {
216 + checkAttributeStringCoercion(expected, name);
217 + }
218 + if (value === '' + (expected: any)) {
219 + return expected;
220 + }
221 + return value;
222 + }
223 +}
224 +
225 +export function getValueForAttributeOnCustomComponent(
226 + node: Element,
227 + name: string,
228 + expected: mixed,
229 +): mixed {
230 + if (__DEV__) {
231 + if (!isAttributeNameSafe(name)) {
232 + return;
233 + }
234 + if (!node.hasAttribute(name)) {
235 + // shouldRemoveAttribute
236 + switch (typeof expected) {
237 + case 'symbol':
238 + case 'object':
239 + // Symbols and objects are ignored when they're emitted so
240 + // it would be expected that they end up not having an attribute.
241 + return expected;
242 + case 'function':
243 + if (enableCustomElementPropertySupport) {
244 + return expected;
245 + }
246 + break;
247 + case 'boolean':
248 + if (enableCustomElementPropertySupport) {
249 + if (expected === false) {
250 + return expected;
251 + }
252 + }
253 + }
254 + return expected === undefined ? undefined : null;
255 + }
256 + if (enableCustomElementPropertySupport && name === 'className') {
257 + // className is a special cased property on the server to render as an attribute.
258 + name = 'class';
259 + }
260 + const value = node.getAttribute(name);
261
262 if (enableCustomElementPropertySupport) {
130 - if (isCustomComponentTag && value === '') {
263 + if (value === '' && expected === true) {
264 return true;
265 }
266 }
@@ -149,26 +282,188 @@ export function getValueForAttribute(
282 * @param {string} name
283 * @param {*} value
284 */
152 -export function setValueForProperty(
285 +export function setValueForProperty(node: Element, name: string, value: mixed) {
286 + if (
287 + // shouldIgnoreAttribute
288 + // We have already filtered out reserved words.
289 + name.length > 2 &&
290 + (name[0] === 'o' || name[0] === 'O') &&
291 + (name[1] === 'n' || name[1] === 'N')
292 + ) {
293 + return;
294 + }
295 +
296 + const propertyInfo = getPropertyInfo(name);
297 + if (propertyInfo !== null) {
298 + if (propertyInfo.mustUseProperty) {
299 + // We assume mustUseProperty are of BOOLEAN type because that's the only way we use it
300 + // right now.
301 + (node: any)[propertyInfo.propertyName] =
302 + value && typeof value !== 'function' && typeof value !== 'symbol';
303 + return;
304 + }
305 +
306 + // The rest are treated as attributes with special cases.
307 +
308 + const attributeName = propertyInfo.attributeName;
309 +
310 + if (value === null) {
311 + node.removeAttribute(attributeName);
312 + return;
313 + }
314 +
315 + // shouldRemoveAttribute
316 + switch (typeof value) {
317 + case 'undefined':
318 + case 'function':
319 + case 'symbol': // eslint-disable-line
320 + node.removeAttribute(attributeName);
321 + return;
322 + case 'boolean': {
323 + if (!propertyInfo.acceptsBooleans) {
324 + node.removeAttribute(attributeName);
325 + return;
326 + }
327 + }
328 + }
329 + if (enableFilterEmptyStringAttributesDOM) {
330 + if (propertyInfo.removeEmptyString && value === '') {
331 + if (__DEV__) {
332 + if (name === 'src') {
333 + console.error(
334 + 'An empty string ("") was passed to the %s attribute. ' +
335 + 'This may cause the browser to download the whole page again over the network. ' +
336 + 'To fix this, either do not render the element at all ' +
337 + 'or pass null to %s instead of an empty string.',
338 + name,
339 + name,
340 + );
341 + } else {
342 + console.error(
343 + 'An empty string ("") was passed to the %s attribute. ' +
344 + 'To fix this, either do not render the element at all ' +
345 + 'or pass null to %s instead of an empty string.',
346 + name,
347 + name,
348 + );
349 + }
350 + }
351 + node.removeAttribute(attributeName);
352 + return;
353 + }
354 + }
355 +
356 + switch (propertyInfo.type) {
357 + case BOOLEAN:
358 + if (value) {
359 + node.setAttribute(attributeName, '');
360 + } else {
361 + node.removeAttribute(attributeName);
362 + return;
363 + }
364 + break;
365 + case OVERLOADED_BOOLEAN:
366 + if (value === true) {
367 + node.setAttribute(attributeName, '');
368 + } else if (value === false) {
369 + node.removeAttribute(attributeName);
370 + } else {
371 + if (__DEV__) {
372 + checkAttributeStringCoercion(value, attributeName);
373 + }
374 + node.setAttribute(attributeName, (value: any));
375 + }
376 + return;
377 + case NUMERIC:
378 + if (!isNaN(value)) {
379 + if (__DEV__) {
380 + checkAttributeStringCoercion(value, attributeName);
381 + }
382 + node.setAttribute(attributeName, (value: any));
383 + } else {
384 + node.removeAttribute(attributeName);
385 + }
386 + break;
387 + case POSITIVE_NUMERIC:
388 + if (!isNaN(value) && (value: any) >= 1) {
389 + if (__DEV__) {
390 + checkAttributeStringCoercion(value, attributeName);
391 + }
392 + node.setAttribute(attributeName, (value: any));
393 + } else {
394 + node.removeAttribute(attributeName);
395 + }
396 + break;
397 + default: {
398 + let attributeValue;
399 + // `setAttribute` with objects becomes only `[object]` in IE8/9,
400 + // ('' + value) makes it output the correct toString()-value.
401 + if (enableTrustedTypesIntegration) {
402 + attributeValue = (value: any);
403 + } else {
404 + if (__DEV__) {
405 + checkAttributeStringCoercion(value, attributeName);
406 + }
407 + attributeValue = '' + (value: any);
408 + }
409 + if (propertyInfo.sanitizeURL) {
410 + sanitizeURL(attributeValue.toString());
411 + }
412 + const attributeNamespace = propertyInfo.attributeNamespace;
413 + if (attributeNamespace) {
414 + node.setAttributeNS(
415 + attributeNamespace,
416 + attributeName,
417 + attributeValue,
418 + );
419 + } else {
420 + node.setAttribute(attributeName, attributeValue);
421 + }
422 + }
423 + }
424 + } else if (isAttributeNameSafe(name)) {
425 + // If the prop isn't in the special list, treat it as a simple attribute.
426 + // shouldRemoveAttribute
427 + if (value === null) {
428 + node.removeAttribute(name);
429 + return;
430 + }
431 + switch (typeof value) {
432 + case 'undefined':
433 + case 'function':
434 + case 'symbol': // eslint-disable-line
435 + node.removeAttribute(name);
436 + return;
437 + case 'boolean': {
438 + const prefix = name.toLowerCase().slice(0, 5);
439 + if (prefix !== 'data-' && prefix !== 'aria-') {
440 + node.removeAttribute(name);
441 + return;
442 + }
443 + }
444 + }
445 + if (__DEV__) {
446 + checkAttributeStringCoercion(value, name);
447 + }
448 + node.setAttribute(
449 + name,
450 + enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
451 + );
452 + }
453 +}
454 +
455 +export function setValueForPropertyOnCustomComponent(
456 node: Element,
457 name: string,
458 value: mixed,
156 - isCustomComponentTag: boolean,
459 ) {
158 - const propertyInfo = getPropertyInfo(name);
159 - if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
160 - return;
161 - }
162 -
460 if (
461 enableCustomElementPropertySupport &&
165 - isCustomComponentTag &&
462 name[0] === 'o' &&
463 name[1] === 'n'
464 ) {
169 - let eventName = name.replace(/Capture$/, '');
170 - const useCapture = name !== eventName;
171 - eventName = eventName.slice(2);
465 + const useCapture = name.endsWith('Capture');
466 + const eventName = name.substr(2, useCapture ? name.length - 9 : undefined);
467
468 const prevProps = getFiberCurrentPropsFromNode(node);
469 const prevValue = prevProps != null ? prevProps[name] : null;
@@ -185,92 +480,46 @@ export function setValueForProperty(
480 node.removeAttribute(name);
481 }
482 }
188 -
483 // $FlowFixMe value can't be casted to EventListener.
484 node.addEventListener(eventName, (value: EventListener), useCapture);
485 return;
486 }
487 }
488
195 - if (
196 - enableCustomElementPropertySupport &&
197 - isCustomComponentTag &&
198 - name in (node: any)
199 - ) {
489 + if (enableCustomElementPropertySupport && name in (node: any)) {
490 (node: any)[name] = value;
491 return;
492 }
493
204 - if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
205 - value = null;
206 - }
207 - if (enableCustomElementPropertySupport) {
208 - if (isCustomComponentTag && value === true) {
209 - value = '';
210 - }
211 - }
212 -
213 - // If the prop isn't in the special list, treat it as a simple attribute.
214 - if (isCustomComponentTag || propertyInfo === null) {
215 - if (isAttributeNameSafe(name)) {
216 - const attributeName = name;
217 - if (value === null) {
218 - node.removeAttribute(attributeName);
219 - } else {
220 - if (__DEV__) {
221 - checkAttributeStringCoercion(value, name);
222 - }
223 - node.setAttribute(
224 - attributeName,
225 - enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
226 - );
227 - }
228 - }
229 - return;
230 - }
231 - const {mustUseProperty} = propertyInfo;
232 - if (mustUseProperty) {
233 - const {propertyName} = propertyInfo;
494 + if (isAttributeNameSafe(name)) {
495 + // shouldRemoveAttribute
496 if (value === null) {
235 - const {type} = propertyInfo;
236 - (node: any)[propertyName] = type === BOOLEAN ? false : '';
237 - } else {
238 - // Contrary to `setAttribute`, object properties are properly
239 - // `toString`ed by IE8/9.
240 - (node: any)[propertyName] = value;
497 + node.removeAttribute(name);
498 + return;
499 }
242 - return;
243 - }
244 - // The rest are treated as attributes with special cases.
245 - const {attributeName, attributeNamespace} = propertyInfo;
246 - if (value === null) {
247 - node.removeAttribute(attributeName);
248 - } else {
249 - const {type} = propertyInfo;
250 - let attributeValue;
251 - if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
252 - // If attribute type is boolean, we know for sure it won't be an execution sink
253 - // and we won't require Trusted Type here.
254 - attributeValue = '';
255 - } else {
256 - // `setAttribute` with objects becomes only `[object]` in IE8/9,
257 - // ('' + value) makes it output the correct toString()-value.
258 - if (enableTrustedTypesIntegration) {
259 - attributeValue = (value: any);
260 - } else {
261 - if (__DEV__) {
262 - checkAttributeStringCoercion(value, attributeName);
500 + switch (typeof value) {
501 + case 'undefined':
502 + case 'function':
503 + case 'symbol': // eslint-disable-line
504 + node.removeAttribute(name);
505 + return;
506 + case 'boolean': {
507 + if (enableCustomElementPropertySupport) {
508 + if (value === true) {
509 + node.setAttribute(name, '');
510 + return;
511 + }
512 + node.removeAttribute(name);
513 + return;
514 }
264 - attributeValue = '' + (value: any);
265 - }
266 - if (propertyInfo.sanitizeURL) {
267 - sanitizeURL(attributeValue.toString());
515 }
516 }
270 - if (attributeNamespace) {
271 - node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
272 - } else {
273 - node.setAttribute(attributeName, attributeValue);
517 + if (__DEV__) {
518 + checkAttributeStringCoercion(value, name);
519 }
520 + node.setAttribute(
521 + name,
522 + enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
523 + );
524 }
525 }
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+666 -357
@@ -18,8 +18,10 @@ import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
18
19 import {
20 getValueForAttribute,
21 + getValueForAttributeOnCustomComponent,
22 getValueForProperty,
23 setValueForProperty,
24 + setValueForPropertyOnCustomComponent,
25 } from './DOMPropertyOperations';
26 import {
27 initWrapperState as ReactDOMInputInitWrapperState,
@@ -56,12 +58,7 @@ import {
58 validateShorthandPropertyCollisionInDev,
59 } from './CSSPropertyOperations';
60 import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces';
59 -import {
60 - getPropertyInfo,
61 - shouldIgnoreAttribute,
62 - shouldRemoveAttribute,
63 -} from '../shared/DOMProperty';
64 -import assertValidProps from './assertValidProps';
61 +import {getPropertyInfo} from '../shared/DOMProperty';
62 import {DOCUMENT_NODE} from './HTMLNodeType';
63 import isCustomComponent from '../shared/isCustomComponent';
64 import possibleStandardNames from '../shared/possibleStandardNames';
@@ -84,14 +81,6 @@ import {
81 let didWarnInvalidHydration = false;
82 let didWarnScriptTags = false;
83
87 -const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
88 -const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
89 -const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
90 -const AUTOFOCUS = 'autoFocus';
91 -const CHILDREN = 'children';
92 -const STYLE = 'style';
93 -const HTML = '__html';
94 -
84 let warnedUnknownTags: {
85 [key: string]: boolean,
86 };
@@ -129,6 +118,18 @@ function validatePropertiesInDevelopment(type: string, props: any) {
118 registrationNameDependencies,
119 possibleRegistrationNames,
120 });
121 + if (
122 + props.contentEditable &&
123 + !props.suppressContentEditableWarning &&
124 + props.children != null
125 + ) {
126 + console.error(
127 + 'A component is `contentEditable` and contains `children` managed by ' +
128 + 'React. It is now your responsibility to guarantee that none of ' +
129 + 'those nodes are unexpectedly modified or duplicated. This is ' +
130 + 'probably not intentional.',
131 + );
132 + }
133 }
134 }
135
@@ -297,64 +298,116 @@ function setInitialDOMProperties(
298 continue;
299 }
300 const nextProp = nextProps[propKey];
300 - if (propKey === STYLE) {
301 - if (__DEV__) {
302 - if (nextProp) {
303 - // Freeze the next style object so that we can assume it won't be
304 - // mutated. We have already warned for this in the past.
305 - Object.freeze(nextProp);
301 + switch (propKey) {
302 + case 'style': {
303 + if (nextProp != null && typeof nextProp !== 'object') {
304 + throw new Error(
305 + 'The `style` prop expects a mapping from style properties to values, ' +
306 + "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
307 + 'using JSX.',
308 + );
309 + }
310 + if (__DEV__) {
311 + if (nextProp) {
312 + // Freeze the next style object so that we can assume it won't be
313 + // mutated. We have already warned for this in the past.
314 + Object.freeze(nextProp);
315 + }
316 }
317 + // Relies on `updateStylesByID` not mutating `styleUpdates`.
318 + setValueForStyles(domElement, nextProp);
319 + break;
320 }
308 - // Relies on `updateStylesByID` not mutating `styleUpdates`.
309 - setValueForStyles(domElement, nextProp);
310 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
311 - const nextHtml = nextProp ? nextProp[HTML] : undefined;
312 - if (nextHtml != null) {
313 - if (disableIEWorkarounds) {
314 - domElement.innerHTML = nextHtml;
315 - } else {
316 - setInnerHTML(domElement, nextHtml);
321 + case 'dangerouslySetInnerHTML': {
322 + if (nextProp != null) {
323 + if (typeof nextProp !== 'object' || !('__html' in nextProp)) {
324 + throw new Error(
325 + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
326 + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
327 + 'for more information.',
328 + );
329 + }
330 + const nextHtml = nextProp.__html;
331 + if (nextHtml != null) {
332 + if (nextProps.children != null) {
333 + throw new Error(
334 + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
335 + );
336 + }
337 + if (disableIEWorkarounds) {
338 + domElement.innerHTML = nextHtml;
339 + } else {
340 + setInnerHTML(domElement, nextHtml);
341 + }
342 + }
343 }
344 + break;
345 }
319 - } else if (propKey === CHILDREN) {
320 - if (typeof nextProp === 'string') {
321 - // Avoid setting initial textContent when the text is empty. In IE11 setting
322 - // textContent on a <textarea> will cause the placeholder to not
323 - // show within the <textarea> until it has been focused and blurred again.
324 - // https://github.com/facebook/react/issues/6731#issuecomment-254874553
325 - const canSetTextContent =
326 - (!enableHostSingletons || tag !== 'body') &&
327 - (tag !== 'textarea' || nextProp !== '');
328 - if (canSetTextContent) {
329 - setTextContent(domElement, nextProp);
346 + case 'children': {
347 + if (typeof nextProp === 'string') {
348 + // Avoid setting initial textContent when the text is empty. In IE11 setting
349 + // textContent on a <textarea> will cause the placeholder to not
350 + // show within the <textarea> until it has been focused and blurred again.
351 + // https://github.com/facebook/react/issues/6731#issuecomment-254874553
352 + const canSetTextContent =
353 + (!enableHostSingletons || tag !== 'body') &&
354 + (tag !== 'textarea' || nextProp !== '');
355 + if (canSetTextContent) {
356 + setTextContent(domElement, nextProp);
357 + }
358 + } else if (typeof nextProp === 'number') {
359 + const canSetTextContent = !enableHostSingletons || tag !== 'body';
360 + if (canSetTextContent) {
361 + setTextContent(domElement, '' + nextProp);
362 + }
363 }
331 - } else if (typeof nextProp === 'number') {
332 - const canSetTextContent = !enableHostSingletons || tag !== 'body';
333 - if (canSetTextContent) {
334 - setTextContent(domElement, '' + nextProp);
364 + break;
365 + }
366 + case 'onScroll': {
367 + if (nextProp != null) {
368 + if (__DEV__ && typeof nextProp !== 'function') {
369 + warnForInvalidEventListener(propKey, nextProp);
370 + }
371 + listenToNonDelegatedEvent('scroll', domElement);
372 }
373 + break;
374 }
337 - } else if (
338 - propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
339 - propKey === SUPPRESS_HYDRATION_WARNING
340 - ) {
341 - // Noop
342 - } else if (propKey === AUTOFOCUS) {
343 - // We polyfill it separately on the client during commit.
344 - // We could have excluded it in the property list instead of
345 - // adding a special case here, but then it wouldn't be emitted
346 - // on server rendering (but we *do* want to emit it in SSR).
347 - } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
348 - if (nextProp != null) {
349 - if (__DEV__ && typeof nextProp !== 'function') {
350 - warnForInvalidEventListener(propKey, nextProp);
375 + case 'suppressContentEditableWarning':
376 + case 'suppressHydrationWarning':
377 + case 'defaultValue': // Reserved
378 + case 'defaultChecked':
379 + case 'innerHTML': {
380 + // Noop
381 + break;
382 + }
383 + case 'autoFocus': {
384 + // We polyfill it separately on the client during commit.
385 + // We could have excluded it in the property list instead of
386 + // adding a special case here, but then it wouldn't be emitted
387 + // on server rendering (but we *do* want to emit it in SSR).
388 + break;
389 + }
390 + case 'innerText': // Properties
391 + case 'textContent':
392 + if (enableCustomElementPropertySupport) {
393 + break;
394 }
352 - if (propKey === 'onScroll') {
353 - listenToNonDelegatedEvent('scroll', domElement);
395 + // eslint-disable-next-line no-fallthrough
396 + default: {
397 + if (registrationNameDependencies.hasOwnProperty(propKey)) {
398 + if (nextProp != null) {
399 + if (__DEV__ && typeof nextProp !== 'function') {
400 + warnForInvalidEventListener(propKey, nextProp);
401 + }
402 + }
403 + } else if (nextProp != null) {
404 + if (isCustomComponentTag) {
405 + setValueForPropertyOnCustomComponent(domElement, propKey, nextProp);
406 + } else {
407 + setValueForProperty(domElement, propKey, nextProp);
408 + }
409 }
410 }
356 - } else if (nextProp != null) {
357 - setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
411 }
412 }
413 }
@@ -369,18 +422,27 @@ function updateDOMProperties(
422 for (let i = 0; i < updatePayload.length; i += 2) {
423 const propKey = updatePayload[i];
424 const propValue = updatePayload[i + 1];
372 - if (propKey === STYLE) {
373 - setValueForStyles(domElement, propValue);
374 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
375 - if (disableIEWorkarounds) {
376 - domElement.innerHTML = propValue;
377 - } else {
378 - setInnerHTML(domElement, propValue);
379 - }
380 - } else if (propKey === CHILDREN) {
381 - setTextContent(domElement, propValue);
382 - } else {
383 - setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
425 + switch (propKey) {
426 + case 'style':
427 + setValueForStyles(domElement, propValue);
428 + break;
429 + case 'dangerouslySetInnerHTML':
430 + if (disableIEWorkarounds) {
431 + domElement.innerHTML = propValue;
432 + } else {
433 + setInnerHTML(domElement, propValue);
434 + }
435 + break;
436 + case 'children':
437 + setTextContent(domElement, propValue);
438 + break;
439 + default:
440 + if (isCustomComponentTag) {
441 + setValueForPropertyOnCustomComponent(domElement, propKey, propValue);
442 + } else {
443 + setValueForProperty(domElement, propKey, propValue);
444 + }
445 + break;
446 }
447 }
448 }
@@ -536,6 +598,16 @@ export function setInitialProperties(
598 case 'iframe':
599 case 'object':
600 case 'embed':
601 + if (
602 + rawProps.children != null ||
603 + rawProps.dangerouslySetInnerHTML != null
604 + ) {
605 + // TODO: Can we make this a DEV warning to avoid this deny list?
606 + throw new Error(
607 + `${tag} is a void element tag and must neither have \`children\` nor ` +
608 + 'use `dangerouslySetInnerHTML`.',
609 + );
610 + }
611 // We listen to this event in case to ensure emulated bubble
612 // listeners still fire for the load event.
613 listenToNonDelegatedEvent('load', domElement);
@@ -551,14 +623,34 @@ export function setInitialProperties(
623 props = rawProps;
624 break;
625 case 'source':
626 + if (
627 + rawProps.children != null ||
628 + rawProps.dangerouslySetInnerHTML != null
629 + ) {
630 + // TODO: Can we make this a DEV warning to avoid this deny list?
631 + throw new Error(
632 + `${tag} is a void element tag and must neither have \`children\` nor ` +
633 + 'use `dangerouslySetInnerHTML`.',
634 + );
635 + }
636 // We listen to this event in case to ensure emulated bubble
637 // listeners still fire for the error event.
638 listenToNonDelegatedEvent('error', domElement);
639 props = rawProps;
640 break;
641 case 'img':
560 - case 'image':
642 case 'link':
643 + if (
644 + rawProps.children != null ||
645 + rawProps.dangerouslySetInnerHTML != null
646 + ) {
647 + throw new Error(
648 + `${tag} is a void element tag and must neither have \`children\` nor ` +
649 + 'use `dangerouslySetInnerHTML`.',
650 + );
651 + }
652 + // eslint-disable-next-line no-fallthrough
653 + case 'image':
654 // We listen to these events in case to ensure emulated bubble
655 // listeners still fire for error and load events.
656 listenToNonDelegatedEvent('error', domElement);
@@ -572,6 +664,15 @@ export function setInitialProperties(
664 props = rawProps;
665 break;
666 case 'input':
667 + if (
668 + rawProps.children != null ||
669 + rawProps.dangerouslySetInnerHTML != null
670 + ) {
671 + throw new Error(
672 + `${tag} is a void element tag and must neither have \`children\` nor ` +
673 + 'use `dangerouslySetInnerHTML`.',
674 + );
675 + }
676 ReactDOMInputInitWrapperState(domElement, rawProps);
677 props = ReactDOMInputGetHostProps(domElement, rawProps);
678 // We listen to this event in case to ensure emulated bubble
@@ -596,12 +697,33 @@ export function setInitialProperties(
697 // listeners still fire for the invalid event.
698 listenToNonDelegatedEvent('invalid', domElement);
699 break;
700 + case 'area':
701 + case 'base':
702 + case 'br':
703 + case 'col':
704 + case 'hr':
705 + case 'keygen':
706 + case 'meta':
707 + case 'param':
708 + case 'track':
709 + case 'wbr':
710 + case 'menuitem': {
711 + if (
712 + rawProps.children != null ||
713 + rawProps.dangerouslySetInnerHTML != null
714 + ) {
715 + // TODO: Can we make this a DEV warning to avoid this deny list?
716 + throw new Error(
717 + `${tag} is a void element tag and must neither have \`children\` nor ` +
718 + 'use `dangerouslySetInnerHTML`.',
719 + );
720 + }
721 + }
722 + // eslint-disable-next-line no-fallthrough
723 default:
724 props = rawProps;
725 }
726
603 - assertValidProps(tag, props);
604 -
727 setInitialDOMProperties(tag, domElement, props, isCustomComponentTag);
728
729 switch (tag) {
@@ -649,6 +771,15 @@ export function diffProperties(
771 let nextProps: Object;
772 switch (tag) {
773 case 'input':
774 + if (
775 + nextRawProps.children != null ||
776 + nextRawProps.dangerouslySetInnerHTML != null
777 + ) {
778 + throw new Error(
779 + `${tag} is a void element tag and must neither have \`children\` nor ` +
780 + 'use `dangerouslySetInnerHTML`.',
781 + );
782 + }
783 lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
784 nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
785 updatePayload = [];
@@ -663,6 +794,33 @@ export function diffProperties(
794 nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
795 updatePayload = [];
796 break;
797 + case 'img':
798 + case 'link':
799 + case 'area':
800 + case 'base':
801 + case 'br':
802 + case 'col':
803 + case 'embed':
804 + case 'hr':
805 + case 'keygen':
806 + case 'meta':
807 + case 'param':
808 + case 'source':
809 + case 'track':
810 + case 'wbr':
811 + case 'menuitem': {
812 + if (
813 + nextRawProps.children != null ||
814 + nextRawProps.dangerouslySetInnerHTML != null
815 + ) {
816 + // TODO: Can we make this a DEV warning to avoid this deny list?
817 + throw new Error(
818 + `${tag} is a void element tag and must neither have \`children\` nor ` +
819 + 'use `dangerouslySetInnerHTML`.',
820 + );
821 + }
822 + }
823 + // eslint-disable-next-line no-fallthrough
824 default:
825 lastProps = lastRawProps;
826 nextProps = nextRawProps;
@@ -676,8 +834,6 @@ export function diffProperties(
834 break;
835 }
836
679 - assertValidProps(tag, nextProps);
680 -
837 let propKey;
838 let styleName;
839 let styleUpdates = null;
@@ -689,36 +845,56 @@ export function diffProperties(
845 ) {
846 continue;
847 }
692 - if (propKey === STYLE) {
693 - const lastStyle = lastProps[propKey];
694 - for (styleName in lastStyle) {
695 - if (lastStyle.hasOwnProperty(styleName)) {
696 - if (!styleUpdates) {
697 - styleUpdates = ({}: {[string]: $FlowFixMe});
848 + switch (propKey) {
849 + case 'style': {
850 + const lastStyle = lastProps[propKey];
851 + for (styleName in lastStyle) {
852 + if (lastStyle.hasOwnProperty(styleName)) {
853 + if (!styleUpdates) {
854 + styleUpdates = ({}: {[string]: $FlowFixMe});
855 + }
856 + styleUpdates[styleName] = '';
857 }
699 - styleUpdates[styleName] = '';
858 }
859 + break;
860 }
702 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) {
703 - // Noop. This is handled by the clear text mechanism.
704 - } else if (
705 - propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
706 - propKey === SUPPRESS_HYDRATION_WARNING
707 - ) {
708 - // Noop
709 - } else if (propKey === AUTOFOCUS) {
710 - // Noop. It doesn't work on updates anyway.
711 - } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
712 - // This is a special case. If any listener updates we need to ensure
713 - // that the "current" fiber pointer gets updated so we need a commit
714 - // to update this element.
715 - if (!updatePayload) {
716 - updatePayload = [];
861 + case 'dangerouslySetInnerHTML':
862 + case 'children': {
863 + // Noop. This is handled by the clear text mechanism.
864 + break;
865 + }
866 + case 'suppressContentEditableWarning':
867 + case 'suppressHydrationWarning':
868 + case 'defaultValue': // Reserved
869 + case 'defaultChecked':
870 + case 'innerHTML': {
871 + // Noop
872 + break;
873 + }
874 + case 'autoFocus': {
875 + // Noop. It doesn't work on updates anyway.
876 + break;
877 + }
878 + case 'innerText': // Properties
879 + case 'textContent':
880 + if (enableCustomElementPropertySupport) {
881 + break;
882 + }
883 + // eslint-disable-next-line no-fallthrough
884 + default: {
885 + if (registrationNameDependencies.hasOwnProperty(propKey)) {
886 + // This is a special case. If any listener updates we need to ensure
887 + // that the "current" fiber pointer gets updated so we need a commit
888 + // to update this element.
889 + if (!updatePayload) {
890 + updatePayload = [];
891 + }
892 + } else {
893 + // For all other deleted properties we add it to the queue. We use
894 + // the allowed property list in the commit phase instead.
895 + (updatePayload = updatePayload || []).push(propKey, null);
896 + }
897 }
718 - } else {
719 - // For all other deleted properties we add it to the queue. We use
720 - // the allowed property list in the commit phase instead.
721 - (updatePayload = updatePayload || []).push(propKey, null);
898 }
899 }
900 for (propKey in nextProps) {
@@ -731,96 +907,153 @@ export function diffProperties(
907 ) {
908 continue;
909 }
734 - if (propKey === STYLE) {
735 - if (__DEV__) {
736 - if (nextProp) {
737 - // Freeze the next style object so that we can assume it won't be
738 - // mutated. We have already warned for this in the past.
739 - Object.freeze(nextProp);
910 + switch (propKey) {
911 + case 'style': {
912 + if (nextProp != null && typeof nextProp !== 'object') {
913 + throw new Error(
914 + 'The `style` prop expects a mapping from style properties to values, ' +
915 + "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
916 + 'using JSX.',
917 + );
918 }
741 - }
742 - if (lastProp) {
743 - // Unset styles on `lastProp` but not on `nextProp`.
744 - for (styleName in lastProp) {
745 - if (
746 - lastProp.hasOwnProperty(styleName) &&
747 - (!nextProp || !nextProp.hasOwnProperty(styleName))
748 - ) {
749 - if (!styleUpdates) {
750 - styleUpdates = ({}: {[string]: string});
751 - }
752 - styleUpdates[styleName] = '';
919 + if (__DEV__) {
920 + if (nextProp) {
921 + // Freeze the next style object so that we can assume it won't be
922 + // mutated. We have already warned for this in the past.
923 + Object.freeze(nextProp);
924 }
925 }
755 - // Update styles that changed since `lastProp`.
756 - for (styleName in nextProp) {
757 - if (
758 - nextProp.hasOwnProperty(styleName) &&
759 - lastProp[styleName] !== nextProp[styleName]
760 - ) {
761 - if (!styleUpdates) {
762 - styleUpdates = ({}: {[string]: $FlowFixMe});
926 + if (lastProp) {
927 + // Unset styles on `lastProp` but not on `nextProp`.
928 + for (styleName in lastProp) {
929 + if (
930 + lastProp.hasOwnProperty(styleName) &&
931 + (!nextProp || !nextProp.hasOwnProperty(styleName))
932 + ) {
933 + if (!styleUpdates) {
934 + styleUpdates = ({}: {[string]: string});
935 + }
936 + styleUpdates[styleName] = '';
937 }
764 - styleUpdates[styleName] = nextProp[styleName];
938 }
766 - }
767 - } else {
768 - // Relies on `updateStylesByID` not mutating `styleUpdates`.
769 - if (!styleUpdates) {
770 - if (!updatePayload) {
771 - updatePayload = [];
939 + // Update styles that changed since `lastProp`.
940 + for (styleName in nextProp) {
941 + if (
942 + nextProp.hasOwnProperty(styleName) &&
943 + lastProp[styleName] !== nextProp[styleName]
944 + ) {
945 + if (!styleUpdates) {
946 + styleUpdates = ({}: {[string]: $FlowFixMe});
947 + }
948 + styleUpdates[styleName] = nextProp[styleName];
949 + }
950 }
773 - updatePayload.push(propKey, styleUpdates);
951 + } else {
952 + // Relies on `updateStylesByID` not mutating `styleUpdates`.
953 + if (!styleUpdates) {
954 + if (!updatePayload) {
955 + updatePayload = [];
956 + }
957 + updatePayload.push(propKey, styleUpdates);
958 + }
959 + styleUpdates = nextProp;
960 }
775 - styleUpdates = nextProp;
961 + break;
962 }
777 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
778 - const nextHtml = nextProp ? nextProp[HTML] : undefined;
779 - const lastHtml = lastProp ? lastProp[HTML] : undefined;
780 - if (nextHtml != null) {
781 - if (lastHtml !== nextHtml) {
782 - (updatePayload = updatePayload || []).push(propKey, nextHtml);
963 + case 'dangerouslySetInnerHTML': {
964 + if (nextProp != null) {
965 + if (typeof nextProp !== 'object' || !('__html' in nextProp)) {
966 + throw new Error(
967 + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
968 + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
969 + 'for more information.',
970 + );
971 + }
972 + const nextHtml = nextProp.__html;
973 + if (nextHtml != null) {
974 + if (nextProps.children != null) {
975 + throw new Error(
976 + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
977 + );
978 + }
979 + const lastHtml = lastProp ? lastProp.__html : undefined;
980 + if (lastHtml !== nextHtml) {
981 + (updatePayload = updatePayload || []).push(propKey, nextHtml);
982 + }
983 + }
984 + } else {
985 + // TODO: It might be too late to clear this if we have children
986 + // inserted already.
987 }
784 - } else {
785 - // TODO: It might be too late to clear this if we have children
786 - // inserted already.
787 - }
788 - } else if (propKey === CHILDREN) {
789 - if (typeof nextProp === 'string' || typeof nextProp === 'number') {
790 - (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
988 + break;
989 }
792 - } else if (
793 - propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
794 - propKey === SUPPRESS_HYDRATION_WARNING
795 - ) {
796 - // Noop
797 - } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
798 - if (nextProp != null) {
799 - // We eagerly listen to this even though we haven't committed yet.
800 - if (__DEV__ && typeof nextProp !== 'function') {
801 - warnForInvalidEventListener(propKey, nextProp);
990 + case 'children': {
991 + if (typeof nextProp === 'string' || typeof nextProp === 'number') {
992 + (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
993 }
803 - if (propKey === 'onScroll') {
994 + break;
995 + }
996 + case 'onScroll': {
997 + if (nextProp != null) {
998 + // We eagerly listen to this even though we haven't committed yet.
999 + if (__DEV__ && typeof nextProp !== 'function') {
1000 + warnForInvalidEventListener(propKey, nextProp);
1001 + }
1002 listenToNonDelegatedEvent('scroll', domElement);
1003 }
1004 + if (!updatePayload && lastProp !== nextProp) {
1005 + // This is a special case. If any listener updates we need to ensure
1006 + // that the "current" props pointer gets updated so we need a commit
1007 + // to update this element.
1008 + updatePayload = [];
1009 + }
1010 + break;
1011 }
807 - if (!updatePayload && lastProp !== nextProp) {
808 - // This is a special case. If any listener updates we need to ensure
809 - // that the "current" props pointer gets updated so we need a commit
810 - // to update this element.
811 - updatePayload = [];
1012 + case 'suppressContentEditableWarning':
1013 + case 'suppressHydrationWarning':
1014 + case 'defaultValue': // Reserved
1015 + case 'defaultChecked':
1016 + case 'innerHTML': {
1017 + // Noop
1018 + break;
1019 + }
1020 + case 'autoFocus': {
1021 + // Noop on updates
1022 + break;
1023 + }
1024 + case 'innerText': // Properties
1025 + case 'textContent':
1026 + if (enableCustomElementPropertySupport) {
1027 + break;
1028 + }
1029 + // eslint-disable-next-line no-fallthrough
1030 + default: {
1031 + if (registrationNameDependencies.hasOwnProperty(propKey)) {
1032 + if (nextProp != null) {
1033 + // We eagerly listen to this even though we haven't committed yet.
1034 + if (__DEV__ && typeof nextProp !== 'function') {
1035 + warnForInvalidEventListener(propKey, nextProp);
1036 + }
1037 + }
1038 + if (!updatePayload && lastProp !== nextProp) {
1039 + // This is a special case. If any listener updates we need to ensure
1040 + // that the "current" props pointer gets updated so we need a commit
1041 + // to update this element.
1042 + updatePayload = [];
1043 + }
1044 + } else {
1045 + // For any other property we always add it to the queue and then we
1046 + // filter it out using the allowed property list during the commit.
1047 + (updatePayload = updatePayload || []).push(propKey, nextProp);
1048 + }
1049 }
813 - } else {
814 - // For any other property we always add it to the queue and then we
815 - // filter it out using the allowed property list during the commit.
816 - (updatePayload = updatePayload || []).push(propKey, nextProp);
1050 }
1051 }
1052 if (styleUpdates) {
1053 if (__DEV__) {
821 - validateShorthandPropertyCollisionInDev(styleUpdates, nextProps[STYLE]);
1054 + validateShorthandPropertyCollisionInDev(styleUpdates, nextProps.style);
1055 }
823 - (updatePayload = updatePayload || []).push(STYLE, styleUpdates);
1056 + (updatePayload = updatePayload || []).push('style', styleUpdates);
1057 }
1058 return updatePayload;
1059 }
@@ -885,6 +1118,227 @@ function getPossibleStandardName(propName: string): string | null {
1118 return null;
1119 }
1120
1121 +function diffHydratedStyles(domElement: Element, value: mixed) {
1122 + if (value != null && typeof value !== 'object') {
1123 + throw new Error(
1124 + 'The `style` prop expects a mapping from style properties to values, ' +
1125 + "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
1126 + 'using JSX.',
1127 + );
1128 + }
1129 + if (canDiffStyleForHydrationWarning) {
1130 + const expectedStyle = createDangerousStringForStyles(value);
1131 + const serverValue = domElement.getAttribute('style');
1132 + if (expectedStyle !== serverValue) {
1133 + warnForPropDifference('style', serverValue, expectedStyle);
1134 + }
1135 + }
1136 +}
1137 +
1138 +function diffHydratedCustomComponent(
1139 + domElement: Element,
1140 + tag: string,
1141 + rawProps: Object,
1142 + parentNamespaceDev: string,
1143 + extraAttributeNames: Set<string>,
1144 +) {
1145 + for (const propKey in rawProps) {
1146 + if (!rawProps.hasOwnProperty(propKey)) {
1147 + continue;
1148 + }
1149 + const nextProp = rawProps[propKey];
1150 + if (nextProp == null) {
1151 + continue;
1152 + }
1153 + if (registrationNameDependencies.hasOwnProperty(propKey)) {
1154 + if (typeof nextProp !== 'function') {
1155 + warnForInvalidEventListener(propKey, nextProp);
1156 + }
1157 + continue;
1158 + }
1159 + if (rawProps.suppressHydrationWarning === true) {
1160 + // Don't bother comparing. We're ignoring all these warnings.
1161 + continue;
1162 + }
1163 + // Validate that the properties correspond to their expected values.
1164 + switch (propKey) {
1165 + case 'children': // Checked above already
1166 + case 'suppressContentEditableWarning':
1167 + case 'suppressHydrationWarning':
1168 + case 'defaultValue':
1169 + case 'defaultChecked':
1170 + case 'innerHTML':
1171 + // Noop
1172 + continue;
1173 + case 'dangerouslySetInnerHTML':
1174 + const serverHTML = domElement.innerHTML;
1175 + const nextHtml = nextProp ? nextProp.__html : undefined;
1176 + if (nextHtml != null) {
1177 + const expectedHTML = normalizeHTML(domElement, nextHtml);
1178 + if (expectedHTML !== serverHTML) {
1179 + warnForPropDifference(propKey, serverHTML, expectedHTML);
1180 + }
1181 + }
1182 + continue;
1183 + case 'style':
1184 + // $FlowFixMe - Should be inferred as not undefined.
1185 + extraAttributeNames.delete(propKey);
1186 + diffHydratedStyles(domElement, nextProp);
1187 + continue;
1188 + case 'offsetParent':
1189 + case 'offsetTop':
1190 + case 'offsetLeft':
1191 + case 'offsetWidth':
1192 + case 'offsetHeight':
1193 + case 'isContentEditable':
1194 + case 'outerText':
1195 + case 'outerHTML':
1196 + if (enableCustomElementPropertySupport) {
1197 + // $FlowFixMe - Should be inferred as not undefined.
1198 + extraAttributeNames.delete(propKey.toLowerCase());
1199 + if (__DEV__) {
1200 + console.error(
1201 + 'Assignment to read-only property will result in a no-op: `%s`',
1202 + propKey,
1203 + );
1204 + }
1205 + continue;
1206 + }
1207 + // eslint-disable-next-line no-fallthrough
1208 + default: {
1209 + let ownNamespaceDev = parentNamespaceDev;
1210 + if (ownNamespaceDev === HTML_NAMESPACE) {
1211 + ownNamespaceDev = getIntrinsicNamespace(tag);
1212 + }
1213 + if (ownNamespaceDev === HTML_NAMESPACE) {
1214 + // $FlowFixMe - Should be inferred as not undefined.
1215 + extraAttributeNames.delete(propKey.toLowerCase());
1216 + } else {
1217 + // $FlowFixMe - Should be inferred as not undefined.
1218 + extraAttributeNames.delete(propKey);
1219 + }
1220 + const serverValue = getValueForAttributeOnCustomComponent(
1221 + domElement,
1222 + propKey,
1223 + nextProp,
1224 + );
1225 + if (nextProp !== serverValue) {
1226 + warnForPropDifference(propKey, serverValue, nextProp);
1227 + }
1228 + }
1229 + }
1230 + }
1231 +}
1232 +
1233 +function diffHydratedGenericElement(
1234 + domElement: Element,
1235 + tag: string,
1236 + rawProps: Object,
1237 + parentNamespaceDev: string,
1238 + extraAttributeNames: Set<string>,
1239 +) {
1240 + for (const propKey in rawProps) {
1241 + if (!rawProps.hasOwnProperty(propKey)) {
1242 + continue;
1243 + }
1244 + const nextProp = rawProps[propKey];
1245 + if (nextProp == null) {
1246 + continue;
1247 + }
1248 + if (registrationNameDependencies.hasOwnProperty(propKey)) {
1249 + if (typeof nextProp !== 'function') {
1250 + warnForInvalidEventListener(propKey, nextProp);
1251 + }
1252 + continue;
1253 + }
1254 + if (rawProps.suppressHydrationWarning === true) {
1255 + // Don't bother comparing. We're ignoring all these warnings.
1256 + continue;
1257 + }
1258 + // Validate that the properties correspond to their expected values.
1259 + switch (propKey) {
1260 + case 'children': // Checked above already
1261 + case 'suppressContentEditableWarning':
1262 + case 'suppressHydrationWarning':
1263 + case 'value': // Controlled attributes are not validated
1264 + case 'checked': // TODO: Only ignore them on controlled tags.
1265 + case 'selected':
1266 + case 'defaultValue':
1267 + case 'defaultChecked':
1268 + case 'innerHTML':
1269 + // Noop
1270 + continue;
1271 + case 'dangerouslySetInnerHTML':
1272 + const serverHTML = domElement.innerHTML;
1273 + const nextHtml = nextProp ? nextProp.__html : undefined;
1274 + if (nextHtml != null) {
1275 + const expectedHTML = normalizeHTML(domElement, nextHtml);
1276 + if (expectedHTML !== serverHTML) {
1277 + warnForPropDifference(propKey, serverHTML, expectedHTML);
1278 + }
1279 + }
1280 + continue;
1281 + case 'style':
1282 + // $FlowFixMe - Should be inferred as not undefined.
1283 + extraAttributeNames.delete(propKey);
1284 + diffHydratedStyles(domElement, nextProp);
1285 + continue;
1286 + // eslint-disable-next-line no-fallthrough
1287 + default:
1288 + if (
1289 + // shouldIgnoreAttribute
1290 + // We have already filtered out null/undefined and reserved words.
1291 + propKey.length > 2 &&
1292 + (propKey[0] === 'o' || propKey[0] === 'O') &&
1293 + (propKey[1] === 'n' || propKey[1] === 'N')
1294 + ) {
1295 + continue;
1296 + }
1297 + const propertyInfo = getPropertyInfo(propKey);
1298 + let isMismatchDueToBadCasing = false;
1299 + let serverValue;
1300 + if (propertyInfo !== null) {
1301 + // $FlowFixMe - Should be inferred as not undefined.
1302 + extraAttributeNames.delete(propertyInfo.attributeName);
1303 + serverValue = getValueForProperty(
1304 + domElement,
1305 + propKey,
1306 + nextProp,
1307 + propertyInfo,
1308 + );
1309 + } else {
1310 + let ownNamespaceDev = parentNamespaceDev;
1311 + if (ownNamespaceDev === HTML_NAMESPACE) {
1312 + ownNamespaceDev = getIntrinsicNamespace(tag);
1313 + }
1314 + if (ownNamespaceDev === HTML_NAMESPACE) {
1315 + // $FlowFixMe - Should be inferred as not undefined.
1316 + extraAttributeNames.delete(propKey.toLowerCase());
1317 + } else {
1318 + const standardName = getPossibleStandardName(propKey);
1319 + if (standardName !== null && standardName !== propKey) {
1320 + // If an SVG prop is supplied with bad casing, it will
1321 + // be successfully parsed from HTML, but will produce a mismatch
1322 + // (and would be incorrectly rendered on the client).
1323 + // However, we already warn about bad casing elsewhere.
1324 + // So we'll skip the misleading extra mismatch warning in this case.
1325 + isMismatchDueToBadCasing = true;
1326 + // $FlowFixMe - Should be inferred as not undefined.
1327 + extraAttributeNames.delete(standardName);
1328 + }
1329 + // $FlowFixMe - Should be inferred as not undefined.
1330 + extraAttributeNames.delete(propKey);
1331 + }
1332 + serverValue = getValueForAttribute(domElement, propKey, nextProp);
1333 + }
1334 +
1335 + if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
1336 + warnForPropDifference(propKey, serverValue, nextProp);
1337 + }
1338 + }
1339 + }
1340 +}
1341 +
1342 export function diffHydratedProperties(
1343 domElement: Element,
1344 tag: string,
@@ -893,8 +1347,6 @@ export function diffHydratedProperties(
1347 shouldWarnDev: boolean,
1348 parentNamespaceDev: string,
1349 ): null | Array<mixed> {
896 - let extraAttributeNames: Set<string>;
897 -
1350 if (__DEV__) {
1351 validatePropertiesInDevelopment(tag, rawProps);
1352 }
@@ -965,30 +1417,6 @@ export function diffHydratedProperties(
1417 listenToNonDelegatedEvent('scroll', domElement);
1418 }
1419
968 - assertValidProps(tag, rawProps);
969 -
970 - if (__DEV__) {
971 - extraAttributeNames = new Set();
972 - const attributes = domElement.attributes;
973 - for (let i = 0; i < attributes.length; i++) {
974 - const name = attributes[i].name.toLowerCase();
975 - switch (name) {
976 - // Controlled attributes are not validated
977 - // TODO: Only ignore them on controlled tags.
978 - case 'value':
979 - break;
980 - case 'checked':
981 - break;
982 - case 'selected':
983 - break;
984 - default:
985 - // Intentionally use the original name.
986 - // See discussion in https://github.com/facebook/react/pull/10676.
987 - extraAttributeNames.add(attributes[i].name);
988 - }
989 - }
990 - }
991 -
1420 let updatePayload = null;
1421
1422 const children = rawProps.children;
@@ -1003,7 +1431,7 @@ export function diffHydratedProperties(
1431 // TODO: Should we use domElement.firstChild.nodeValue to compare?
1432 if (typeof children === 'string' || typeof children === 'number') {
1433 if (domElement.textContent !== '' + children) {
1006 - if (rawProps[SUPPRESS_HYDRATION_WARNING] !== true) {
1434 + if (rawProps.suppressHydrationWarning !== true) {
1435 checkForUnmatchedText(
1436 domElement.textContent,
1437 children,
@@ -1012,171 +1440,52 @@ export function diffHydratedProperties(
1440 );
1441 }
1442 if (!isConcurrentMode) {
1015 - updatePayload = [CHILDREN, children];
1443 + updatePayload = ['children', children];
1444 }
1445 }
1446 }
1447
1448 if (__DEV__ && shouldWarnDev) {
1021 - const isCustomComponentTag = isCustomComponent(tag, rawProps);
1022 -
1023 - for (const propKey in rawProps) {
1024 - if (!rawProps.hasOwnProperty(propKey)) {
1025 - continue;
1026 - }
1027 - const nextProp = rawProps[propKey];
1028 - if (propKey === CHILDREN) {
1029 - // Checked above already
1030 - } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
1031 - if (nextProp != null) {
1032 - if (typeof nextProp !== 'function') {
1033 - warnForInvalidEventListener(propKey, nextProp);
1034 - }
1035 - }
1036 - } else {
1037 - // Validate that the properties correspond to their expected values.
1038 - let serverValue;
1039 - const propertyInfo =
1040 - isCustomComponentTag && enableCustomElementPropertySupport
1041 - ? null
1042 - : getPropertyInfo(propKey);
1043 - if (rawProps[SUPPRESS_HYDRATION_WARNING] === true) {
1044 - // Don't bother comparing. We're ignoring all these warnings.
1045 - } else if (
1046 - propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
1047 - propKey === SUPPRESS_HYDRATION_WARNING ||
1048 - // Controlled attributes are not validated
1049 - // TODO: Only ignore them on controlled tags.
1050 - propKey === 'value' ||
1051 - propKey === 'checked' ||
1052 - propKey === 'selected'
1053 - ) {
1054 - // Noop
1055 - } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
1056 - const serverHTML = domElement.innerHTML;
1057 - const nextHtml = nextProp ? nextProp[HTML] : undefined;
1058 - if (nextHtml != null) {
1059 - const expectedHTML = normalizeHTML(domElement, nextHtml);
1060 - if (expectedHTML !== serverHTML) {
1061 - warnForPropDifference(propKey, serverHTML, expectedHTML);
1062 - }
1063 - }
1064 - } else if (propKey === STYLE) {
1065 - // $FlowFixMe - Should be inferred as not undefined.
1066 - extraAttributeNames.delete(propKey);
1067 -
1068 - if (canDiffStyleForHydrationWarning) {
1069 - const expectedStyle = createDangerousStringForStyles(nextProp);
1070 - serverValue = domElement.getAttribute('style');
1071 - if (expectedStyle !== serverValue) {
1072 - warnForPropDifference(propKey, serverValue, expectedStyle);
1073 - }
1074 - }
1075 - } else if (
1076 - enableCustomElementPropertySupport &&
1077 - isCustomComponentTag &&
1078 - (propKey === 'offsetParent' ||
1079 - propKey === 'offsetTop' ||
1080 - propKey === 'offsetLeft' ||
1081 - propKey === 'offsetWidth' ||
1082 - propKey === 'offsetHeight' ||
1083 - propKey === 'isContentEditable' ||
1084 - propKey === 'outerText' ||
1085 - propKey === 'outerHTML')
1086 - ) {
1087 - // $FlowFixMe - Should be inferred as not undefined.
1088 - extraAttributeNames.delete(propKey.toLowerCase());
1089 - if (__DEV__) {
1090 - console.error(
1091 - 'Assignment to read-only property will result in a no-op: `%s`',
1092 - propKey,
1093 - );
1094 - }
1095 - } else if (
1096 - isCustomComponentTag &&
1097 - !enableCustomElementPropertySupport
1098 - ) {
1099 - // $FlowFixMe - Should be inferred as not undefined.
1100 - extraAttributeNames.delete(propKey.toLowerCase());
1101 - serverValue = getValueForAttribute(
1102 - domElement,
1103 - propKey,
1104 - nextProp,
1105 - isCustomComponentTag,
1106 - );
1107 -
1108 - if (nextProp !== serverValue) {
1109 - warnForPropDifference(propKey, serverValue, nextProp);
1110 - }
1111 - } else if (
1112 - !shouldIgnoreAttribute(propKey, propertyInfo, isCustomComponentTag) &&
1113 - !shouldRemoveAttribute(
1114 - propKey,
1115 - nextProp,
1116 - propertyInfo,
1117 - isCustomComponentTag,
1118 - )
1119 - ) {
1120 - let isMismatchDueToBadCasing = false;
1121 - if (propertyInfo !== null) {
1122 - // $FlowFixMe - Should be inferred as not undefined.
1123 - extraAttributeNames.delete(propertyInfo.attributeName);
1124 - serverValue = getValueForProperty(
1125 - domElement,
1126 - propKey,
1127 - nextProp,
1128 - propertyInfo,
1129 - );
1130 - } else {
1131 - let ownNamespaceDev = parentNamespaceDev;
1132 - if (ownNamespaceDev === HTML_NAMESPACE) {
1133 - ownNamespaceDev = getIntrinsicNamespace(tag);
1134 - }
1135 - if (ownNamespaceDev === HTML_NAMESPACE) {
1136 - // $FlowFixMe - Should be inferred as not undefined.
1137 - extraAttributeNames.delete(propKey.toLowerCase());
1138 - } else {
1139 - const standardName = getPossibleStandardName(propKey);
1140 - if (standardName !== null && standardName !== propKey) {
1141 - // If an SVG prop is supplied with bad casing, it will
1142 - // be successfully parsed from HTML, but will produce a mismatch
1143 - // (and would be incorrectly rendered on the client).
1144 - // However, we already warn about bad casing elsewhere.
1145 - // So we'll skip the misleading extra mismatch warning in this case.
1146 - isMismatchDueToBadCasing = true;
1147 - // $FlowFixMe - Should be inferred as not undefined.
1148 - extraAttributeNames.delete(standardName);
1149 - }
1150 - // $FlowFixMe - Should be inferred as not undefined.
1151 - extraAttributeNames.delete(propKey);
1152 - }
1153 - serverValue = getValueForAttribute(
1154 - domElement,
1155 - propKey,
1156 - nextProp,
1157 - isCustomComponentTag,
1158 - );
1159 - }
1160 -
1161 - const dontWarnCustomElement =
1162 - enableCustomElementPropertySupport &&
1163 - isCustomComponentTag &&
1164 - (typeof nextProp === 'function' || typeof nextProp === 'object');
1165 - if (
1166 - !dontWarnCustomElement &&
1167 - nextProp !== serverValue &&
1168 - !isMismatchDueToBadCasing
1169 - ) {
1170 - warnForPropDifference(propKey, serverValue, nextProp);
1171 - }
1172 - }
1449 + const extraAttributeNames: Set<string> = new Set();
1450 + const attributes = domElement.attributes;
1451 + for (let i = 0; i < attributes.length; i++) {
1452 + const name = attributes[i].name.toLowerCase();
1453 + switch (name) {
1454 + // Controlled attributes are not validated
1455 + // TODO: Only ignore them on controlled tags.
1456 + case 'value':
1457 + break;
1458 + case 'checked':
1459 + break;
1460 + case 'selected':
1461 + break;
1462 + default:
1463 + // Intentionally use the original name.
1464 + // See discussion in https://github.com/facebook/react/pull/10676.
1465 + extraAttributeNames.add(attributes[i].name);
1466 }
1467 }
1175 -
1468 + if (isCustomComponent(tag, rawProps)) {
1469 + diffHydratedCustomComponent(
1470 + domElement,
1471 + tag,
1472 + rawProps,
1473 + parentNamespaceDev,
1474 + extraAttributeNames,
1475 + );
1476 + } else {
1477 + diffHydratedGenericElement(
1478 + domElement,
1479 + tag,
1480 + rawProps,
1481 + parentNamespaceDev,
1482 + extraAttributeNames,
1483 + );
1484 + }
1485 if (
1486 // $FlowFixMe - Should be inferred as not undefined.
1487 extraAttributeNames.size > 0 &&
1179 - rawProps[SUPPRESS_HYDRATION_WARNING] !== true
1488 + rawProps.suppressHydrationWarning !== true
1489 ) {
1490 // $FlowFixMe - Should be inferred as not undefined.
1491 warnForExtraAttributes(extraAttributeNames);
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+6 -2
@@ -70,7 +70,6 @@ import {
70 DOCUMENT_TYPE_NODE,
71 DOCUMENT_FRAGMENT_NODE,
72 } from './HTMLNodeType';
73 -import dangerousStyleValue from './dangerousStyleValue';
73
74 import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
75
@@ -750,7 +749,12 @@ export function unhideInstance(instance: Instance, props: Props): void {
749 styleProp.hasOwnProperty('display')
750 ? styleProp.display
751 : null;
753 - instance.style.display = dangerousStyleValue('display', display);
752 + instance.style.display =
753 + display == null || typeof display === 'boolean'
754 + ? ''
755 + : // The value would've errored already if it wasn't safe.
756 + // eslint-disable-next-line react-internal/safe-string-coercion
757 + ('' + display).trim();
758 }
759
760 export function unhideTextInstance(
packages/react-dom-bindings/src/client/ReactDOMInput.js
+1 -2
@@ -10,7 +10,6 @@
10 // TODO: direct imports like some-package/src/* are bad. Fix me.
11 import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
12
13 -import {setValueForProperty} from './DOMPropertyOperations';
13 import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
14 import {getToStringValue, toString} from './ToStringValue';
15 import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
@@ -130,7 +129,7 @@ export function updateChecked(element: Element, props: Object) {
129 const node = ((element: any): InputWithWrapperState);
130 const checked = props.checked;
131 if (checked != null) {
133 - setValueForProperty(node, 'checked', checked, false);
132 + node.checked = checked;
133 }
134 }
135
packages/react-dom-bindings/src/client/assertValidProps.js deleted
-69
@@ -1,69 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @noflow
8 - */
9 -
10 -import voidElementTags from './voidElementTags';
11 -
12 -const HTML = '__html';
13 -
14 -function assertValidProps(tag: string, props: ?Object) {
15 - if (!props) {
16 - return;
17 - }
18 - // Note the use of `==` which checks for null or undefined.
19 - if (voidElementTags[tag]) {
20 - if (props.children != null || props.dangerouslySetInnerHTML != null) {
21 - throw new Error(
22 - `${tag} is a void element tag and must neither have \`children\` nor ` +
23 - 'use `dangerouslySetInnerHTML`.',
24 - );
25 - }
26 - }
27 - if (props.dangerouslySetInnerHTML != null) {
28 - if (props.children != null) {
29 - throw new Error(
30 - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
31 - );
32 - }
33 -
34 - if (
35 - typeof props.dangerouslySetInnerHTML !== 'object' ||
36 - !(HTML in props.dangerouslySetInnerHTML)
37 - ) {
38 - throw new Error(
39 - '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
40 - 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
41 - 'for more information.',
42 - );
43 - }
44 - }
45 - if (__DEV__) {
46 - if (
47 - !props.suppressContentEditableWarning &&
48 - props.contentEditable &&
49 - props.children != null
50 - ) {
51 - console.error(
52 - 'A component is `contentEditable` and contains `children` managed by ' +
53 - 'React. It is now your responsibility to guarantee that none of ' +
54 - 'those nodes are unexpectedly modified or duplicated. This is ' +
55 - 'probably not intentional.',
56 - );
57 - }
58 - }
59 -
60 - if (props.style != null && typeof props.style !== 'object') {
61 - throw new Error(
62 - 'The `style` prop expects a mapping from style properties to values, ' +
63 - "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
64 - 'using JSX.',
65 - );
66 - }
67 -}
68 -
69 -export default assertValidProps;
packages/react-dom-bindings/src/client/dangerousStyleValue.js deleted
-51
@@ -1,51 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import {isUnitlessNumber} from '../shared/CSSProperty';
9 -import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
10 -
11 -/**
12 - * Convert a value into the proper css writable value. The style name `name`
13 - * should be logical (no hyphens), as specified
14 - * in `CSSProperty.isUnitlessNumber`.
15 - *
16 - * @param {string} name CSS property name such as `topMargin`.
17 - * @param {*} value CSS property value such as `10px`.
18 - * @return {string} Normalized style value with dimensions applied.
19 - */
20 -function dangerousStyleValue(name, value, isCustomProperty) {
21 - // Note that we've removed escapeTextForBrowser() calls here since the
22 - // whole string will be escaped when the attribute is injected into
23 - // the markup. If you provide unsafe user data here they can inject
24 - // arbitrary CSS which may be problematic (I couldn't repro this):
25 - // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
26 - // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
27 - // This is not an XSS hole but instead a potential CSS injection issue
28 - // which has lead to a greater discussion about how we're going to
29 - // trust URLs moving forward. See #2115901
30 -
31 - const isEmpty = value == null || typeof value === 'boolean' || value === '';
32 - if (isEmpty) {
33 - return '';
34 - }
35 -
36 - if (
37 - !isCustomProperty &&
38 - typeof value === 'number' &&
39 - value !== 0 &&
40 - !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])
41 - ) {
42 - return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
43 - }
44 -
45 - if (__DEV__) {
46 - checkCSSPropertyStringCoercion(value, name);
47 - }
48 - return ('' + value).trim();
49 -}
50 -
51 -export default dangerousStyleValue;
packages/react-dom-bindings/src/client/omittedCloseTags.js deleted
-30
@@ -1,30 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -// For HTML, certain tags should omit their close tag. We keep a list for
9 -// those special-case tags.
10 -
11 -const omittedCloseTags = {
12 - area: true,
13 - base: true,
14 - br: true,
15 - col: true,
16 - embed: true,
17 - hr: true,
18 - img: true,
19 - input: true,
20 - keygen: true,
21 - link: true,
22 - meta: true,
23 - param: true,
24 - source: true,
25 - track: true,
26 - wbr: true,
27 - // NOTE: menuitem's close tag should be omitted, but that causes problems.
28 -};
29 -
30 -export default omittedCloseTags;
packages/react-dom-bindings/src/client/voidElementTags.js deleted
-18
@@ -1,18 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -import omittedCloseTags from './omittedCloseTags';
9 -
10 -// For HTML, certain tags cannot have children. This has the same purpose as
11 -// `omittedCloseTags` except that `menuitem` should still have its closing tag.
12 -
13 -const voidElementTags = {
14 - menuitem: true,
15 - ...omittedCloseTags,
16 -};
17 -
18 -export default voidElementTags;
packages/react-dom-bindings/src/shared/DOMProperty.js
-157
@@ -7,18 +7,10 @@
7 * @flow
8 */
9
10 -import {
11 - enableFilterEmptyStringAttributesDOM,
12 - enableCustomElementPropertySupport,
13 -} from 'shared/ReactFeatureFlags';
10 import hasOwnProperty from 'shared/hasOwnProperty';
11
12 type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
13
18 -// A reserved attribute.
19 -// It is handled by React separately and shouldn't be written to the DOM.
20 -export const RESERVED = 0;
21 -
14 // A simple string attribute.
15 // Attributes that aren't in the filter are presumed to have this type.
16 export const STRING = 1;
@@ -91,124 +83,6 @@ export function isAttributeNameSafe(attributeName: string): boolean {
83 return false;
84 }
85
94 -export function shouldIgnoreAttribute(
95 - name: string,
96 - propertyInfo: PropertyInfo | null,
97 - isCustomComponentTag: boolean,
98 -): boolean {
99 - if (propertyInfo !== null) {
100 - return propertyInfo.type === RESERVED;
101 - }
102 - if (isCustomComponentTag) {
103 - return false;
104 - }
105 - if (
106 - name.length > 2 &&
107 - (name[0] === 'o' || name[0] === 'O') &&
108 - (name[1] === 'n' || name[1] === 'N')
109 - ) {
110 - return true;
111 - }
112 - return false;
113 -}
114 -
115 -export function shouldRemoveAttributeWithWarning(
116 - name: string,
117 - value: mixed,
118 - propertyInfo: PropertyInfo | null,
119 - isCustomComponentTag: boolean,
120 -): boolean {
121 - if (propertyInfo !== null && propertyInfo.type === RESERVED) {
122 - return false;
123 - }
124 - switch (typeof value) {
125 - case 'function':
126 - case 'symbol': // eslint-disable-line
127 - return true;
128 - case 'boolean': {
129 - if (isCustomComponentTag) {
130 - return false;
131 - }
132 - if (propertyInfo !== null) {
133 - return !propertyInfo.acceptsBooleans;
134 - } else {
135 - const prefix = name.toLowerCase().slice(0, 5);
136 - return prefix !== 'data-' && prefix !== 'aria-';
137 - }
138 - }
139 - default:
140 - return false;
141 - }
142 -}
143 -
144 -export function shouldRemoveAttribute(
145 - name: string,
146 - value: mixed,
147 - propertyInfo: PropertyInfo | null,
148 - isCustomComponentTag: boolean,
149 -): boolean {
150 - if (value === null || typeof value === 'undefined') {
151 - return true;
152 - }
153 - if (
154 - shouldRemoveAttributeWithWarning(
155 - name,
156 - value,
157 - propertyInfo,
158 - isCustomComponentTag,
159 - )
160 - ) {
161 - return true;
162 - }
163 - if (isCustomComponentTag) {
164 - if (enableCustomElementPropertySupport) {
165 - if (value === false) {
166 - return true;
167 - }
168 - }
169 - return false;
170 - }
171 - if (propertyInfo !== null) {
172 - if (enableFilterEmptyStringAttributesDOM) {
173 - if (propertyInfo.removeEmptyString && value === '') {
174 - if (__DEV__) {
175 - if (name === 'src') {
176 - console.error(
177 - 'An empty string ("") was passed to the %s attribute. ' +
178 - 'This may cause the browser to download the whole page again over the network. ' +
179 - 'To fix this, either do not render the element at all ' +
180 - 'or pass null to %s instead of an empty string.',
181 - name,
182 - name,
183 - );
184 - } else {
185 - console.error(
186 - 'An empty string ("") was passed to the %s attribute. ' +
187 - 'To fix this, either do not render the element at all ' +
188 - 'or pass null to %s instead of an empty string.',
189 - name,
190 - name,
191 - );
192 - }
193 - }
194 - return true;
195 - }
196 - }
197 -
198 - switch (propertyInfo.type) {
199 - case BOOLEAN:
200 - return !value;
201 - case OVERLOADED_BOOLEAN:
202 - return value === false;
203 - case NUMERIC:
204 - return isNaN(value);
205 - case POSITIVE_NUMERIC:
206 - return isNaN(value) || (value: any) < 1;
207 - }
208 - }
209 - return false;
210 -}
211 -
86 export function getPropertyInfo(name: string): PropertyInfo | null {
87 return properties.hasOwnProperty(name) ? properties[name] : null;
88 }
@@ -241,37 +115,6 @@ function PropertyInfoRecord(
115 // name warnings.
116 const properties: {[string]: $FlowFixMe} = {};
117
244 -// These props are reserved by React. They shouldn't be written to the DOM.
245 -const reservedProps = [
246 - 'children',
247 - 'dangerouslySetInnerHTML',
248 - // TODO: This prevents the assignment of defaultValue to regular
249 - // elements (not just inputs). Now that ReactDOMInput assigns to the
250 - // defaultValue property -- do we need this?
251 - 'defaultValue',
252 - 'defaultChecked',
253 - 'innerHTML',
254 - 'suppressContentEditableWarning',
255 - 'suppressHydrationWarning',
256 - 'style',
257 -];
258 -if (enableCustomElementPropertySupport) {
259 - reservedProps.push('innerText', 'textContent');
260 -}
261 -
262 -reservedProps.forEach(name => {
263 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
264 - properties[name] = new PropertyInfoRecord(
265 - name,
266 - RESERVED,
267 - false, // mustUseProperty
268 - name, // attributeName
269 - null, // attributeNamespace
270 - false, // sanitizeURL
271 - false, // removeEmptyString
272 - );
273 -});
274 -
118 // A few React string attributes have a different name.
119 // This is a mapping from React prop names to the attribute names.
120 [
packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js
+61 -47
@@ -5,16 +5,11 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {
9 - ATTRIBUTE_NAME_CHAR,
10 - BOOLEAN,
11 - RESERVED,
12 - shouldRemoveAttributeWithWarning,
13 - getPropertyInfo,
14 -} from './DOMProperty';
8 +import {ATTRIBUTE_NAME_CHAR, BOOLEAN, getPropertyInfo} from './DOMProperty';
9 import isCustomComponent from './isCustomComponent';
10 import possibleStandardNames from './possibleStandardNames';
11 import hasOwnProperty from 'shared/hasOwnProperty';
12 +import {enableCustomElementPropertySupport} from 'shared/ReactFeatureFlags';
13
14 const warnedProperties = {};
15 const EVENT_NAME_REGEX = /^on./;
@@ -136,7 +131,6 @@ function validateProperty(tagName, name, value, eventRegistry) {
131 }
132
133 const propertyInfo = getPropertyInfo(name);
139 - const isReserved = propertyInfo !== null && propertyInfo.type === RESERVED;
134
135 // Known attributes should match the casing specified in the property config.
136 if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
@@ -150,7 +144,7 @@ function validateProperty(tagName, name, value, eventRegistry) {
144 warnedProperties[name] = true;
145 return true;
146 }
153 - } else if (!isReserved && name !== lowerCasedName) {
147 + } else if (name !== lowerCasedName) {
148 // Unknown attributes should have lowercase casing since that's how they
149 // will be cased anyway with server rendering.
150 console.error(
@@ -166,51 +160,71 @@ function validateProperty(tagName, name, value, eventRegistry) {
160 return true;
161 }
162
169 - if (
170 - typeof value === 'boolean' &&
171 - shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)
172 - ) {
173 - if (value) {
174 - console.error(
175 - 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
176 - 'If you want to write it to the DOM, pass a string instead: ' +
177 - '%s="%s" or %s={value.toString()}.',
178 - value,
179 - name,
180 - name,
181 - value,
182 - name,
183 - );
184 - } else {
185 - console.error(
186 - 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
187 - 'If you want to write it to the DOM, pass a string instead: ' +
188 - '%s="%s" or %s={value.toString()}.\n\n' +
189 - 'If you used to conditionally omit it with %s={condition && value}, ' +
190 - 'pass %s={condition ? value : undefined} instead.',
191 - value,
192 - name,
193 - name,
194 - value,
195 - name,
196 - name,
197 - name,
198 - );
163 + // Now that we've validated casing, do not validate
164 + // data types for reserved props
165 + switch (name) {
166 + case 'dangerouslySetInnerHTML':
167 + case 'children':
168 + case 'style':
169 + case 'suppressContentEditableWarning':
170 + case 'suppressHydrationWarning':
171 + case 'defaultValue': // Reserved
172 + case 'defaultChecked':
173 + case 'innerHTML': {
174 + return true;
175 }
200 - warnedProperties[name] = true;
201 - return true;
176 + case 'innerText': // Properties
177 + case 'textContent':
178 + if (enableCustomElementPropertySupport) {
179 + return true;
180 + }
181 }
182
204 - // Now that we've validated casing, do not validate
205 - // data types for reserved props
206 - if (isReserved) {
183 + if (typeof value === 'boolean') {
184 + const prefix = name.toLowerCase().slice(0, 5);
185 + const acceptsBooleans =
186 + propertyInfo !== null
187 + ? propertyInfo.acceptsBooleans
188 + : prefix === 'data-' || prefix === 'aria-';
189 + if (!acceptsBooleans) {
190 + if (value) {
191 + console.error(
192 + 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
193 + 'If you want to write it to the DOM, pass a string instead: ' +
194 + '%s="%s" or %s={value.toString()}.',
195 + value,
196 + name,
197 + name,
198 + value,
199 + name,
200 + );
201 + } else {
202 + console.error(
203 + 'Received `%s` for a non-boolean attribute `%s`.\n\n' +
204 + 'If you want to write it to the DOM, pass a string instead: ' +
205 + '%s="%s" or %s={value.toString()}.\n\n' +
206 + 'If you used to conditionally omit it with %s={condition && value}, ' +
207 + 'pass %s={condition ? value : undefined} instead.',
208 + value,
209 + name,
210 + name,
211 + value,
212 + name,
213 + name,
214 + name,
215 + );
216 + }
217 + }
218 + warnedProperties[name] = true;
219 return true;
220 }
221
222 // Warn when a known attribute is a bad type
211 - if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {
212 - warnedProperties[name] = true;
213 - return false;
223 + switch (typeof value) {
224 + case 'function':
225 + case 'symbol': // eslint-disable-line
226 + warnedProperties[name] = true;
227 + return false;
228 }
229
230 // Warn when passing the strings 'false' or 'true' into a boolean prop
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+2 -1
@@ -1369,7 +1369,8 @@ describe('ReactDOMComponent', () => {
1369 expect(function () {
1370 mountComponent({children: '', dangerouslySetInnerHTML: ''});
1371 }).toThrowError(
1372 - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
1372 + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1373 + 'Please visit https://reactjs.org/link/dangerously-set-inner-html for more information.',
1374 );
1375 });
1376