From b9cbc6c4d62633ac0567dae81b9a260877ff20cb Mon Sep 17 00:00:00 2001 From: sebmarkbage Date: Mon, 5 May 2025 21:17:22 -0700 Subject: [PATCH] [Fiber] Replay onChange Events if input/textarea/select has changed before hydration (#33129) This fixes a long standing issue that controlled inputs gets out of sync with the browser state if it's changed before we hydrate. This resolves the issue by replaying the change events (click, input and change) if the value has changed by the time we commit the hydration. That way you can reflect the new value in state to bring it in sync. It does this whether controlled or uncontrolled. The idea is that this should be ok to replay because it's similar to the continuous events in that it doesn't replay a sequence but only reflects the current state of the tree. Since this is a breaking change I added it behind `enableHydrationChangeEvent` flag. There is still an additional issue remaining that I intend to address in a follow up. If a `useLayoutEffect` triggers an sync rerender on hydration (always a bad idea) then that can rerender before we have had a chance to replay the change events. If that renders through a input then that input will always override the browser value with the controlled value. Which will reset it before we've had a change to update to the new value. DiffTrain build for [587cb8f8967866139bbfdbae3f519cb37e68a054](https://github.com/facebook/react/commit/587cb8f8967866139bbfdbae3f519cb37e68a054) --- compiled-rn/VERSION_NATIVE_FB | 2 +- .../react/react-dom/cjs/ReactDOM-dev.js | 4 +- .../react/react-dom/cjs/ReactDOM-prod.js | 4 +- .../react/react-dom/cjs/ReactDOM-profiling.js | 4 +- .../react/react-dom/cjs/ReactDOMClient-dev.js | 706 ++++++++------ .../react-dom/cjs/ReactDOMClient-prod.js | 878 +++++++++-------- .../react-dom/cjs/ReactDOMClient-profiling.js | 905 ++++++++++-------- .../react-dom/cjs/ReactDOMProfiling-dev.js | 706 ++++++++------ .../react-dom/cjs/ReactDOMProfiling-prod.js | 878 +++++++++-------- .../cjs/ReactDOMProfiling-profiling.js | 905 ++++++++++-------- .../cjs/ReactTestRenderer-dev.js | 37 +- .../cjs/ReactTestRenderer-prod.js | 51 +- .../cjs/ReactTestRenderer-profiling.js | 32 +- .../vendor/react/react/cjs/React-dev.js | 4 +- .../vendor/react/react/cjs/React-prod.js | 4 +- .../vendor/react/react/cjs/React-profiling.js | 4 +- .../Libraries/Renderer/REVISION | 2 +- .../implementations/ReactFabric-dev.fb.js | 31 +- .../implementations/ReactFabric-prod.fb.js | 54 +- .../ReactFabric-profiling.fb.js | 39 +- .../ReactNativeRenderer-dev.fb.js | 35 +- .../ReactNativeRenderer-prod.fb.js | 55 +- .../ReactNativeRenderer-profiling.fb.js | 40 +- .../eslint-plugin-react-hooks/package.json | 2 +- 24 files changed, 3093 insertions(+), 2289 deletions(-) diff --git a/compiled-rn/VERSION_NATIVE_FB b/compiled-rn/VERSION_NATIVE_FB index 459a6ec159..27fbf7188c 100644 --- a/compiled-rn/VERSION_NATIVE_FB +++ b/compiled-rn/VERSION_NATIVE_FB @@ -1 +1 @@ -19.2.0-native-fb-79586c7e-20250505 \ No newline at end of file +19.2.0-native-fb-587cb8f8-20250506 \ No newline at end of file diff --git a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-dev.js b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-dev.js index 20bfd4477b..cdcca14b2b 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-dev.js +++ b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-dev.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<> */ "use strict"; @@ -404,5 +404,5 @@ __DEV__ && exports.useFormStatus = function () { return resolveDispatcher().useHostTransitionStatus(); }; - exports.version = "19.2.0-native-fb-79586c7e-20250505"; + exports.version = "19.2.0-native-fb-587cb8f8-20250506"; })(); diff --git a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-prod.js b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-prod.js index b1dacbb628..22a5cd0345 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-prod.js +++ b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-prod.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<> */ "use strict"; @@ -203,4 +203,4 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-native-fb-79586c7e-20250505"; +exports.version = "19.2.0-native-fb-587cb8f8-20250506"; diff --git a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-profiling.js b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-profiling.js index b1dacbb628..22a5cd0345 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-profiling.js +++ b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOM-profiling.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<> + * @generated SignedSource<> */ "use strict"; @@ -203,4 +203,4 @@ exports.useFormState = function (action, initialState, permalink) { exports.useFormStatus = function () { return ReactSharedInternals.H.useHostTransitionStatus(); }; -exports.version = "19.2.0-native-fb-79586c7e-20250505"; +exports.version = "19.2.0-native-fb-587cb8f8-20250506"; diff --git a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-dev.js b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-dev.js index e335750a21..0795d72788 100644 --- a/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-dev.js +++ b/compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-dom/cjs/ReactDOMClient-dev.js @@ -7,7 +7,7 @@ * @noflow * @nolint * @preventMunge - * @generated SignedSource<<8aa824879ada19c90d13dba44435eba4>> + * @generated SignedSource<> */ /* @@ -1589,14 +1589,11 @@ __DEV__ && ("checkbox" === type || "radio" === type) ); } - function trackValueOnNode(node) { - var valueField = isCheckable(node) ? "checked" : "value", - descriptor = Object.getOwnPropertyDescriptor( - node.constructor.prototype, - valueField - ); - checkFormFieldValueStringCoercion(node[valueField]); - var currentValue = "" + node[valueField]; + function trackValueOnNode(node, valueField, currentValue) { + var descriptor = Object.getOwnPropertyDescriptor( + node.constructor.prototype, + valueField + ); if ( !node.hasOwnProperty(valueField) && "undefined" !== typeof descriptor && @@ -1635,7 +1632,24 @@ __DEV__ && } } function track(node) { - node._valueTracker || (node._valueTracker = trackValueOnNode(node)); + if (!node._valueTracker) { + var valueField = isCheckable(node) ? "checked" : "value"; + node._valueTracker = trackValueOnNode( + node, + valueField, + "" + node[valueField] + ); + } + } + function trackHydrated(node, initialValue, initialChecked) { + if (node._valueTracker) return !1; + if (isCheckable(node)) { + var valueField = "checked"; + initialValue = "" + initialChecked; + } else valueField = "value"; + initialChecked = "" + node[valueField]; + node._valueTracker = trackValueOnNode(node, valueField, initialValue); + return initialChecked !== initialValue; } function updateValueIfChanged(node) { if (!node) return !1; @@ -1737,48 +1751,6 @@ __DEV__ && (element.name = "" + getToStringValue(name))) : element.removeAttribute("name"); } - function initInput( - element, - value, - defaultValue, - checked, - defaultChecked, - type, - name, - isHydrating - ) { - null != type && - "function" !== typeof type && - "symbol" !== typeof type && - "boolean" !== typeof type && - (checkAttributeStringCoercion(type, "type"), (element.type = type)); - if (null != value || null != defaultValue) { - if ( - !( - ("submit" !== type && "reset" !== type) || - (void 0 !== value && null !== value) - ) - ) - return; - defaultValue = - null != defaultValue ? "" + getToStringValue(defaultValue) : ""; - value = null != value ? "" + getToStringValue(value) : defaultValue; - isHydrating || value === element.value || (element.value = value); - element.defaultValue = value; - } - checked = null != checked ? checked : defaultChecked; - checked = - "function" !== typeof checked && - "symbol" !== typeof checked && - !!checked; - element.checked = isHydrating ? element.checked : !!checked; - element.defaultChecked = !!checked; - null != name && - "function" !== typeof name && - "symbol" !== typeof name && - "boolean" !== typeof name && - (checkAttributeStringCoercion(name, "name"), (element.name = name)); - } function setDefaultValue(node, type, value) { ("number" === type && getActiveElement(node.ownerDocument) === node) || node.defaultValue === "" + value || @@ -1897,37 +1869,12 @@ __DEV__ && element.defaultValue = null != defaultValue ? "" + getToStringValue(defaultValue) : ""; } - function initTextarea(element, value, defaultValue, children) { - if (null == value) { - if (null != children) { - if (null != defaultValue) - throw Error( - "If you supply `defaultValue` on a