diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
index 2959274d1c865..28776b1eb62fb 100644
--- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
+++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
@@ -155,6 +155,355 @@ describe('DOMPropertyOperations', () => {
// Regression test for https://github.com/facebook/react/issues/6119
expect(container.firstChild.hasAttribute('value')).toBe(false);
});
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element custom events lowercase', () => {
+ const oncustomevent = jest.fn();
+ function Test() {
+ return ;
+ }
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ container
+ .querySelector('my-custom-element')
+ .dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element custom events uppercase', () => {
+ const oncustomevent = jest.fn();
+ function Test() {
+ return ;
+ }
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ container
+ .querySelector('my-custom-element')
+ .dispatchEvent(new Event('Customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element custom event with dash in name', () => {
+ const oncustomevent = jest.fn();
+ function Test() {
+ return ;
+ }
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ container
+ .querySelector('my-custom-element')
+ .dispatchEvent(new Event('custom-event'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element remove event handler', () => {
+ const oncustomevent = jest.fn();
+ function Test(props) {
+ return ;
+ }
+
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+
+ ReactDOM.render(, container);
+ // Make sure that the second render didn't create a new element. We want
+ // to make sure removeEventListener actually gets called on the same element.
+ expect(customElement).toBe(customElement);
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(2);
+
+ const oncustomevent2 = jest.fn();
+ ReactDOM.render(, container);
+ customElement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(2);
+ expect(oncustomevent2).toHaveBeenCalledTimes(1);
+ });
+
+ it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(
+ ,
+ container,
+ );
+ const customElement = container.querySelector('my-custom-element');
+ expect(customElement.getAttribute('onstring')).toBe('hello');
+ expect(customElement.getAttribute('onobj')).toBe('[object Object]');
+ expect(customElement.getAttribute('onarray')).toBe('one,two');
+ expect(customElement.getAttribute('ontrue')).toBe('true');
+ expect(customElement.getAttribute('onfalse')).toBe('false');
+
+ // Dispatch the corresponding event names to make sure that nothing crashes.
+ customElement.dispatchEvent(new Event('string'));
+ customElement.dispatchEvent(new Event('obj'));
+ customElement.dispatchEvent(new Event('array'));
+ customElement.dispatchEvent(new Event('true'));
+ customElement.dispatchEvent(new Event('false'));
+ });
+
+ it('custom elements should still have onClick treated like regular elements', () => {
+ let syntheticClickEvent = null;
+ const syntheticEventHandler = jest.fn(
+ event => (syntheticClickEvent = event),
+ );
+ let nativeClickEvent = null;
+ const nativeEventHandler = jest.fn(event => (nativeClickEvent = event));
+ function Test() {
+ return ;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ ReactDOM.render(, container);
+
+ const customElement = container.querySelector('my-custom-element');
+ customElement.onclick = nativeEventHandler;
+ container.querySelector('my-custom-element').click();
+
+ expect(nativeEventHandler).toHaveBeenCalledTimes(1);
+ expect(syntheticEventHandler).toHaveBeenCalledTimes(1);
+ expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom elements should allow custom events with capture event listeners', () => {
+ const oncustomeventCapture = jest.fn();
+ const oncustomevent = jest.fn();
+ function Test() {
+ return (
+
+
+
+ );
+ }
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ container
+ .querySelector('my-custom-element > div')
+ .dispatchEvent(new Event('customevent', {bubbles: false}));
+ expect(oncustomeventCapture).toHaveBeenCalledTimes(1);
+ expect(oncustomevent).toHaveBeenCalledTimes(0);
+ });
+
+ it('innerHTML should not work on custom elements', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ expect(customElement.getAttribute('innerHTML')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+
+ // Render again to verify the update codepath doesn't accidentally let
+ // something through.
+ ReactDOM.render(, container);
+ expect(customElement.getAttribute('innerHTML')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('innerText should not work on custom elements', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ expect(customElement.getAttribute('innerText')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+
+ // Render again to verify the update codepath doesn't accidentally let
+ // something through.
+ ReactDOM.render(, container);
+ expect(customElement.getAttribute('innerText')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('textContent should not work on custom elements', () => {
+ const container = document.createElement('div');
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ expect(customElement.getAttribute('textContent')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+
+ // Render again to verify the update codepath doesn't accidentally let
+ // something through.
+ ReactDOM.render(, container);
+ expect(customElement.getAttribute('textContent')).toBe(null);
+ expect(customElement.hasChildNodes()).toBe(false);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('values should not be converted to booleans when assigning into custom elements', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ customElement.foo = null;
+
+ // true => string
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(true);
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe('bar');
+
+ // false => string
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(false);
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe('bar');
+
+ // true => null
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(true);
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(null);
+
+ // false => null
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(false);
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe(null);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element custom event handlers assign multiple types', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const oncustomevent = jest.fn();
+
+ // First render with string
+ ReactDOM.render(, container);
+ const customelement = container.querySelector('my-custom-element');
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(0);
+ expect(customelement.oncustomevent).toBe(undefined);
+ expect(customelement.getAttribute('oncustomevent')).toBe('foo');
+
+ // string => event listener
+ ReactDOM.render(
+ ,
+ container,
+ );
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ expect(customelement.oncustomevent).toBe(undefined);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+
+ // event listener => string
+ ReactDOM.render(, container);
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ expect(customelement.oncustomevent).toBe(undefined);
+ expect(customelement.getAttribute('oncustomevent')).toBe('foo');
+
+ // string => nothing
+ ReactDOM.render(, container);
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ expect(customelement.oncustomevent).toBe(undefined);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+
+ // nothing => event listener
+ ReactDOM.render(
+ ,
+ container,
+ );
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(2);
+ expect(customelement.oncustomevent).toBe(undefined);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('custom element custom event handlers assign multiple types with setter', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const oncustomevent = jest.fn();
+
+ // First render with nothing
+ ReactDOM.render(, container);
+ const customelement = container.querySelector('my-custom-element');
+ // Install a setter to activate the `in` heuristic
+ Object.defineProperty(customelement, 'oncustomevent', {
+ set: function(x) {
+ this._oncustomevent = x;
+ },
+ get: function() {
+ return this._oncustomevent;
+ },
+ });
+ expect(customelement.oncustomevent).toBe(undefined);
+
+ // nothing => event listener
+ ReactDOM.render(
+ ,
+ container,
+ );
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ expect(customelement.oncustomevent).toBe(null);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+
+ // event listener => string
+ ReactDOM.render(, container);
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(1);
+ expect(customelement.oncustomevent).toBe('foo');
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+
+ // string => event listener
+ ReactDOM.render(
+ ,
+ container,
+ );
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(2);
+ expect(customelement.oncustomevent).toBe(null);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+
+ // event listener => nothing
+ ReactDOM.render(, container);
+ customelement.dispatchEvent(new Event('customevent'));
+ expect(oncustomevent).toHaveBeenCalledTimes(2);
+ expect(customelement.oncustomevent).toBe(null);
+ expect(customelement.getAttribute('oncustomevent')).toBe(null);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('assigning to a custom element property should not remove attributes', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ ReactDOM.render(, container);
+ const customElement = container.querySelector('my-custom-element');
+ expect(customElement.getAttribute('foo')).toBe('one');
+
+ // Install a setter to activate the `in` heuristic
+ Object.defineProperty(customElement, 'foo', {
+ set: function(x) {
+ this._foo = x;
+ },
+ get: function() {
+ return this._foo;
+ },
+ });
+ ReactDOM.render(, container);
+ expect(customElement.foo).toBe('two');
+ expect(customElement.getAttribute('foo')).toBe('one');
+ });
});
describe('deleteValueForProperty', () => {
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
index c1c23e2185842..b32146d55e44e 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
@@ -10,6 +10,7 @@
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
+const ReactFeatureFlags = require('shared/ReactFeatureFlags');
let React;
let ReactDOM;
@@ -36,6 +37,7 @@ const {
resetModules,
itRenders,
clientCleanRender,
+ clientRenderOnServerString,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
@@ -657,17 +659,28 @@ describe('ReactDOMServerIntegration', () => {
});
itRenders('className for custom elements', async render => {
- const e = await render(
, 0);
- expect(e.getAttribute('className')).toBe('test');
+ if (ReactFeatureFlags.enableCustomElementPropertySupport) {
+ const e = await render(
+ ,
+ render === clientRenderOnServerString ? 1 : 0,
+ );
+ expect(e.getAttribute('className')).toBe(null);
+ expect(e.getAttribute('class')).toBe('test');
+ } else {
+ const e = await render(, 0);
+ expect(e.getAttribute('className')).toBe('test');
+ }
});
itRenders('htmlFor attribute on custom elements', async render => {
const e = await render();
expect(e.getAttribute('htmlFor')).toBe('test');
+ expect(e.getAttribute('for')).toBe(null);
});
itRenders('for attribute on custom elements', async render => {
const e = await render();
+ expect(e.getAttribute('htmlFor')).toBe(null);
expect(e.getAttribute('for')).toBe('test');
});
diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
index b39d7c8ff5173..6489151b05504 100644
--- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
@@ -1097,4 +1097,43 @@ describe('ReactDOMServer', () => {
'However, it is set to a string.',
);
});
+
+ describe('custom element server rendering', () => {
+ it('String properties should be server rendered for custom elements', () => {
+ const output = ReactDOMServer.renderToString(
+ ,
+ );
+ expect(output).toBe(``);
+ });
+
+ it('Number properties should be server rendered for custom elements', () => {
+ const output = ReactDOMServer.renderToString(
+ ,
+ );
+ expect(output).toBe(``);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('Object properties should not be server rendered for custom elements', () => {
+ const output = ReactDOMServer.renderToString(
+ ,
+ );
+ expect(output).toBe(``);
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('Array properties should not be server rendered for custom elements', () => {
+ const output = ReactDOMServer.renderToString(
+ ,
+ );
+ expect(output).toBe(``);
+ });
+
+ it('Function properties should not be server rendered for custom elements', () => {
+ const output = ReactDOMServer.renderToString(
+ console.log('bar')} />,
+ );
+ expect(output).toBe(``);
+ });
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
index 0069946767252..3d63954a0857f 100644
--- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
@@ -530,4 +530,70 @@ describe('ReactDOMServerHydration', () => {
'Warning: Did not expect server HTML to contain a in
',
);
});
+
+ it('should warn when hydrating read-only properties', () => {
+ const readOnlyProperties = [
+ 'offsetParent',
+ 'offsetTop',
+ 'offsetLeft',
+ 'offsetWidth',
+ 'offsetHeight',
+ 'isContentEditable',
+ 'outerText',
+ 'outerHTML',
+ ];
+ readOnlyProperties.forEach(readOnlyProperty => {
+ const props = {};
+ props[readOnlyProperty] = 'hello';
+ const jsx = React.createElement('my-custom-element', props);
+ const element = document.createElement('div');
+ element.innerHTML = ReactDOMServer.renderToString(jsx);
+ if (gate(flags => flags.enableCustomElementPropertySupport)) {
+ expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev(
+ `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
+ );
+ } else {
+ ReactDOM.hydrate(jsx, element);
+ }
+ });
+ });
+
+ // @gate enableCustomElementPropertySupport
+ it('should not re-assign properties on hydration', () => {
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const jsx = React.createElement('my-custom-element', {
+ str: 'string',
+ obj: {foo: 'bar'},
+ });
+
+ container.innerHTML = ReactDOMServer.renderToString(jsx);
+ const customElement = container.querySelector('my-custom-element');
+
+ // Install setters to activate `in` check
+ Object.defineProperty(customElement, 'str', {
+ set: function(x) {
+ this._str = x;
+ },
+ get: function() {
+ return this._str;
+ },
+ });
+ Object.defineProperty(customElement, 'obj', {
+ set: function(x) {
+ this._obj = x;
+ },
+ get: function() {
+ return this._obj;
+ },
+ });
+
+ ReactDOM.hydrate(jsx, container);
+
+ expect(customElement.getAttribute('str')).toBe('string');
+ expect(customElement.getAttribute('obj')).toBe(null);
+ expect(customElement.str).toBe(undefined);
+ expect(customElement.obj).toBe(undefined);
+ });
});
diff --git a/packages/react-dom/src/client/DOMPropertyOperations.js b/packages/react-dom/src/client/DOMPropertyOperations.js
index ad94f7ccb7026..71b53d703c6d4 100644
--- a/packages/react-dom/src/client/DOMPropertyOperations.js
+++ b/packages/react-dom/src/client/DOMPropertyOperations.js
@@ -19,8 +19,10 @@ import sanitizeURL from '../shared/sanitizeURL';
import {
disableJavaScriptURLs,
enableTrustedTypesIntegration,
+ enableCustomElementPropertySupport,
} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
+import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import type {PropertyInfo} from '../shared/DOMProperty';
@@ -149,9 +151,52 @@ export function setValueForProperty(
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
return;
}
+
+ if (
+ enableCustomElementPropertySupport &&
+ isCustomComponentTag &&
+ name[0] === 'o' &&
+ name[1] === 'n'
+ ) {
+ let eventName = name.replace(/Capture$/, '');
+ const useCapture = name !== eventName;
+ eventName = eventName.slice(2);
+
+ const prevProps = getFiberCurrentPropsFromNode(node);
+ const prevValue = prevProps != null ? prevProps[name] : null;
+ if (typeof prevValue === 'function') {
+ node.removeEventListener(eventName, prevValue, useCapture);
+ }
+ if (typeof value === 'function') {
+ if (typeof prevValue !== 'function' && prevValue !== null) {
+ // If we previously assigned a non-function type into this node, then
+ // remove it when switching to event listener mode.
+ if (name in (node: any)) {
+ (node: any)[name] = null;
+ } else if (node.hasAttribute(name)) {
+ node.removeAttribute(name);
+ }
+ }
+
+ // $FlowFixMe value can't be casted to EventListener.
+ node.addEventListener(eventName, (value: EventListener), useCapture);
+ return;
+ }
+ }
+
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
value = null;
}
+
+ if (
+ enableCustomElementPropertySupport &&
+ isCustomComponentTag &&
+ name in (node: any)
+ ) {
+ (node: any)[name] = value;
+ return;
+ }
+
// If the prop isn't in the special list, treat it as a simple attribute.
if (isCustomComponentTag || propertyInfo === null) {
if (isAttributeNameSafe(name)) {
diff --git a/packages/react-dom/src/client/ReactDOMComponent.js b/packages/react-dom/src/client/ReactDOMComponent.js
index aa9b0281e2f8d..ffd90c128d462 100644
--- a/packages/react-dom/src/client/ReactDOMComponent.js
+++ b/packages/react-dom/src/client/ReactDOMComponent.js
@@ -69,7 +69,10 @@ import {validateProperties as validateARIAProperties} from '../shared/ReactDOMIn
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
-import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
+import {
+ enableTrustedTypesIntegration,
+ enableCustomElementPropertySupport,
+} from 'shared/ReactFeatureFlags';
import {
mediaEventTypes,
listenToNonDelegatedEvent,
@@ -998,7 +1001,10 @@ export function diffHydratedProperties(
) {
// Validate that the properties correspond to their expected values.
let serverValue;
- const propertyInfo = getPropertyInfo(propKey);
+ const propertyInfo =
+ isCustomComponentTag && enableCustomElementPropertySupport
+ ? null
+ : getPropertyInfo(propKey);
if (suppressHydrationWarning) {
// Don't bother comparing. We're ignoring all these warnings.
} else if (
@@ -1031,7 +1037,27 @@ export function diffHydratedProperties(
warnForPropDifference(propKey, serverValue, expectedStyle);
}
}
- } else if (isCustomComponentTag) {
+ } else if (
+ enableCustomElementPropertySupport &&
+ isCustomComponentTag &&
+ (propKey === 'offsetParent' ||
+ propKey === 'offsetTop' ||
+ propKey === 'offsetLeft' ||
+ propKey === 'offsetWidth' ||
+ propKey === 'offsetHeight' ||
+ propKey === 'isContentEditable' ||
+ propKey === 'outerText' ||
+ propKey === 'outerHTML')
+ ) {
+ // $FlowFixMe - Should be inferred as not undefined.
+ extraAttributeNames.delete(propKey.toLowerCase());
+ if (__DEV__) {
+ console.error(
+ 'Assignment to read-only property will result in a no-op: `%s`',
+ propKey,
+ );
+ }
+ } else if (isCustomComponentTag && !enableCustomElementPropertySupport) {
// $FlowFixMe - Should be inferred as not undefined.
extraAttributeNames.delete(propKey.toLowerCase());
serverValue = getValueForAttribute(domElement, propKey, nextProp);
@@ -1084,7 +1110,15 @@ export function diffHydratedProperties(
serverValue = getValueForAttribute(domElement, propKey, nextProp);
}
- if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
+ const dontWarnCustomElement =
+ enableCustomElementPropertySupport &&
+ isCustomComponentTag &&
+ (typeof nextProp === 'function' || typeof nextProp === 'object');
+ if (
+ !dontWarnCustomElement &&
+ nextProp !== serverValue &&
+ !isMismatchDueToBadCasing
+ ) {
warnForPropDifference(propKey, serverValue, nextProp);
}
}
diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js
index 64fd789b7a1e8..341d1fa7a3764 100644
--- a/packages/react-dom/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom/src/client/ReactDOMHostConfig.js
@@ -442,11 +442,11 @@ export function commitUpdate(
newProps: Props,
internalInstanceHandle: Object,
): void {
+ // Apply the diff to the DOM node.
+ updateProperties(domElement, updatePayload, type, oldProps, newProps);
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
- // Apply the diff to the DOM node.
- updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
export function resetTextContent(domElement: Instance): void {
diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index c4f8d89a8c907..5ea8d772732bd 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -17,7 +17,10 @@ import {
import {Children} from 'react';
-import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags';
+import {
+ enableFilterEmptyStringAttributesDOM,
+ enableCustomElementPropertySupport,
+} from 'shared/ReactFeatureFlags';
import type {
Destination,
@@ -1115,12 +1118,26 @@ function pushStartCustomElement(
let children = null;
let innerHTML = null;
- for (const propKey in props) {
+ for (let propKey in props) {
if (hasOwnProperty.call(props, propKey)) {
const propValue = props[propKey];
if (propValue == null) {
continue;
}
+ if (
+ enableCustomElementPropertySupport &&
+ (typeof propValue === 'function' || typeof propValue === 'object')
+ ) {
+ // It is normal to render functions and objects on custom elements when
+ // client rendering, but when server rendering the output isn't useful,
+ // so skip it.
+ continue;
+ }
+ if (enableCustomElementPropertySupport && propKey === 'className') {
+ // className gets rendered as class on the client, so it should be
+ // rendered as class on the server.
+ propKey = 'class';
+ }
switch (propKey) {
case 'children':
children = propValue;
diff --git a/packages/react-dom/src/shared/DOMProperty.js b/packages/react-dom/src/shared/DOMProperty.js
index 83b48555ebd83..278a9fd12b006 100644
--- a/packages/react-dom/src/shared/DOMProperty.js
+++ b/packages/react-dom/src/shared/DOMProperty.js
@@ -7,7 +7,10 @@
* @flow
*/
-import {enableFilterEmptyStringAttributesDOM} from 'shared/ReactFeatureFlags';
+import {
+ enableFilterEmptyStringAttributesDOM,
+ enableCustomElementPropertySupport,
+} from 'shared/ReactFeatureFlags';
import hasOwnProperty from 'shared/hasOwnProperty';
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
@@ -247,6 +250,9 @@ const reservedProps = [
'suppressHydrationWarning',
'style',
];
+if (enableCustomElementPropertySupport) {
+ reservedProps.push('innerText', 'textContent');
+}
reservedProps.forEach(name => {
properties[name] = new PropertyInfoRecord(
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 806fcdeb29d13..c5f798d559ddf 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -140,6 +140,12 @@ export const deletedTreeCleanUpLevel = 3;
// Note that this should be an uncommon use case and can be avoided by using the transition API.
export const enableSuspenseLayoutEffectSemantics = true;
+// Changes the behavior for rendering custom elements in both server rendering
+// and client rendering, mostly to allow JSX attributes to apply to the custom
+// element's object properties instead of only HTML attributes.
+// https://github.com/facebook/react/issues/11347
+export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
+
// --------------------------
// Future APIs to be deprecated
// --------------------------
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index b1d5d68454788..0f30707a49f3a 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -72,6 +72,7 @@ export const disableSchedulerTimeoutInWorkLoop = false;
export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = true;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableUseMutableSource = true;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index beb55a13fc0b7..a660772b7e354 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = false;
export const enablePersistentOffscreenHostContainer = false;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableUseMutableSource = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index ba200210122f0..e6c4e5ec21fdd 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = false;
export const enablePersistentOffscreenHostContainer = false;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableUseMutableSource = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index bf87304fae18f..51bbc5a1627df 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = true;
export const enablePersistentOffscreenHostContainer = false;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.js b/packages/shared/forks/ReactFeatureFlags.testing.js
index 3ed2d5956834e..182bebe61441e 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.js
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = false;
export const enablePersistentOffscreenHostContainer = false;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
export const enableUseMutableSource = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.testing.www.js b/packages/shared/forks/ReactFeatureFlags.testing.www.js
index e96c9fc2023ac..f62dc8af4960d 100644
--- a/packages/shared/forks/ReactFeatureFlags.testing.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.testing.www.js
@@ -64,6 +64,7 @@ export const enableLazyContextPropagation = false;
export const enableSyncDefaultUpdates = true;
export const allowConcurrentByDefault = true;
export const enablePersistentOffscreenHostContainer = false;
+export const enableCustomElementPropertySupport = false;
export const consoleManagedByDevToolsDuringStrictMode = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index a46b66fb20db0..cffa189840f8c 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -104,6 +104,8 @@ export const consoleManagedByDevToolsDuringStrictMode = true;
// Some www surfaces are still using this. Remove once they have been migrated.
export const enableUseMutableSource = true;
+export const enableCustomElementPropertySupport = __EXPERIMENTAL__;
+
// Flow magic to verify the exports of this file match the original version.
// eslint-disable-next-line no-unused-vars
type Check<_X, Y: _X, X: Y = _X> = null;