From f97c16522abce93961f632bcacab8e3ebb9ed32d Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:00:26 -0700 Subject: [PATCH] Call inherited host node methods on the host node in strict refs The strict ref wrapper uses Object.create(node), so methods that live on the host node prototype chain are reached through the wrapper but invoked with the wrapper as the receiver. RN host node methods read internals off this, which the wrapper does not carry, so ref.focus() and ref.blur() stop working on native. Re-expose inherited methods as own properties that call through to the host node. Own methods of the node are untouched because RN assigns those as closures over the node. --- packages/react-strict-dom/CHANGELOG.md | 4 ++ .../src/native/modules/useStrictDOMElement.js | 45 +++++++++++++++++++ .../tests/html/html-refs-test.native.js | 35 +++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/packages/react-strict-dom/CHANGELOG.md b/packages/react-strict-dom/CHANGELOG.md index b27380ef..87c80169 100644 --- a/packages/react-strict-dom/CHANGELOG.md +++ b/packages/react-strict-dom/CHANGELOG.md @@ -6,6 +6,10 @@ * [Native] React Strict DOM and React Strict Animated now require `react-native >=0.82.0` for native builds that use RN DOM Node APIs. +### Fixes + +* [Native] Call inherited host node methods, such as `focus` and `blur`, on the host node so imperative ref APIs work again. + ### Internal * [Native] `useStrictDOMElement` now wraps the RN host node with `Object.create(node)` and defines only strict-dom overrides as own properties. diff --git a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js index a8a6c7bd..eaf42291 100644 --- a/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js +++ b/packages/react-strict-dom/src/native/modules/useStrictDOMElement.js @@ -49,6 +49,8 @@ type StrictRefTarget = { const memoizedStrictRefs: WeakMap = new WeakMap(); +const objectPrototype: interface {} | null = Object.getPrototypeOf({}); + const lengthPropertySet: ReadonlySet = new Set([ 'clientHeight', 'clientLeft', @@ -64,6 +66,45 @@ const lengthPropertySet: ReadonlySet = new Set([ 'scrollWidth' ]); +/** + * Re-exposes the host node's inherited methods as own properties that call + * through to the node. + * + * Reading a method off the wrapper resolves it via the prototype chain but + * calls it with the wrapper as the receiver. RN's host node methods read + * internals off `this`, and those internals are not reachable from the + * wrapper, so `strictRef.focus()` is a no-op while `node.focus()` works. + * Own methods of the node are left alone: RN assigns those as closures over + * the node, so their receiver does not matter. + */ +function bindInheritedMethods(strictRef: StrictRefTarget, node: HostInstance) { + let proto: interface {} | null = Object.getPrototypeOf(node); + while (proto != null && proto !== objectPrototype) { + const names: ReadonlyArray = Object.getOwnPropertyNames(proto); + for (const name of names) { + if ( + name === 'constructor' || + Object.getOwnPropertyDescriptor(strictRef, name) != null + ) { + continue; + } + const descriptor = Object.getOwnPropertyDescriptor(proto, name); + // Accessors are left on the prototype chain: they read the plain + // instance fields that the wrapper already inherits. + if (descriptor == null || typeof descriptor.value !== 'function') { + continue; + } + Object.defineProperty(strictRef, name, { + value: (...args: Array) => + // $FlowFixMe[prop-missing] - dynamic method call on the RN host node. + node[name](...args), + configurable: true + }); + } + proto = Object.getPrototypeOf(proto); + } +} + /** * Uses the RN host node as the wrapper's prototype so non-overridden reads * resolve via the prototype chain — including the symbol-keyed internals @@ -88,6 +129,10 @@ function getOrCreateStrictRef( // $FlowFixMe[class-object-subtyping] - read the host node's polyfill members. const nodeInternals: StrictRefPolyfills = node; + // Runs before the definitions below so the strict-dom overrides, such as the + // viewport-scaled `getBoundingClientRect`, still win. + bindInheritedMethods(strictRef, node); + Object.defineProperty(strictRef, 'nodeName', { value: tagName.toUpperCase(), configurable: true diff --git a/packages/react-strict-dom/tests/html/html-refs-test.native.js b/packages/react-strict-dom/tests/html/html-refs-test.native.js index 5b80dbe2..20dfda8e 100644 --- a/packages/react-strict-dom/tests/html/html-refs-test.native.js +++ b/packages/react-strict-dom/tests/html/html-refs-test.native.js @@ -94,6 +94,41 @@ describe(' refs', () => { }); }); + test('input ref calls inherited host node methods on the host node', () => { + // RN host instances define `focus` and `blur` on the prototype chain and + // read internals off `this`, so the receiver has to stay the host node. + const receivers = {}; + let hostNode; + function createNodeMock() { + const prototype = { + blur() { + receivers.blur = this; + }, + focus() { + receivers.focus = this; + } + }; + const ownProperties = buildHostNodeMock(); + delete ownProperties.blur; + delete ownProperties.focus; + hostNode = Object.assign(Object.create(prototype), ownProperties); + return hostNode; + } + act(() => { + create( + { + node.focus(); + node.blur(); + }} + />, + { createNodeMock } + ); + }); + expect(receivers.focus).toBe(hostNode); + expect(receivers.blur).toBe(hostNode); + }); + test('input ref does not throw when host node is null', () => { act(() => { create(