Skip to content

Commit d3c2bb4

Browse files
Yamin YassinYamin Yassin
authored andcommitted
Use prototype delegation in useStrictDOMElement
RN 0.82 settled the DOM Node prototype hierarchy, so we can stop cloning the host node. Object.create(node) makes the raw node the wrapper's prototype; strict-dom defines its overrides on top; everything else falls through. Symbol-keyed internals like INSTANCE_HANDLE_KEY stay reachable through the chain, so RN's prototype methods work when called on the wrapper. Changes vs the previous clone: * No descriptor snapshot. Reads stay in sync with the node. * No try/catch fallback. defineProperty on a fresh object can't fail in normal use. * getBoundingClientRect and the length getters only install when viewportScale isn't 1. Scale 1 skips them. * nodeName is a value descriptor now, since tagName.toUpperCase() doesn't change for a given wrapper. * writable: true removed from the value descriptors. DOM spec is read-only for nodeName, getBoundingClientRect and setSelectionRange, so strict-mode assignments throw now. * configurable: true on every override. instanceof still works: the chain is one link longer (wrapper to node to ReactNativeElement.prototype) but the class prototype is still on it. Ten tests in html-refs-test.native.js pass.
1 parent 27022ae commit d3c2bb4

2 files changed

Lines changed: 99 additions & 88 deletions

File tree

packages/react-strict-dom/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
### Internal
1010

11-
* [Native] `useStrictDOMElement` now wraps the underlying React Native host node in a thin `Proxy` instead of cloning it via `Object.create` and `Object.defineProperties`. Strict-dom continues to override `nodeName` (returns uppercase DOM names like `'DIV'`), to scale `getBoundingClientRect` and length properties by the active viewport scale, to polyfill `<img>.complete`, and to polyfill the `setSelectionRange` / `selectionStart` / `selectionEnd` trio on `<input>` / `<textarea>`. All other DOM Node properties and methods (`ownerDocument`, `getRootNode`, `children`, `childNodes`, `parentNode`, `parentElement`, `contains`, `compareDocumentPosition`, pointer-capture methods, legacy `measure*`, etc.) now pass through directly from the underlying RN node.
11+
* [Native] `useStrictDOMElement` now wraps the RN host node via `Object.create(node)` and defines only strict-dom's overrides as own properties. Non-overridden reads resolve through the prototype chain to the real node, keeping a static hidden class Hermes can optimize. Overrides unchanged: uppercase `nodeName`, viewport-scaled `getBoundingClientRect` and length properties, `<img>.complete`, and `setSelectionRange` / `selectionStart` / `selectionEnd` on `<input>` / `<textarea>`.
1212

1313
## 0.0.55 (Jan 9, 2026)
1414

packages/react-strict-dom/src/native/modules/useStrictDOMElement.js

Lines changed: 98 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ const lengthPropertySet: ReadonlySet<string> = new Set([
3939
]);
4040

4141
/**
42-
* Wraps a React Native host node in a Proxy so strict-dom can override the
43-
* remaining DOM APIs it polyfills while forwarding RN's DOM Node APIs.
44-
* Methods are bound to the host node so RN internals receive the expected
45-
* `this` value.
42+
* Uses the RN host node as the wrapper's prototype so non-overridden reads
43+
* resolve via the prototype chain — including the symbol-keyed internals
44+
* RN's prototype methods consult via `this`. Keeps a static hidden class so
45+
* Hermes can optimize access; a Proxy cannot.
46+
*
47+
* Descendants are not wrapped; values read after traversal are not scaled.
4648
*/
4749
function getOrCreateStrictRef(
4850
node: Node,
@@ -54,72 +56,87 @@ function getOrCreateStrictRef(
5456
return ref;
5557
}
5658

57-
const isImg = tagName === 'img';
58-
const isTextInput = tagName === 'input' || tagName === 'textarea';
59-
const upperTagName = tagName.toUpperCase();
60-
const scaled = (n: number) => n / viewportScale;
59+
const strictRef: Node = Object.create(node);
6160

62-
// $FlowFixMe[unclear-type] - ProxyHandler is not in Flow's built-in types.
63-
const handler: any = {
64-
get(target, prop, _receiver) {
65-
if (prop === 'nodeName') {
66-
return upperTagName;
67-
}
68-
if (prop === 'getBoundingClientRect') {
69-
const fn = target.getBoundingClientRect;
70-
if (typeof fn !== 'function') {
71-
return fn;
72-
}
73-
return () => {
74-
const rect = fn.call(target);
75-
if (viewportScale !== 1) {
76-
return new DOMRect(
77-
scaled(rect.x),
78-
scaled(rect.y),
79-
scaled(rect.width),
80-
scaled(rect.height)
81-
);
82-
}
83-
return rect;
84-
};
85-
}
86-
if (
87-
viewportScale !== 1 &&
88-
typeof prop === 'string' &&
89-
lengthPropertySet.has(prop)
90-
) {
91-
const raw = target[prop];
92-
return typeof raw === 'number' ? scaled(raw) : raw;
93-
}
94-
if (isImg && prop === 'complete') {
95-
return target.complete ?? false;
96-
}
97-
if (isTextInput) {
98-
if (prop === 'setSelectionRange' && target.setSelectionRange == null) {
99-
return (a: number, b: number) => {
100-
target.setSelection(a, b);
101-
target._selectionStart = a;
102-
target._selectionEnd = b;
103-
};
104-
}
105-
if (prop === 'selectionStart' && target.selectionStart == null) {
106-
return target._selectionStart ?? 0;
107-
}
108-
if (prop === 'selectionEnd' && target.selectionEnd == null) {
109-
return target._selectionEnd ?? 0;
110-
}
111-
}
112-
// $FlowFixMe[unclear-type]
113-
const value: any = Reflect.get(target, prop, target);
114-
if (typeof value === 'function') {
115-
return value.bind(target);
61+
// $FlowFixMe[prop-missing]
62+
Object.defineProperty(strictRef, 'nodeName', {
63+
value: tagName.toUpperCase(),
64+
configurable: true
65+
});
66+
67+
if (viewportScale !== 1) {
68+
const scale = (n: number) => n / viewportScale;
69+
70+
if (typeof node.getBoundingClientRect === 'function') {
71+
// $FlowFixMe[prop-missing]
72+
Object.defineProperty(strictRef, 'getBoundingClientRect', {
73+
value: () => {
74+
const rect = node.getBoundingClientRect();
75+
return new DOMRect(
76+
scale(rect.x),
77+
scale(rect.y),
78+
scale(rect.width),
79+
scale(rect.height)
80+
);
81+
},
82+
configurable: true
83+
});
84+
}
85+
86+
for (const prop of lengthPropertySet) {
87+
if (prop in node) {
88+
// $FlowFixMe[prop-missing]
89+
Object.defineProperty(strictRef, prop, {
90+
get() {
91+
const value = node[prop];
92+
return typeof value === 'number' ? scale(value) : value;
93+
},
94+
configurable: true
95+
});
11696
}
117-
return value;
11897
}
119-
};
98+
}
99+
100+
if (tagName === 'img') {
101+
// $FlowFixMe[prop-missing]
102+
Object.defineProperty(strictRef, 'complete', {
103+
get() {
104+
return node.complete ?? false;
105+
},
106+
configurable: true
107+
});
108+
} else if (tagName === 'input' || tagName === 'textarea') {
109+
if (node.setSelectionRange == null) {
110+
// $FlowFixMe[prop-missing]
111+
Object.defineProperty(strictRef, 'setSelectionRange', {
112+
value: (a: number, b: number) => {
113+
node.setSelection(a, b);
114+
node._selectionStart = a;
115+
node._selectionEnd = b;
116+
},
117+
configurable: true
118+
});
119+
}
120+
if (node.selectionStart == null) {
121+
// $FlowFixMe[prop-missing]
122+
Object.defineProperty(strictRef, 'selectionStart', {
123+
get() {
124+
return node._selectionStart ?? 0;
125+
},
126+
configurable: true
127+
});
128+
}
129+
if (node.selectionEnd == null) {
130+
// $FlowFixMe[prop-missing]
131+
Object.defineProperty(strictRef, 'selectionEnd', {
132+
get() {
133+
return node._selectionEnd ?? 0;
134+
},
135+
configurable: true
136+
});
137+
}
138+
}
120139

121-
// $FlowFixMe[invalid-constructor] - Flow types Proxy strictly here.
122-
const strictRef: Node = new Proxy(node, handler);
123140
memoizedStrictRefs.set(node, strictRef);
124141
return strictRef;
125142
}
@@ -130,33 +147,27 @@ export function useStrictDOMElement<T>(
130147
): CallbackRef<T> {
131148
const { scale: viewportScale } = useViewportScale();
132149

133-
const elementCallback = useElementCallback(
150+
return useElementCallback(
134151
React.useCallback(
135152
// $FlowFixMe[unclear-type]
136153
(node: any) => {
137-
if (ref == null) {
138-
return undefined;
139-
} else {
140-
const strictRef = getOrCreateStrictRef(node, tagName, viewportScale);
141-
if (typeof ref === 'function') {
142-
// $FlowFixMe[incompatible-type] - Flow does not understand ref cleanup.
143-
const cleanup: void | (() => void) = ref(strictRef);
144-
return typeof cleanup === 'function'
145-
? cleanup
146-
: () => {
147-
ref(null);
148-
};
149-
} else {
150-
ref.current = strictRef;
151-
return () => {
152-
ref.current = null;
153-
};
154-
}
154+
if (ref == null) return undefined;
155+
const strictRef = getOrCreateStrictRef(node, tagName, viewportScale);
156+
if (typeof ref === 'function') {
157+
// $FlowFixMe[incompatible-type] - Flow does not understand ref cleanup.
158+
const cleanup: void | (() => void) = ref(strictRef);
159+
return typeof cleanup === 'function'
160+
? cleanup
161+
: () => {
162+
ref(null);
163+
};
155164
}
165+
ref.current = strictRef;
166+
return () => {
167+
ref.current = null;
168+
};
156169
},
157170
[ref, tagName, viewportScale]
158171
)
159172
);
160-
161-
return elementCallback;
162173
}

0 commit comments

Comments
 (0)