Skip to content

Commit 083f2a6

Browse files
committed
Go back to using ref callback instead of useImperativeHandle
We shouldn't create an object if the underlying node is null, as this handle is meant to emulate the node but can't proxy methods if none exist. Since useImperativeHandle isn't reset when the underlying node changes, we go back to using ref callbacks. Rather than mutating the underling node, we create a new object that wraps the relevant fields.
1 parent bcb6534 commit 083f2a6

File tree

5 files changed

+430
-1729
lines changed

5 files changed

+430
-1729
lines changed

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

Lines changed: 82 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
* @flow strict-local
88
*/
99

10+
import type { CallbackRef } from '../../types/react';
11+
1012
import * as React from 'react';
1113

1214
import { errorMsg } from '../../shared/logUtils';
15+
import { useElementCallback } from '../../shared/useElementCallback';
1316
import { useViewportScale } from './ContextViewportScale';
1417

1518
type Options = {
@@ -25,9 +28,8 @@ function errorUnimplemented(name: string) {
2528
}
2629
}
2730

28-
function proxy(nativeRef: React.RefObject<Node>, name: string) {
31+
function proxy(node: Node, name: string) {
2932
return (...args: Array<mixed>) => {
30-
const node = nativeRef.current;
3133
if (node?.[name]) {
3234
return node[name](...args);
3335
}
@@ -38,68 +40,89 @@ function proxy(nativeRef: React.RefObject<Node>, name: string) {
3840
export function useStrictDOMElement<T>(
3941
ref: React.RefSetter<Node>,
4042
{ tagName }: Options
41-
): React.RefObject<T | null> {
42-
const nativeRef = React.useRef<T | null>(null);
43+
): CallbackRef<T> {
4344
const { scale: viewportScale } = useViewportScale();
4445

45-
React.useImperativeHandle(ref, () => {
46-
return {
47-
get __nativeTag() {
48-
const node = nativeRef.current as Node;
49-
return node?.__nativeTag;
50-
},
51-
addEventListener: proxy(nativeRef, 'addEventListener'),
52-
animate: proxy(nativeRef, 'animate'),
53-
blur: proxy(nativeRef, 'blur'),
54-
click: proxy(nativeRef, 'click'),
55-
get complete() {
56-
if (tagName === 'img') {
57-
const node = nativeRef.current as Node;
58-
if (node?.complete == null) {
59-
// Assume images are never pre-loaded in React Native
60-
return false;
46+
const elementCallback = useElementCallback(
47+
React.useCallback(
48+
// $FlowFixMe[unclear-type]
49+
(node: any) => {
50+
const obj = {
51+
get __nativeTag() {
52+
return node?.__nativeTag;
53+
},
54+
addEventListener: proxy(node, 'addEventListener'),
55+
animate: proxy(node, 'animate'),
56+
blur: proxy(node, 'blur'),
57+
click: proxy(node, 'click'),
58+
get complete() {
59+
if (tagName === 'img') {
60+
if (node?.complete == null) {
61+
// Assume images are never pre-loaded in React Native
62+
return false;
63+
} else {
64+
return node.complete;
65+
}
66+
}
67+
},
68+
contains: proxy(node, 'contains'),
69+
dispatchEvent: proxy(node, 'dispatchEvent'),
70+
focus: proxy(node, 'focus'),
71+
getAttribute: proxy(node, 'getAttribute'),
72+
getBoundingClientRect() {
73+
const getBoundingClientRect =
74+
node?.getBoundingClientRect ??
75+
node?.unstable_getBoundingClientRect;
76+
if (getBoundingClientRect) {
77+
const rect = getBoundingClientRect.call(node);
78+
if (viewportScale !== 1) {
79+
return new DOMRect(
80+
rect.x / viewportScale,
81+
rect.y / viewportScale,
82+
rect.width / viewportScale,
83+
rect.height / viewportScale
84+
);
85+
}
86+
return rect;
87+
}
88+
return errorUnimplemented('getBoundingClientRect');
89+
},
90+
getRootNode: proxy(node, 'getRootNode'),
91+
hasPointerCapture: proxy(node, 'hasPointerCapture'),
92+
nodeName: tagName.toUpperCase(),
93+
releasePointerCapture: proxy(node, 'releasePointerCapture'),
94+
removeEventListener: proxy(node, 'removeEventListener'),
95+
scroll: proxy(node, 'scroll'),
96+
scrollBy: proxy(node, 'scrollBy'),
97+
scrollIntoView: proxy(node, 'scrollIntoView'),
98+
scrollTo: proxy(node, 'scrollTo'),
99+
select: proxy(node, 'select'),
100+
setSelectionRange: proxy(node, 'setSelectionRange'),
101+
setPointerCapture: proxy(node, 'setPointerCapture'),
102+
showPicker: proxy(node, 'showPicker')
103+
};
104+
if (ref == null) {
105+
return undefined;
106+
} else {
107+
if (typeof ref === 'function') {
108+
// $FlowFixMe[incompatible-type] - Flow does not understand ref cleanup.
109+
const cleanup: void | (() => void) = ref(obj);
110+
return typeof cleanup === 'function'
111+
? cleanup
112+
: () => {
113+
ref(null);
114+
};
61115
} else {
62-
return node.complete;
63-
}
64-
}
65-
},
66-
contains: proxy(nativeRef, 'contains'),
67-
dispatchEvent: proxy(nativeRef, 'dispatchEvent'),
68-
focus: proxy(nativeRef, 'focus'),
69-
getAttribute: proxy(nativeRef, 'getAttribute'),
70-
getBoundingClientRect() {
71-
const node = nativeRef.current as Node;
72-
const getBoundingClientRect =
73-
node?.getBoundingClientRect ?? node?.unstable_getBoundingClientRect;
74-
if (getBoundingClientRect) {
75-
const rect = getBoundingClientRect.call(node);
76-
if (viewportScale !== 1) {
77-
return new DOMRect(
78-
rect.x / viewportScale,
79-
rect.y / viewportScale,
80-
rect.width / viewportScale,
81-
rect.height / viewportScale
82-
);
116+
ref.current = obj;
117+
return () => {
118+
ref.current = null;
119+
};
83120
}
84-
return rect;
85121
}
86-
return errorUnimplemented('getBoundingClientRect');
87122
},
88-
getRootNode: proxy(nativeRef, 'getRootNode'),
89-
hasPointerCapture: proxy(nativeRef, 'hasPointerCapture'),
90-
nodeName: tagName.toUpperCase(),
91-
releasePointerCapture: proxy(nativeRef, 'releasePointerCapture'),
92-
removeEventListener: proxy(nativeRef, 'removeEventListener'),
93-
scroll: proxy(nativeRef, 'scroll'),
94-
scrollBy: proxy(nativeRef, 'scrollBy'),
95-
scrollIntoView: proxy(nativeRef, 'scrollIntoView'),
96-
scrollTo: proxy(nativeRef, 'scrollTo'),
97-
select: proxy(nativeRef, 'select'),
98-
setSelectionRange: proxy(nativeRef, 'setSelectionRange'),
99-
setPointerCapture: proxy(nativeRef, 'setPointerCapture'),
100-
showPicker: proxy(nativeRef, 'showPicker')
101-
};
102-
}, [tagName, viewportScale]);
123+
[ref, tagName, viewportScale]
124+
)
125+
);
103126

104-
return nativeRef;
127+
return elementCallback;
105128
}

packages/react-strict-dom/tests/__snapshots__/compat-test.native.js.snap-native

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
exports[`<compat.native> "as" equals "image": as=image 1`] = `
44
<Image
55
accessibilityLabel="label"
6-
ref={
7-
{
8-
"current": null,
9-
}
10-
}
6+
ref={[Function]}
117
srcSet="1x.img, 2x.img 2x"
128
style={
139
{
@@ -23,11 +19,7 @@ exports[`<compat.native> "as" equals "input": as=input 1`] = `
2319
<TextInput
2420
accessibilityLabel="label"
2521
placeholderTextColor="green"
26-
ref={
27-
{
28-
"current": null,
29-
}
30-
}
22+
ref={[Function]}
3123
secureTextEntry={true}
3224
style={
3325
{
@@ -42,11 +34,7 @@ exports[`<compat.native> "as" equals "text": as=text 1`] = `
4234
<Text
4335
accessibilityLabel="label"
4436
numberOfLines={3}
45-
ref={
46-
{
47-
"current": null,
48-
}
49-
}
37+
ref={[Function]}
5038
style={
5139
{
5240
"boxSizing": "content-box",
@@ -63,11 +51,7 @@ exports[`<compat.native> "as" equals "textarea": as=textarea 1`] = `
6351
accessibilityLabel="label"
6452
multiline={true}
6553
numberOfLines={3}
66-
ref={
67-
{
68-
"current": null,
69-
}
70-
}
54+
ref={[Function]}
7155
style={
7256
{
7357
"boxSizing": "content-box",
@@ -80,11 +64,7 @@ exports[`<compat.native> "as" equals "textarea": as=textarea 1`] = `
8064
exports[`<compat.native> "as" equals "view": as=view 1`] = `
8165
<View
8266
accessibilityLabel="label"
83-
ref={
84-
{
85-
"current": null,
86-
}
87-
}
67+
ref={[Function]}
8868
style={
8969
{
9070
"boxSizing": "content-box",
@@ -98,11 +78,7 @@ exports[`<compat.native> "as" equals "view": as=view 1`] = `
9878
exports[`<compat.native> default: default 1`] = `
9979
<Pressable
10080
accessibilityLabel="label"
101-
ref={
102-
{
103-
"current": null,
104-
}
105-
}
81+
ref={[Function]}
10682
style={
10783
{
10884
"boxSizing": "content-box",
@@ -116,11 +92,7 @@ exports[`<compat.native> default: default 1`] = `
11692
exports[`<compat.native> nested: nested 1`] = `
11793
<View
11894
accessibilityLabel="label"
119-
ref={
120-
{
121-
"current": null,
122-
}
123-
}
95+
ref={[Function]}
12496
style={
12597
{
12698
"boxSizing": "content-box",
@@ -129,11 +101,7 @@ exports[`<compat.native> nested: nested 1`] = `
129101
}
130102
>
131103
<Text
132-
ref={
133-
{
134-
"current": null,
135-
}
136-
}
104+
ref={[Function]}
137105
style={
138106
{
139107
"boxSizing": "content-box",

0 commit comments

Comments
 (0)