@samitouri / QOS-React-2 / commits / 85de6fde51

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