Skip to content

Commit d9f8e39

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 d9f8e39

File tree

5 files changed

+423
-1705
lines changed

5 files changed

+423
-1705
lines changed

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

Lines changed: 75 additions & 35 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,36 +28,36 @@ 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
}
3436
errorUnimplemented(name);
3537
};
3638
}
3739

38-
export function useStrictDOMElement<T>(
39-
ref: React.RefSetter<Node>,
40-
{ tagName }: Options
41-
): React.RefObject<T | null> {
42-
const nativeRef = React.useRef<T | null>(null);
43-
const { scale: viewportScale } = useViewportScale();
40+
const memoizedStrictRefs: WeakMap<Node, mixed> = new WeakMap();
4441

45-
React.useImperativeHandle(ref, () => {
46-
return {
42+
function getOrCreateStrictRef(
43+
node: Node,
44+
tagName: string,
45+
viewportScale: number
46+
) {
47+
const ref = memoizedStrictRefs.get(node);
48+
if (ref != null) {
49+
return ref;
50+
} else {
51+
const strictRef = {
4752
get __nativeTag() {
48-
const node = nativeRef.current as Node;
4953
return node?.__nativeTag;
5054
},
51-
addEventListener: proxy(nativeRef, 'addEventListener'),
52-
animate: proxy(nativeRef, 'animate'),
53-
blur: proxy(nativeRef, 'blur'),
54-
click: proxy(nativeRef, 'click'),
55+
addEventListener: proxy(node, 'addEventListener'),
56+
animate: proxy(node, 'animate'),
57+
blur: proxy(node, 'blur'),
58+
click: proxy(node, 'click'),
5559
get complete() {
5660
if (tagName === 'img') {
57-
const node = nativeRef.current as Node;
5861
if (node?.complete == null) {
5962
// Assume images are never pre-loaded in React Native
6063
return false;
@@ -63,12 +66,11 @@ export function useStrictDOMElement<T>(
6366
}
6467
}
6568
},
66-
contains: proxy(nativeRef, 'contains'),
67-
dispatchEvent: proxy(nativeRef, 'dispatchEvent'),
68-
focus: proxy(nativeRef, 'focus'),
69-
getAttribute: proxy(nativeRef, 'getAttribute'),
69+
contains: proxy(node, 'contains'),
70+
dispatchEvent: proxy(node, 'dispatchEvent'),
71+
focus: proxy(node, 'focus'),
72+
getAttribute: proxy(node, 'getAttribute'),
7073
getBoundingClientRect() {
71-
const node = nativeRef.current as Node;
7274
const getBoundingClientRect =
7375
node?.getBoundingClientRect ?? node?.unstable_getBoundingClientRect;
7476
if (getBoundingClientRect) {
@@ -85,21 +87,59 @@ export function useStrictDOMElement<T>(
8587
}
8688
return errorUnimplemented('getBoundingClientRect');
8789
},
88-
getRootNode: proxy(nativeRef, 'getRootNode'),
89-
hasPointerCapture: proxy(nativeRef, 'hasPointerCapture'),
90+
getRootNode: proxy(node, 'getRootNode'),
91+
hasPointerCapture: proxy(node, 'hasPointerCapture'),
9092
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')
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')
101103
};
102-
}, [tagName, viewportScale]);
103104

104-
return nativeRef;
105+
memoizedStrictRefs.set(node, strictRef);
106+
return strictRef;
107+
}
108+
}
109+
110+
export function useStrictDOMElement<T>(
111+
ref: React.RefSetter<Node>,
112+
{ tagName }: Options
113+
): CallbackRef<T> {
114+
const { scale: viewportScale } = useViewportScale();
115+
116+
const elementCallback = useElementCallback(
117+
React.useCallback(
118+
// $FlowFixMe[unclear-type]
119+
(node: any) => {
120+
if (ref == null) {
121+
return undefined;
122+
} else {
123+
const strictRef = getOrCreateStrictRef(node, tagName, viewportScale);
124+
if (typeof ref === 'function') {
125+
// $FlowFixMe[incompatible-type] - Flow does not understand ref cleanup.
126+
const cleanup: void | (() => void) = ref(strictRef);
127+
return typeof cleanup === 'function'
128+
? cleanup
129+
: () => {
130+
ref(null);
131+
};
132+
} else {
133+
ref.current = strictRef;
134+
return () => {
135+
ref.current = null;
136+
};
137+
}
138+
}
139+
},
140+
[ref, tagName, viewportScale]
141+
)
142+
);
143+
144+
return elementCallback;
105145
}

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)