Refactor DOM special cases per tags including controlled fields (#26501)
I use a shared helper when setting properties into a helper whether it's initial or update. I moved the special cases per tag to commit phase so we can check it only once. This also effectively inlines getHostProps which can be done in a single check per prop key. The diffProperties operation is simplified to mostly just generating a plain diff of all properties, generating an update payload. This might generate a few more entries that are now ignored in the commit phase. that previously would've been ignored earlier. We could skip this and just do the whole diff in the commit phase by always scheduling a commit phase update. I tested the attribute table (one change documented below) and a few select DOM fixtures.
Sebastian Markbåge committed
Mar 28, 2023 at 22:40 UTC
85de6fde515148babd36eae2b7384ad8e62b732a
5 files changed
+592
-650
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+533
-590
@@ -7,6 +7,8 @@
7
* @flow
8
*/
9
10
+import type {InputWithWrapperState} from './ReactDOMInput';
11
+
12
import {
13
registrationNameDependencies,
14
possibleRegistrationNames,
@@ -25,7 +27,6 @@ import {
27
} from './DOMPropertyOperations';
28
import {
29
initWrapperState as ReactDOMInputInitWrapperState,
28
- getHostProps as ReactDOMInputGetHostProps,
30
postMountWrapper as ReactDOMInputPostMountWrapper,
31
updateChecked as ReactDOMInputUpdateChecked,
32
updateWrapper as ReactDOMInputUpdateWrapper,
@@ -37,14 +38,12 @@ import {
38
} from './ReactDOMOption';
39
import {
40
initWrapperState as ReactDOMSelectInitWrapperState,
40
- getHostProps as ReactDOMSelectGetHostProps,
41
postMountWrapper as ReactDOMSelectPostMountWrapper,
42
restoreControlledState as ReactDOMSelectRestoreControlledState,
43
postUpdateWrapper as ReactDOMSelectPostUpdateWrapper,
44
} from './ReactDOMSelect';
45
import {
46
initWrapperState as ReactDOMTextareaInitWrapperState,
47
- getHostProps as ReactDOMTextareaGetHostProps,
47
postMountWrapper as ReactDOMTextareaPostMountWrapper,
48
updateWrapper as ReactDOMTextareaUpdateWrapper,
49
restoreControlledState as ReactDOMTextareaRestoreControlledState,
@@ -287,162 +286,131 @@ export function trapClickOnNonInteractiveElement(node: HTMLElement) {
286
node.onclick = noop;
287
}
288
290
-function setInitialDOMProperties(
291
- tag: string,
289
+function setProp(
290
domElement: Element,
293
- nextProps: Object,
291
+ tag: string,
292
+ key: string,
293
+ value: mixed,
294
isCustomComponentTag: boolean,
295
+ props: any,
296
): void {
296
- for (const propKey in nextProps) {
297
- if (!nextProps.hasOwnProperty(propKey)) {
298
- continue;
297
+ switch (key) {
298
+ case 'style': {
299
+ if (value != null && typeof value !== 'object') {
300
+ throw new Error(
301
+ 'The `style` prop expects a mapping from style properties to values, ' +
302
+ "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
303
+ 'using JSX.',
304
+ );
305
+ }
306
+ if (__DEV__) {
307
+ if (value) {
308
+ // Freeze the next style object so that we can assume it won't be
309
+ // mutated. We have already warned for this in the past.
310
+ Object.freeze(value);
311
+ }
312
+ }
313
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
314
+ setValueForStyles(domElement, value);
315
+ break;
316
}
300
- const nextProp = nextProps[propKey];
301
- switch (propKey) {
302
- case 'style': {
303
- if (nextProp != null && typeof nextProp !== 'object') {
317
+ case 'dangerouslySetInnerHTML': {
318
+ if (value != null) {
319
+ if (typeof value !== 'object' || !('__html' in value)) {
320
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.',
321
+ '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
322
+ 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
323
+ 'for more information.',
324
);
325
}
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
- }
321
- case 'dangerouslySetInnerHTML': {
322
- if (nextProp != null) {
323
- if (typeof nextProp !== 'object' || !('__html' in nextProp)) {
326
+ const nextHtml: any = value.__html;
327
+ if (nextHtml != null) {
328
+ if (props.children != null) {
329
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.',
330
+ 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
331
);
332
}
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
- }
333
+ if (disableIEWorkarounds) {
334
+ domElement.innerHTML = nextHtml;
335
+ } else {
336
+ setInnerHTML(domElement, nextHtml);
337
}
338
}
344
- break;
339
}
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
- }
340
+ break;
341
+ }
342
+ case 'children': {
343
+ if (typeof value === 'string') {
344
+ // Avoid setting initial textContent when the text is empty. In IE11 setting
345
+ // textContent on a <textarea> will cause the placeholder to not
346
+ // show within the <textarea> until it has been focused and blurred again.
347
+ // https://github.com/facebook/react/issues/6731#issuecomment-254874553
348
+ const canSetTextContent =
349
+ (!enableHostSingletons || tag !== 'body') &&
350
+ (tag !== 'textarea' || value !== '');
351
+ if (canSetTextContent) {
352
+ setTextContent(domElement, value);
353
}
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);
354
+ } else if (typeof value === 'number') {
355
+ const canSetTextContent = !enableHostSingletons || tag !== 'body';
356
+ if (canSetTextContent) {
357
+ setTextContent(domElement, '' + value);
358
}
373
- break;
359
}
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;
360
+ break;
361
+ }
362
+ case 'onScroll': {
363
+ if (value != null) {
364
+ if (__DEV__ && typeof value !== 'function') {
365
+ warnForInvalidEventListener(key, value);
366
}
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
- }
367
+ listenToNonDelegatedEvent('scroll', domElement);
368
+ }
369
+ break;
370
+ }
371
+ case 'onClick': {
372
+ // TODO: This cast may not be sound for SVG, MathML or custom elements.
373
+ if (value != null) {
374
+ if (__DEV__ && typeof value !== 'function') {
375
+ warnForInvalidEventListener(key, value);
376
}
377
+ trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
378
}
379
+ break;
380
}
412
- }
413
-}
414
-
415
-function updateDOMProperties(
416
- domElement: Element,
417
- updatePayload: Array<any>,
418
- wasCustomComponentTag: boolean,
419
- isCustomComponentTag: boolean,
420
-): void {
421
- // TODO: Handle wasCustomComponentTag
422
- for (let i = 0; i < updatePayload.length; i += 2) {
423
- const propKey = updatePayload[i];
424
- const propValue = updatePayload[i + 1];
425
- switch (propKey) {
426
- case 'style':
427
- setValueForStyles(domElement, propValue);
381
+ case 'suppressContentEditableWarning':
382
+ case 'suppressHydrationWarning':
383
+ case 'defaultValue': // Reserved
384
+ case 'defaultChecked':
385
+ case 'innerHTML': {
386
+ // Noop
387
+ break;
388
+ }
389
+ case 'autoFocus': {
390
+ // We polyfill it separately on the client during commit.
391
+ // We could have excluded it in the property list instead of
392
+ // adding a special case here, but then it wouldn't be emitted
393
+ // on server rendering (but we *do* want to emit it in SSR).
394
+ break;
395
+ }
396
+ case 'innerText': // Properties
397
+ case 'textContent':
398
+ if (enableCustomElementPropertySupport) {
399
break;
429
- case 'dangerouslySetInnerHTML':
430
- if (disableIEWorkarounds) {
431
- domElement.innerHTML = propValue;
432
- } else {
433
- setInnerHTML(domElement, propValue);
400
+ }
401
+ // eslint-disable-next-line no-fallthrough
402
+ default: {
403
+ if (registrationNameDependencies.hasOwnProperty(key)) {
404
+ if (__DEV__ && value != null && typeof value !== 'function') {
405
+ warnForInvalidEventListener(key, value);
406
}
435
- break;
436
- case 'children':
437
- setTextContent(domElement, propValue);
438
- break;
439
- default:
407
+ } else {
408
if (isCustomComponentTag) {
441
- setValueForPropertyOnCustomComponent(domElement, propKey, propValue);
409
+ setValueForPropertyOnCustomComponent(domElement, key, value);
410
} else {
443
- setValueForProperty(domElement, propKey, propValue);
411
+ setValueForProperty(domElement, key, value);
412
}
445
- break;
413
+ }
414
}
415
}
416
}
@@ -580,124 +548,194 @@ export function createTextNode(
548
export function setInitialProperties(
549
domElement: Element,
550
tag: string,
583
- rawProps: Object,
551
+ props: Object,
552
): void {
585
- const isCustomComponentTag = isCustomComponent(tag, rawProps);
553
if (__DEV__) {
587
- validatePropertiesInDevelopment(tag, rawProps);
554
+ validatePropertiesInDevelopment(tag, props);
555
}
556
557
// TODO: Make sure that we check isMounted before firing any of these events.
591
- let props: Object;
558
+
559
switch (tag) {
593
- case 'dialog':
560
+ case 'input': {
561
+ ReactDOMInputInitWrapperState(domElement, props);
562
+ // We listen to this event in case to ensure emulated bubble
563
+ // listeners still fire for the invalid event.
564
+ listenToNonDelegatedEvent('invalid', domElement);
565
+ for (const propKey in props) {
566
+ if (!props.hasOwnProperty(propKey)) {
567
+ continue;
568
+ }
569
+ const propValue = props[propKey];
570
+ if (propValue == null) {
571
+ continue;
572
+ }
573
+ switch (propKey) {
574
+ case 'checked': {
575
+ const node = ((domElement: any): InputWithWrapperState);
576
+ const checked =
577
+ propValue != null ? propValue : node._wrapperState.initialChecked;
578
+ node.checked =
579
+ !!checked &&
580
+ typeof checked !== 'function' &&
581
+ checked !== 'symbol';
582
+ break;
583
+ }
584
+ case 'value': {
585
+ // This is handled by updateWrapper below.
586
+ break;
587
+ }
588
+ case 'children':
589
+ case 'dangerouslySetInnerHTML': {
590
+ if (propValue != null) {
591
+ throw new Error(
592
+ `${tag} is a void element tag and must neither have \`children\` nor ` +
593
+ 'use `dangerouslySetInnerHTML`.',
594
+ );
595
+ }
596
+ break;
597
+ }
598
+ // defaultChecked and defaultValue are ignored by setProp
599
+ default: {
600
+ setProp(domElement, tag, propKey, propValue, false, props);
601
+ }
602
+ }
603
+ }
604
+ // TODO: Make sure we check if this is still unmounted or do any clean
605
+ // up necessary since we never stop tracking anymore.
606
+ track((domElement: any));
607
+ ReactDOMInputPostMountWrapper(domElement, props, false);
608
+ return;
609
+ }
610
+ case 'select': {
611
+ ReactDOMSelectInitWrapperState(domElement, props);
612
+ // We listen to this event in case to ensure emulated bubble
613
+ // listeners still fire for the invalid event.
614
+ listenToNonDelegatedEvent('invalid', domElement);
615
+ for (const propKey in props) {
616
+ if (!props.hasOwnProperty(propKey)) {
617
+ continue;
618
+ }
619
+ const propValue = props[propKey];
620
+ if (propValue == null) {
621
+ continue;
622
+ }
623
+ switch (propKey) {
624
+ case 'value': {
625
+ // This is handled by updateWrapper below.
626
+ break;
627
+ }
628
+ // defaultValue are ignored by setProp
629
+ default: {
630
+ setProp(domElement, tag, propKey, propValue, false, props);
631
+ }
632
+ }
633
+ }
634
+ ReactDOMSelectPostMountWrapper(domElement, props);
635
+ return;
636
+ }
637
+ case 'textarea': {
638
+ ReactDOMTextareaInitWrapperState(domElement, props);
639
+ // We listen to this event in case to ensure emulated bubble
640
+ // listeners still fire for the invalid event.
641
+ listenToNonDelegatedEvent('invalid', domElement);
642
+ for (const propKey in props) {
643
+ if (!props.hasOwnProperty(propKey)) {
644
+ continue;
645
+ }
646
+ const propValue = props[propKey];
647
+ if (propValue == null) {
648
+ continue;
649
+ }
650
+ switch (propKey) {
651
+ case 'value': {
652
+ // This is handled by updateWrapper below.
653
+ break;
654
+ }
655
+ case 'children': {
656
+ // TODO: Handled by initWrapperState above.
657
+ break;
658
+ }
659
+ case 'dangerouslySetInnerHTML': {
660
+ if (propValue != null) {
661
+ // TODO: Do we really need a special error message for this. It's also pretty blunt.
662
+ throw new Error(
663
+ '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
664
+ );
665
+ }
666
+ break;
667
+ }
668
+ // defaultValue is ignored by setProp
669
+ default: {
670
+ setProp(domElement, tag, propKey, propValue, false, props);
671
+ }
672
+ }
673
+ }
674
+ // TODO: Make sure we check if this is still unmounted or do any clean
675
+ // up necessary since we never stop tracking anymore.
676
+ track((domElement: any));
677
+ ReactDOMTextareaPostMountWrapper(domElement, props);
678
+ return;
679
+ }
680
+ case 'option': {
681
+ ReactDOMOptionValidateProps(domElement, props);
682
+ for (const propKey in props) {
683
+ if (!props.hasOwnProperty(propKey)) {
684
+ continue;
685
+ }
686
+ const propValue = props[propKey];
687
+ if (propValue == null) {
688
+ continue;
689
+ }
690
+ setProp(domElement, tag, propKey, propValue, false, props);
691
+ }
692
+ ReactDOMOptionPostMountWrapper(domElement, props);
693
+ return;
694
+ }
695
+ case 'dialog': {
696
listenToNonDelegatedEvent('cancel', domElement);
697
listenToNonDelegatedEvent('close', domElement);
596
- props = rawProps;
698
break;
598
- case 'embed':
599
- if (
600
- rawProps.children != null ||
601
- rawProps.dangerouslySetInnerHTML != null
602
- ) {
603
- // TODO: Can we make this a DEV warning to avoid this deny list?
604
- throw new Error(
605
- `${tag} is a void element tag and must neither have \`children\` nor ` +
606
- 'use `dangerouslySetInnerHTML`.',
607
- );
608
- }
609
- // eslint-disable-next-line no-fallthrough
699
+ }
700
case 'iframe':
611
- case 'object':
701
+ case 'object': {
702
// We listen to this event in case to ensure emulated bubble
703
// listeners still fire for the load event.
704
listenToNonDelegatedEvent('load', domElement);
615
- props = rawProps;
705
break;
706
+ }
707
case 'video':
618
- case 'audio':
708
+ case 'audio': {
709
// We listen to these events in case to ensure emulated bubble
710
// listeners still fire for all the media events.
711
for (let i = 0; i < mediaEventTypes.length; i++) {
712
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
713
}
624
- props = rawProps;
714
break;
626
- case 'source':
627
- if (
628
- rawProps.children != null ||
629
- rawProps.dangerouslySetInnerHTML != null
630
- ) {
631
- // TODO: Can we make this a DEV warning to avoid this deny list?
632
- throw new Error(
633
- `${tag} is a void element tag and must neither have \`children\` nor ` +
634
- 'use `dangerouslySetInnerHTML`.',
635
- );
636
- }
637
- // We listen to this event in case to ensure emulated bubble
638
- // listeners still fire for the error event.
639
- listenToNonDelegatedEvent('error', domElement);
640
- props = rawProps;
641
- break;
642
- case 'img':
643
- case 'link':
644
- if (
645
- rawProps.children != null ||
646
- rawProps.dangerouslySetInnerHTML != null
647
- ) {
648
- throw new Error(
649
- `${tag} is a void element tag and must neither have \`children\` nor ` +
650
- 'use `dangerouslySetInnerHTML`.',
651
- );
652
- }
653
- // eslint-disable-next-line no-fallthrough
654
- case 'image':
715
+ }
716
+ case 'image': {
717
// We listen to these events in case to ensure emulated bubble
718
// listeners still fire for error and load events.
719
listenToNonDelegatedEvent('error', domElement);
720
listenToNonDelegatedEvent('load', domElement);
659
- props = rawProps;
721
break;
661
- case 'details':
722
+ }
723
+ case 'details': {
724
// We listen to this event in case to ensure emulated bubble
725
// listeners still fire for the toggle event.
726
listenToNonDelegatedEvent('toggle', domElement);
665
- props = rawProps;
666
- break;
667
- case 'input':
668
- if (
669
- rawProps.children != null ||
670
- rawProps.dangerouslySetInnerHTML != null
671
- ) {
672
- throw new Error(
673
- `${tag} is a void element tag and must neither have \`children\` nor ` +
674
- 'use `dangerouslySetInnerHTML`.',
675
- );
676
- }
677
- ReactDOMInputInitWrapperState(domElement, rawProps);
678
- props = ReactDOMInputGetHostProps(domElement, rawProps);
679
- // We listen to this event in case to ensure emulated bubble
680
- // listeners still fire for the invalid event.
681
- listenToNonDelegatedEvent('invalid', domElement);
682
- break;
683
- case 'option':
684
- ReactDOMOptionValidateProps(domElement, rawProps);
685
- props = rawProps;
686
- break;
687
- case 'select':
688
- ReactDOMSelectInitWrapperState(domElement, rawProps);
689
- props = ReactDOMSelectGetHostProps(domElement, rawProps);
690
- // We listen to this event in case to ensure emulated bubble
691
- // listeners still fire for the invalid event.
692
- listenToNonDelegatedEvent('invalid', domElement);
693
- break;
694
- case 'textarea':
695
- ReactDOMTextareaInitWrapperState(domElement, rawProps);
696
- props = ReactDOMTextareaGetHostProps(domElement, rawProps);
697
- // We listen to this event in case to ensure emulated bubble
698
- // listeners still fire for the invalid event.
699
- listenToNonDelegatedEvent('invalid', domElement);
727
break;
728
+ }
729
+ case 'embed':
730
+ case 'source':
731
+ case 'img':
732
+ case 'link': {
733
+ // These are void elements that also need delegated events.
734
+ listenToNonDelegatedEvent('error', domElement);
735
+ listenToNonDelegatedEvent('load', domElement);
736
+ // We fallthrough to the return of the void elements
737
+ }
738
+ // eslint-disable-next-line no-fallthrough
739
case 'area':
740
case 'base':
741
case 'br':
@@ -709,49 +747,45 @@ export function setInitialProperties(
747
case 'track':
748
case 'wbr':
749
case 'menuitem': {
712
- if (
713
- rawProps.children != null ||
714
- rawProps.dangerouslySetInnerHTML != null
715
- ) {
716
- // TODO: Can we make this a DEV warning to avoid this deny list?
717
- throw new Error(
718
- `${tag} is a void element tag and must neither have \`children\` nor ` +
719
- 'use `dangerouslySetInnerHTML`.',
720
- );
750
+ // Void elements
751
+ for (const propKey in props) {
752
+ if (!props.hasOwnProperty(propKey)) {
753
+ continue;
754
+ }
755
+ const propValue = props[propKey];
756
+ if (propValue == null) {
757
+ continue;
758
+ }
759
+ switch (propKey) {
760
+ case 'children':
761
+ case 'dangerouslySetInnerHTML': {
762
+ // TODO: Can we make this a DEV warning to avoid this deny list?
763
+ throw new Error(
764
+ `${tag} is a void element tag and must neither have \`children\` nor ` +
765
+ 'use `dangerouslySetInnerHTML`.',
766
+ );
767
+ }
768
+ // defaultChecked and defaultValue are ignored by setProp
769
+ default: {
770
+ // TODO: If the `is` prop is specified, this should go through the isCustomComponentTag flow.
771
+ setProp(domElement, tag, propKey, propValue, false, props);
772
+ }
773
+ }
774
}
775
+ return;
776
}
723
- // eslint-disable-next-line no-fallthrough
724
- default:
725
- props = rawProps;
777
}
778
728
- setInitialDOMProperties(tag, domElement, props, isCustomComponentTag);
729
-
730
- switch (tag) {
731
- case 'input':
732
- // TODO: Make sure we check if this is still unmounted or do any clean
733
- // up necessary since we never stop tracking anymore.
734
- track((domElement: any));
735
- ReactDOMInputPostMountWrapper(domElement, rawProps, false);
736
- break;
737
- case 'textarea':
738
- // TODO: Make sure we check if this is still unmounted or do any clean
739
- // up necessary since we never stop tracking anymore.
740
- track((domElement: any));
741
- ReactDOMTextareaPostMountWrapper(domElement, rawProps);
742
- break;
743
- case 'option':
744
- ReactDOMOptionPostMountWrapper(domElement, rawProps);
745
- break;
746
- case 'select':
747
- ReactDOMSelectPostMountWrapper(domElement, rawProps);
748
- break;
749
- default:
750
- if (typeof props.onClick === 'function') {
751
- // TODO: This cast may not be sound for SVG, MathML or custom elements.
752
- trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
753
- }
754
- break;
779
+ const isCustomComponentTag = isCustomComponent(tag, props);
780
+ for (const propKey in props) {
781
+ if (!props.hasOwnProperty(propKey)) {
782
+ continue;
783
+ }
784
+ const propValue = props[propKey];
785
+ if (propValue == null) {
786
+ continue;
787
+ }
788
+ setProp(domElement, tag, propKey, propValue, isCustomComponentTag, props);
789
}
790
}
791
@@ -759,82 +793,15 @@ export function setInitialProperties(
793
export function diffProperties(
794
domElement: Element,
795
tag: string,
762
- lastRawProps: Object,
763
- nextRawProps: Object,
796
+ lastProps: Object,
797
+ nextProps: Object,
798
): null | Array<mixed> {
799
if (__DEV__) {
766
- validatePropertiesInDevelopment(tag, nextRawProps);
800
+ validatePropertiesInDevelopment(tag, nextProps);
801
}
802
803
let updatePayload: null | Array<any> = null;
804
771
- let lastProps: Object;
772
- let nextProps: Object;
773
- switch (tag) {
774
- case 'input':
775
- if (
776
- nextRawProps.children != null ||
777
- nextRawProps.dangerouslySetInnerHTML != null
778
- ) {
779
- throw new Error(
780
- `${tag} is a void element tag and must neither have \`children\` nor ` +
781
- 'use `dangerouslySetInnerHTML`.',
782
- );
783
- }
784
- lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
785
- nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
786
- updatePayload = [];
787
- break;
788
- case 'select':
789
- lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps);
790
- nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps);
791
- updatePayload = [];
792
- break;
793
- case 'textarea':
794
- lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps);
795
- nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps);
796
- updatePayload = [];
797
- break;
798
- case 'img':
799
- case 'link':
800
- case 'area':
801
- case 'base':
802
- case 'br':
803
- case 'col':
804
- case 'embed':
805
- case 'hr':
806
- case 'keygen':
807
- case 'meta':
808
- case 'param':
809
- case 'source':
810
- case 'track':
811
- case 'wbr':
812
- case 'menuitem': {
813
- if (
814
- nextRawProps.children != null ||
815
- nextRawProps.dangerouslySetInnerHTML != null
816
- ) {
817
- // TODO: Can we make this a DEV warning to avoid this deny list?
818
- throw new Error(
819
- `${tag} is a void element tag and must neither have \`children\` nor ` +
820
- 'use `dangerouslySetInnerHTML`.',
821
- );
822
- }
823
- }
824
- // eslint-disable-next-line no-fallthrough
825
- default:
826
- lastProps = lastRawProps;
827
- nextProps = nextRawProps;
828
- if (
829
- typeof lastProps.onClick !== 'function' &&
830
- typeof nextProps.onClick === 'function'
831
- ) {
832
- // TODO: This cast may not be sound for SVG, MathML or custom elements.
833
- trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
834
- }
835
- break;
836
- }
837
-
805
let propKey;
806
let styleName;
807
let styleUpdates = null;
@@ -859,42 +826,10 @@ export function diffProperties(
826
}
827
break;
828
}
862
- case 'dangerouslySetInnerHTML':
863
- case 'children': {
864
- // Noop. This is handled by the clear text mechanism.
865
- break;
866
- }
867
- case 'suppressContentEditableWarning':
868
- case 'suppressHydrationWarning':
869
- case 'defaultValue': // Reserved
870
- case 'defaultChecked':
871
- case 'innerHTML': {
872
- // Noop
873
- break;
874
- }
875
- case 'autoFocus': {
876
- // Noop. It doesn't work on updates anyway.
877
- break;
878
- }
879
- case 'innerText': // Properties
880
- case 'textContent':
881
- if (enableCustomElementPropertySupport) {
882
- break;
883
- }
884
- // eslint-disable-next-line no-fallthrough
829
default: {
886
- if (registrationNameDependencies.hasOwnProperty(propKey)) {
887
- // This is a special case. If any listener updates we need to ensure
888
- // that the "current" fiber pointer gets updated so we need a commit
889
- // to update this element.
890
- if (!updatePayload) {
891
- updatePayload = [];
892
- }
893
- } else {
894
- // For all other deleted properties we add it to the queue. We use
895
- // the allowed property list in the commit phase instead.
896
- (updatePayload = updatePayload || []).push(propKey, null);
897
- }
830
+ // For all other deleted properties we add it to the queue. We use
831
+ // the allowed property list in the commit phase instead.
832
+ (updatePayload = updatePayload || []).push(propKey, null);
833
}
834
}
835
}
@@ -902,149 +837,50 @@ export function diffProperties(
837
const nextProp = nextProps[propKey];
838
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
839
if (
905
- !nextProps.hasOwnProperty(propKey) ||
906
- nextProp === lastProp ||
907
- (nextProp == null && lastProp == null)
840
+ nextProps.hasOwnProperty(propKey) &&
841
+ nextProp !== lastProp &&
842
+ (nextProp != null || lastProp != null)
843
) {
909
- continue;
910
- }
911
- switch (propKey) {
912
- case 'style': {
913
- if (nextProp != null && typeof nextProp !== 'object') {
914
- throw new Error(
915
- 'The `style` prop expects a mapping from style properties to values, ' +
916
- "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
917
- 'using JSX.',
918
- );
919
- }
920
- if (__DEV__) {
921
- if (nextProp) {
922
- // Freeze the next style object so that we can assume it won't be
923
- // mutated. We have already warned for this in the past.
924
- Object.freeze(nextProp);
925
- }
926
- }
927
- if (lastProp) {
928
- // Unset styles on `lastProp` but not on `nextProp`.
929
- for (styleName in lastProp) {
930
- if (
931
- lastProp.hasOwnProperty(styleName) &&
932
- (!nextProp || !nextProp.hasOwnProperty(styleName))
933
- ) {
934
- if (!styleUpdates) {
935
- styleUpdates = ({}: {[string]: string});
844
+ switch (propKey) {
845
+ case 'style': {
846
+ if (lastProp) {
847
+ // Unset styles on `lastProp` but not on `nextProp`.
848
+ for (styleName in lastProp) {
849
+ if (
850
+ lastProp.hasOwnProperty(styleName) &&
851
+ (!nextProp || !nextProp.hasOwnProperty(styleName))
852
+ ) {
853
+ if (!styleUpdates) {
854
+ styleUpdates = ({}: {[string]: string});
855
+ }
856
+ styleUpdates[styleName] = '';
857
}
937
- styleUpdates[styleName] = '';
858
}
939
- }
940
- // Update styles that changed since `lastProp`.
941
- for (styleName in nextProp) {
942
- if (
943
- nextProp.hasOwnProperty(styleName) &&
944
- lastProp[styleName] !== nextProp[styleName]
945
- ) {
946
- if (!styleUpdates) {
947
- styleUpdates = ({}: {[string]: $FlowFixMe});
859
+ // Update styles that changed since `lastProp`.
860
+ for (styleName in nextProp) {
861
+ if (
862
+ nextProp.hasOwnProperty(styleName) &&
863
+ lastProp[styleName] !== nextProp[styleName]
864
+ ) {
865
+ if (!styleUpdates) {
866
+ styleUpdates = ({}: {[string]: $FlowFixMe});
867
+ }
868
+ styleUpdates[styleName] = nextProp[styleName];
869
}
949
- styleUpdates[styleName] = nextProp[styleName];
870
}
951
- }
952
- } else {
953
- // Relies on `updateStylesByID` not mutating `styleUpdates`.
954
- if (!styleUpdates) {
955
- if (!updatePayload) {
956
- updatePayload = [];
957
- }
958
- updatePayload.push(propKey, styleUpdates);
959
- }
960
- styleUpdates = nextProp;
961
- }
962
- break;
963
- }
964
- case 'dangerouslySetInnerHTML': {
965
- if (nextProp != null) {
966
- if (typeof nextProp !== 'object' || !('__html' in nextProp)) {
967
- throw new Error(
968
- '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
969
- 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
970
- 'for more information.',
971
- );
972
- }
973
- const nextHtml = nextProp.__html;
974
- if (nextHtml != null) {
975
- if (nextProps.children != null) {
976
- throw new Error(
977
- 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
978
- );
979
- }
980
- const lastHtml = lastProp ? lastProp.__html : undefined;
981
- if (lastHtml !== nextHtml) {
982
- (updatePayload = updatePayload || []).push(propKey, nextHtml);
871
+ } else {
872
+ // Relies on `updateStylesByID` not mutating `styleUpdates`.
873
+ if (!styleUpdates) {
874
+ if (!updatePayload) {
875
+ updatePayload = [];
876
+ }
877
+ updatePayload.push(propKey, styleUpdates);
878
}
879
+ styleUpdates = nextProp;
880
}
985
- } else {
986
- // TODO: It might be too late to clear this if we have children
987
- // inserted already.
988
- }
989
- break;
990
- }
991
- case 'children': {
992
- if (typeof nextProp === 'string' || typeof nextProp === 'number') {
993
- (updatePayload = updatePayload || []).push(propKey, '' + nextProp);
994
- }
995
- break;
996
- }
997
- case 'onScroll': {
998
- if (nextProp != null) {
999
- // We eagerly listen to this even though we haven't committed yet.
1000
- if (__DEV__ && typeof nextProp !== 'function') {
1001
- warnForInvalidEventListener(propKey, nextProp);
1002
- }
1003
- listenToNonDelegatedEvent('scroll', domElement);
1004
- }
1005
- if (!updatePayload && lastProp !== nextProp) {
1006
- // This is a special case. If any listener updates we need to ensure
1007
- // that the "current" props pointer gets updated so we need a commit
1008
- // to update this element.
1009
- updatePayload = [];
1010
- }
1011
- break;
1012
- }
1013
- case 'suppressContentEditableWarning':
1014
- case 'suppressHydrationWarning':
1015
- case 'defaultValue': // Reserved
1016
- case 'defaultChecked':
1017
- case 'innerHTML': {
1018
- // Noop
1019
- break;
1020
- }
1021
- case 'autoFocus': {
1022
- // Noop on updates
1023
- break;
1024
- }
1025
- case 'innerText': // Properties
1026
- case 'textContent':
1027
- if (enableCustomElementPropertySupport) {
881
break;
882
}
1030
- // eslint-disable-next-line no-fallthrough
1031
- default: {
1032
- if (registrationNameDependencies.hasOwnProperty(propKey)) {
1033
- if (nextProp != null) {
1034
- // We eagerly listen to this even though we haven't committed yet.
1035
- if (__DEV__ && typeof nextProp !== 'function') {
1036
- warnForInvalidEventListener(propKey, nextProp);
1037
- }
1038
- }
1039
- if (!updatePayload && lastProp !== nextProp) {
1040
- // This is a special case. If any listener updates we need to ensure
1041
- // that the "current" props pointer gets updated so we need a commit
1042
- // to update this element.
1043
- updatePayload = [];
1044
- }
1045
- } else {
1046
- // For any other property we always add it to the queue and then we
1047
- // filter it out using the allowed property list during the commit.
883
+ default: {
884
(updatePayload = updatePayload || []).push(propKey, nextProp);
885
}
886
}
@@ -1064,47 +900,165 @@ export function updateProperties(
900
domElement: Element,
901
updatePayload: Array<any>,
902
tag: string,
1067
- lastRawProps: Object,
1068
- nextRawProps: Object,
903
+ lastProps: Object,
904
+ nextProps: Object,
905
): void {
1070
- // Update checked *before* name.
1071
- // In the middle of an update, it is possible to have multiple checked.
1072
- // When a checked radio tries to change name, browser makes another radio's checked false.
1073
- if (
1074
- tag === 'input' &&
1075
- nextRawProps.type === 'radio' &&
1076
- nextRawProps.name != null
1077
- ) {
1078
- ReactDOMInputUpdateChecked(domElement, nextRawProps);
1079
- }
1080
-
1081
- const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
1082
- const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
1083
- // Apply the diff.
1084
- updateDOMProperties(
1085
- domElement,
1086
- updatePayload,
1087
- wasCustomComponentTag,
1088
- isCustomComponentTag,
1089
- );
1090
-
1091
- // TODO: Ensure that an update gets scheduled if any of the special props
1092
- // changed.
906
switch (tag) {
1094
- case 'input':
907
+ case 'input': {
908
+ // Update checked *before* name.
909
+ // In the middle of an update, it is possible to have multiple checked.
910
+ // When a checked radio tries to change name, browser makes another radio's checked false.
911
+ if (nextProps.type === 'radio' && nextProps.name != null) {
912
+ ReactDOMInputUpdateChecked(domElement, nextProps);
913
+ }
914
+ for (let i = 0; i < updatePayload.length; i += 2) {
915
+ const propKey = updatePayload[i];
916
+ const propValue = updatePayload[i + 1];
917
+ switch (propKey) {
918
+ case 'checked': {
919
+ const node = ((domElement: any): InputWithWrapperState);
920
+ const checked =
921
+ propValue != null ? propValue : node._wrapperState.initialChecked;
922
+ node.checked =
923
+ !!checked &&
924
+ typeof checked !== 'function' &&
925
+ checked !== 'symbol';
926
+ break;
927
+ }
928
+ case 'value': {
929
+ // This is handled by updateWrapper below.
930
+ break;
931
+ }
932
+ case 'children':
933
+ case 'dangerouslySetInnerHTML': {
934
+ if (propValue != null) {
935
+ throw new Error(
936
+ `${tag} is a void element tag and must neither have \`children\` nor ` +
937
+ 'use `dangerouslySetInnerHTML`.',
938
+ );
939
+ }
940
+ break;
941
+ }
942
+ // defaultChecked and defaultValue are ignored by setProp
943
+ default: {
944
+ setProp(domElement, tag, propKey, propValue, false, nextProps);
945
+ }
946
+ }
947
+ }
948
// Update the wrapper around inputs *after* updating props. This has to
1096
- // happen after `updateDOMProperties`. Otherwise HTML5 input validations
949
+ // happen after updating the rest of props. Otherwise HTML5 input validations
950
// raise warnings and prevent the new value from being assigned.
1098
- ReactDOMInputUpdateWrapper(domElement, nextRawProps);
1099
- break;
1100
- case 'textarea':
1101
- ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
1102
- break;
1103
- case 'select':
951
+ ReactDOMInputUpdateWrapper(domElement, nextProps);
952
+ return;
953
+ }
954
+ case 'select': {
955
+ for (let i = 0; i < updatePayload.length; i += 2) {
956
+ const propKey = updatePayload[i];
957
+ const propValue = updatePayload[i + 1];
958
+ switch (propKey) {
959
+ case 'value': {
960
+ // This is handled by updateWrapper below.
961
+ break;
962
+ }
963
+ // defaultValue are ignored by setProp
964
+ default: {
965
+ setProp(domElement, tag, propKey, propValue, false, nextProps);
966
+ }
967
+ }
968
+ }
969
// <select> value update needs to occur after <option> children
970
// reconciliation
1106
- ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
1107
- break;
971
+ ReactDOMSelectPostUpdateWrapper(domElement, nextProps);
972
+ return;
973
+ }
974
+ case 'textarea': {
975
+ for (let i = 0; i < updatePayload.length; i += 2) {
976
+ const propKey = updatePayload[i];
977
+ const propValue = updatePayload[i + 1];
978
+ switch (propKey) {
979
+ case 'value': {
980
+ // This is handled by updateWrapper below.
981
+ break;
982
+ }
983
+ case 'children': {
984
+ // TODO: This doesn't actually do anything if it updates.
985
+ break;
986
+ }
987
+ case 'dangerouslySetInnerHTML': {
988
+ if (propValue != null) {
989
+ // TODO: Do we really need a special error message for this. It's also pretty blunt.
990
+ throw new Error(
991
+ '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
992
+ );
993
+ }
994
+ break;
995
+ }
996
+ // defaultValue is ignored by setProp
997
+ default: {
998
+ setProp(domElement, tag, propKey, propValue, false, nextProps);
999
+ }
1000
+ }
1001
+ }
1002
+ ReactDOMTextareaUpdateWrapper(domElement, nextProps);
1003
+ return;
1004
+ }
1005
+ case 'img':
1006
+ case 'link':
1007
+ case 'area':
1008
+ case 'base':
1009
+ case 'br':
1010
+ case 'col':
1011
+ case 'embed':
1012
+ case 'hr':
1013
+ case 'keygen':
1014
+ case 'meta':
1015
+ case 'param':
1016
+ case 'source':
1017
+ case 'track':
1018
+ case 'wbr':
1019
+ case 'menuitem': {
1020
+ // Void elements
1021
+ for (let i = 0; i < updatePayload.length; i += 2) {
1022
+ const propKey = updatePayload[i];
1023
+ const propValue = updatePayload[i + 1];
1024
+ switch (propKey) {
1025
+ case 'children':
1026
+ case 'dangerouslySetInnerHTML': {
1027
+ if (propValue != null) {
1028
+ // TODO: Can we make this a DEV warning to avoid this deny list?
1029
+ throw new Error(
1030
+ `${tag} is a void element tag and must neither have \`children\` nor ` +
1031
+ 'use `dangerouslySetInnerHTML`.',
1032
+ );
1033
+ }
1034
+ break;
1035
+ }
1036
+ // defaultChecked and defaultValue are ignored by setProp
1037
+ default: {
1038
+ // TODO: If the `is` prop is specified, this should go through the isCustomComponentTag flow.
1039
+ setProp(domElement, tag, propKey, propValue, false, nextProps);
1040
+ }
1041
+ }
1042
+ }
1043
+ return;
1044
+ }
1045
+ }
1046
+
1047
+ // TODO: Handle wasCustomComponentTag. Changing "is" isn't valid.
1048
+ // const wasCustomComponentTag = isCustomComponent(tag, lastProps);
1049
+ const isCustomComponentTag = isCustomComponent(tag, nextProps);
1050
+ // Apply the diff.
1051
+ for (let i = 0; i < updatePayload.length; i += 2) {
1052
+ const propKey = updatePayload[i];
1053
+ const propValue = updatePayload[i + 1];
1054
+ setProp(
1055
+ domElement,
1056
+ tag,
1057
+ propKey,
1058
+ propValue,
1059
+ isCustomComponentTag,
1060
+ nextProps,
1061
+ );
1062
}
1063
}
1064
@@ -1139,15 +1093,15 @@ function diffHydratedStyles(domElement: Element, value: mixed) {
1093
function diffHydratedCustomComponent(
1094
domElement: Element,
1095
tag: string,
1142
- rawProps: Object,
1096
+ props: Object,
1097
parentNamespaceDev: string,
1098
extraAttributeNames: Set<string>,
1099
) {
1146
- for (const propKey in rawProps) {
1147
- if (!rawProps.hasOwnProperty(propKey)) {
1100
+ for (const propKey in props) {
1101
+ if (!props.hasOwnProperty(propKey)) {
1102
continue;
1103
}
1150
- const nextProp = rawProps[propKey];
1104
+ const nextProp = props[propKey];
1105
if (nextProp == null) {
1106
continue;
1107
}
@@ -1157,7 +1111,7 @@ function diffHydratedCustomComponent(
1111
}
1112
continue;
1113
}
1160
- if (rawProps.suppressHydrationWarning === true) {
1114
+ if (props.suppressHydrationWarning === true) {
1115
// Don't bother comparing. We're ignoring all these warnings.
1116
continue;
1117
}
@@ -1230,15 +1184,15 @@ function diffHydratedCustomComponent(
1184
function diffHydratedGenericElement(
1185
domElement: Element,
1186
tag: string,
1233
- rawProps: Object,
1187
+ props: Object,
1188
parentNamespaceDev: string,
1189
extraAttributeNames: Set<string>,
1190
) {
1237
- for (const propKey in rawProps) {
1238
- if (!rawProps.hasOwnProperty(propKey)) {
1191
+ for (const propKey in props) {
1192
+ if (!props.hasOwnProperty(propKey)) {
1193
continue;
1194
}
1241
- const nextProp = rawProps[propKey];
1195
+ const nextProp = props[propKey];
1196
if (nextProp == null) {
1197
continue;
1198
}
@@ -1248,7 +1202,7 @@ function diffHydratedGenericElement(
1202
}
1203
continue;
1204
}
1251
- if (rawProps.suppressHydrationWarning === true) {
1205
+ if (props.suppressHydrationWarning === true) {
1206
// Don't bother comparing. We're ignoring all these warnings.
1207
continue;
1208
}
@@ -1334,13 +1288,13 @@ function diffHydratedGenericElement(
1288
export function diffHydratedProperties(
1289
domElement: Element,
1290
tag: string,
1337
- rawProps: Object,
1291
+ props: Object,
1292
isConcurrentMode: boolean,
1293
shouldWarnDev: boolean,
1294
parentNamespaceDev: string,
1295
): null | Array<mixed> {
1296
if (__DEV__) {
1343
- validatePropertiesInDevelopment(tag, rawProps);
1297
+ validatePropertiesInDevelopment(tag, props);
1298
}
1299
1300
// TODO: Make sure that we check isMounted before firing any of these events.
@@ -1383,35 +1337,44 @@ export function diffHydratedProperties(
1337
listenToNonDelegatedEvent('toggle', domElement);
1338
break;
1339
case 'input':
1386
- ReactDOMInputInitWrapperState(domElement, rawProps);
1340
+ ReactDOMInputInitWrapperState(domElement, props);
1341
// We listen to this event in case to ensure emulated bubble
1342
// listeners still fire for the invalid event.
1343
listenToNonDelegatedEvent('invalid', domElement);
1344
+ // TODO: Make sure we check if this is still unmounted or do any clean
1345
+ // up necessary since we never stop tracking anymore.
1346
+ track((domElement: any));
1347
+ // For input and textarea we current always set the value property at
1348
+ // post mount to force it to diverge from attributes. However, for
1349
+ // option and select we don't quite do the same thing and select
1350
+ // is not resilient to the DOM state changing so we don't do that here.
1351
+ // TODO: Consider not doing this for input and textarea.
1352
+ ReactDOMInputPostMountWrapper(domElement, props, true);
1353
break;
1354
case 'option':
1392
- ReactDOMOptionValidateProps(domElement, rawProps);
1355
+ ReactDOMOptionValidateProps(domElement, props);
1356
break;
1357
case 'select':
1395
- ReactDOMSelectInitWrapperState(domElement, rawProps);
1358
+ ReactDOMSelectInitWrapperState(domElement, props);
1359
// We listen to this event in case to ensure emulated bubble
1360
// listeners still fire for the invalid event.
1361
listenToNonDelegatedEvent('invalid', domElement);
1362
break;
1363
case 'textarea':
1401
- ReactDOMTextareaInitWrapperState(domElement, rawProps);
1364
+ ReactDOMTextareaInitWrapperState(domElement, props);
1365
// We listen to this event in case to ensure emulated bubble
1366
// listeners still fire for the invalid event.
1367
listenToNonDelegatedEvent('invalid', domElement);
1368
+ // TODO: Make sure we check if this is still unmounted or do any clean
1369
+ // up necessary since we never stop tracking anymore.
1370
+ track((domElement: any));
1371
+ ReactDOMTextareaPostMountWrapper(domElement, props);
1372
break;
1373
}
1374
1408
- if (rawProps.hasOwnProperty('onScroll')) {
1409
- listenToNonDelegatedEvent('scroll', domElement);
1410
- }
1411
-
1375
let updatePayload = null;
1376
1414
- const children = rawProps.children;
1377
+ const children = props.children;
1378
// For text content children we compare against textContent. This
1379
// might match additional HTML that is hidden when we read it using
1380
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
@@ -1423,7 +1386,7 @@ export function diffHydratedProperties(
1386
// TODO: Should we use domElement.firstChild.nodeValue to compare?
1387
if (typeof children === 'string' || typeof children === 'number') {
1388
if (domElement.textContent !== '' + children) {
1426
- if (rawProps.suppressHydrationWarning !== true) {
1389
+ if (props.suppressHydrationWarning !== true) {
1390
checkForUnmatchedText(
1391
domElement.textContent,
1392
children,
@@ -1437,6 +1400,15 @@ export function diffHydratedProperties(
1400
}
1401
}
1402
1403
+ if (props.onScroll != null) {
1404
+ listenToNonDelegatedEvent('scroll', domElement);
1405
+ }
1406
+
1407
+ if (props.onClick != null) {
1408
+ // TODO: This cast may not be sound for SVG, MathML or custom elements.
1409
+ trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
1410
+ }
1411
+
1412
if (__DEV__ && shouldWarnDev) {
1413
const extraAttributeNames: Set<string> = new Set();
1414
const attributes = domElement.attributes;
@@ -1457,11 +1429,11 @@ export function diffHydratedProperties(
1429
extraAttributeNames.add(attributes[i].name);
1430
}
1431
}
1460
- if (isCustomComponent(tag, rawProps)) {
1432
+ if (isCustomComponent(tag, props)) {
1433
diffHydratedCustomComponent(
1434
domElement,
1435
tag,
1464
- rawProps,
1436
+ props,
1437
parentNamespaceDev,
1438
extraAttributeNames,
1439
);
@@ -1469,48 +1441,19 @@ export function diffHydratedProperties(
1441
diffHydratedGenericElement(
1442
domElement,
1443
tag,
1472
- rawProps,
1444
+ props,
1445
parentNamespaceDev,
1446
extraAttributeNames,
1447
);
1448
}
1449
if (
1450
extraAttributeNames.size > 0 &&
1479
- rawProps.suppressHydrationWarning !== true
1451
+ props.suppressHydrationWarning !== true
1452
) {
1453
warnForExtraAttributes(extraAttributeNames);
1454
}
1455
}
1456
1485
- switch (tag) {
1486
- case 'input':
1487
- // TODO: Make sure we check if this is still unmounted or do any clean
1488
- // up necessary since we never stop tracking anymore.
1489
- track((domElement: any));
1490
- ReactDOMInputPostMountWrapper(domElement, rawProps, true);
1491
- break;
1492
- case 'textarea':
1493
- // TODO: Make sure we check if this is still unmounted or do any clean
1494
- // up necessary since we never stop tracking anymore.
1495
- track((domElement: any));
1496
- ReactDOMTextareaPostMountWrapper(domElement, rawProps);
1497
- break;
1498
- case 'select':
1499
- case 'option':
1500
- // For input and textarea we current always set the value property at
1501
- // post mount to force it to diverge from attributes. However, for
1502
- // option and select we don't quite do the same thing and select
1503
- // is not resilient to the DOM state changing so we don't do that here.
1504
- // TODO: Consider not doing this for input and textarea.
1505
- break;
1506
- default:
1507
- if (typeof rawProps.onClick === 'function') {
1508
- // TODO: This cast may not be sound for SVG, MathML or custom elements.
1509
- trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
1510
- }
1511
- break;
1512
- }
1513
-
1457
return updatePayload;
1458
}
1459
packages/react-dom-bindings/src/client/ReactDOMInput.js
+19
-26
@@ -15,19 +15,22 @@ import {getToStringValue, toString} from './ToStringValue';
15
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
16
import {updateValueIfChanged} from './inputValueTracking';
17
import getActiveElement from './getActiveElement';
18
-import assign from 'shared/assign';
18
import {disableInputAttributeSyncing} from 'shared/ReactFeatureFlags';
19
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
20
21
import type {ToStringValue} from './ToStringValue';
22
24
-type InputWithWrapperState = HTMLInputElement & {
23
+export type InputWithWrapperState = HTMLInputElement & {
24
_wrapperState: {
25
initialValue: ToStringValue,
26
initialChecked: ?boolean,
27
controlled?: boolean,
28
...
29
},
30
+ checked: boolean,
31
+ value: string,
32
+ defaultChecked: boolean,
33
+ defaultValue: string,
34
...
35
};
36
@@ -58,20 +61,6 @@ function isControlled(props: any) {
61
* See http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
62
*/
63
61
-export function getHostProps(element: Element, props: Object): Object {
62
- const node = ((element: any): InputWithWrapperState);
63
- const checked = props.checked;
64
-
65
- const hostProps = assign({}, props, {
66
- defaultChecked: undefined,
67
- defaultValue: undefined,
68
- value: undefined,
69
- checked: checked != null ? checked : node._wrapperState.initialChecked,
70
- });
71
-
72
- return hostProps;
73
-}
74
-
64
export function initWrapperState(element: Element, props: Object) {
65
if (__DEV__) {
66
checkControlledValueProps('input', props);
@@ -114,10 +103,13 @@ export function initWrapperState(element: Element, props: Object) {
103
104
const node = ((element: any): InputWithWrapperState);
105
const defaultValue = props.defaultValue == null ? '' : props.defaultValue;
117
-
106
+ const initialChecked =
107
+ props.checked != null ? props.checked : props.defaultChecked;
108
node._wrapperState = {
109
initialChecked:
120
- props.checked != null ? props.checked : props.defaultChecked,
110
+ typeof initialChecked !== 'function' &&
111
+ typeof initialChecked !== 'symbol' &&
112
+ !!initialChecked,
113
initialValue: getToStringValue(
114
props.value != null ? props.value : defaultValue,
115
),
@@ -312,15 +304,16 @@ export function postMountWrapper(
304
node.name = '';
305
}
306
315
- if (disableInputAttributeSyncing) {
316
- // When not syncing the checked attribute, the checked property
317
- // never gets assigned. It must be manually set. We don't want
318
- // to do this when hydrating so that existing user input isn't
319
- // modified
320
- if (!isHydrating) {
321
- updateChecked(element, props);
322
- }
307
+ // The checked property never gets assigned. It must be manually set.
308
+ // We don't want to do this when hydrating so that existing user input isn't
309
+ // modified
310
+ // TODO: I'm pretty sure this is a bug because initialValueTracking won't be
311
+ // correct for the hydration case then.
312
+ if (!isHydrating) {
313
+ node.checked = !!node._wrapperState.initialChecked;
314
+ }
315
316
+ if (disableInputAttributeSyncing) {
317
// Only assign the checked attribute if it is defined. This saves
318
// a DOM write when controlling the checked attribute isn't needed
319
// (text inputs, submit/reset)
packages/react-dom-bindings/src/client/ReactDOMSelect.js
-7
@@ -12,7 +12,6 @@ import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCur
12
13
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
14
import {getToStringValue, toString} from './ToStringValue';
15
-import assign from 'shared/assign';
15
import isArray from 'shared/isArray';
16
17
let didWarnValueDefaultValue;
@@ -130,12 +129,6 @@ function updateOptions(
129
* selected.
130
*/
131
133
-export function getHostProps(element: Element, props: Object): Object {
134
- return assign({}, props, {
135
- value: undefined,
136
- });
137
-}
138
-
132
export function initWrapperState(element: Element, props: Object) {
133
const node = ((element: any): SelectWithWrapperState);
134
if (__DEV__) {
packages/react-dom-bindings/src/client/ReactDOMTextarea.js
+4
-27
@@ -17,7 +17,7 @@ import {disableTextareaChildren} from 'shared/ReactFeatureFlags';
17
18
let didWarnValDefaultVal = false;
19
20
-type TextAreaWithWrapperState = HTMLTextAreaElement & {
20
+export type TextAreaWithWrapperState = HTMLTextAreaElement & {
21
_wrapperState: {initialValue: ToStringValue},
22
};
23
@@ -37,31 +37,6 @@ type TextAreaWithWrapperState = HTMLTextAreaElement & {
37
* `defaultValue` if specified, or the children content (deprecated).
38
*/
39
40
-export function getHostProps(element: Element, props: Object): Object {
41
- const node = ((element: any): TextAreaWithWrapperState);
42
-
43
- if (props.dangerouslySetInnerHTML != null) {
44
- throw new Error(
45
- '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
46
- );
47
- }
48
-
49
- // Always set children to the same thing. In IE9, the selection range will
50
- // get reset if `textContent` is mutated. We could add a check in setTextContent
51
- // to only set the value if/when the value differs from the node value (which would
52
- // completely solve this IE9 bug), but Sebastian+Sophie seemed to like this
53
- // solution. The value can be a boolean or object so that's why it's forced
54
- // to be a string.
55
- const hostProps = {
56
- ...props,
57
- value: undefined,
58
- defaultValue: undefined,
59
- children: toString(node._wrapperState.initialValue),
60
- };
61
-
62
- return hostProps;
63
-}
64
-
40
export function initWrapperState(element: Element, props: Object) {
41
const node = ((element: any): TextAreaWithWrapperState);
42
if (__DEV__) {
@@ -120,8 +95,10 @@ export function initWrapperState(element: Element, props: Object) {
95
initialValue = defaultValue;
96
}
97
98
+ const stringValue = getToStringValue(initialValue);
99
+ node.defaultValue = (stringValue: any); // This will be toString:ed.
100
node._wrapperState = {
124
- initialValue: getToStringValue(initialValue),
101
+ initialValue: stringValue,
102
};
103
}
104
packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js
+36
@@ -133,6 +133,42 @@ describe('ChangeEventPlugin', () => {
133
}
134
});
135
136
+ it('should not invoke a change event for textarea same value', () => {
137
+ let called = 0;
138
+
139
+ function cb(e) {
140
+ called++;
141
+ expect(e.type).toBe('change');
142
+ }
143
+
144
+ const node = ReactDOM.render(
145
+ <textarea onChange={cb} defaultValue="initial" />,
146
+ container,
147
+ );
148
+ node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
149
+ node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
150
+ // There should be no React change events because the value stayed the same.
151
+ expect(called).toBe(0);
152
+ });
153
+
154
+ it('should not invoke a change event for textarea same value (capture)', () => {
155
+ let called = 0;
156
+
157
+ function cb(e) {
158
+ called++;
159
+ expect(e.type).toBe('change');
160
+ }
161
+
162
+ const node = ReactDOM.render(
163
+ <textarea onChangeCapture={cb} defaultValue="initial" />,
164
+ container,
165
+ );
166
+ node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
167
+ node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
168
+ // There should be no React change events because the value stayed the same.
169
+ expect(called).toBe(0);
170
+ });
171
+
172
it('should consider initial checkbox checked=true to be current', () => {
173
let called = 0;
174