Repository navigation
Report a VirtualGestureDetector child that swallows the ref
#4596
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,62 +1,82 @@ | ||||||
| import type { PropsWithChildren } from 'react'; | ||||||
| import React, { useCallback } from 'react'; | ||||||
| import React from 'react'; | ||||||
|
|
||||||
| import type { WrapRef } from '../../../hostInstance'; | ||||||
| import { assignRef, preferHostInstance } from '../../../hostInstance'; | ||||||
| import { | ||||||
| assignRef, | ||||||
| detachRef, | ||||||
| preferHostInstance, | ||||||
| } from '../../../hostInstance'; | ||||||
| import { tagMessage } from '../../../utils'; | ||||||
|
|
||||||
| export type { WrapRef }; | ||||||
|
|
||||||
| export type WrapProps = PropsWithChildren<{ ref?: WrapRef }>; | ||||||
|
|
||||||
| export const Wrap: React.FunctionComponent<WrapProps> = ({ ref, children }) => { | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| let child: any; | ||||||
| try { | ||||||
| child = React.Children.only(children); | ||||||
| } catch (e) { | ||||||
| throw new Error( | ||||||
| tagMessage( | ||||||
| `VirtualGestureDetector expects exactly one React element as its child. To use a gesture with multiple views, wrap them in a single parent view and attach the gesture to that.` | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
| export type WrapProps = PropsWithChildren<{ | ||||||
| hostRef?: WrapRef; | ||||||
| }>; | ||||||
|
|
||||||
| // A class on purpose: the detector walks the fiber from its instance to tell | ||||||
| // a child that swallowed the ref from one that has no host view yet. | ||||||
| export class Wrap extends React.Component<WrapProps> { | ||||||
| private childRef?: WrapRef; | ||||||
| private hostRef?: WrapRef; | ||||||
|
|
||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||||||
| const childRef = child.props.ref as WrapRef; | ||||||
| private attachRef: ((instance: unknown) => (() => void) | undefined) | null = | ||||||
| null; | ||||||
|
|
||||||
| const attachRef = useCallback( | ||||||
| (instance: unknown) => { | ||||||
| private createAttachRef(childRef: WrapRef, hostRef: WrapRef) { | ||||||
| return (instance: unknown) => { | ||||||
| const childCleanup = assignRef(childRef, instance); | ||||||
| const forwardedCleanup = assignRef(ref, preferHostInstance(instance)); | ||||||
| const hostCleanup = assignRef(hostRef, preferHostInstance(instance)); | ||||||
|
|
||||||
| const hasCleanup = | ||||||
| typeof childCleanup === 'function' || | ||||||
| typeof forwardedCleanup === 'function'; | ||||||
| typeof childCleanup === 'function' || typeof hostCleanup === 'function'; | ||||||
|
|
||||||
| if (!hasCleanup) { | ||||||
| return undefined; | ||||||
| } | ||||||
|
|
||||||
| return () => { | ||||||
| if (typeof childCleanup === 'function') { | ||||||
| childCleanup(); | ||||||
| } else { | ||||||
| assignRef(childRef, null); | ||||||
| } | ||||||
| if (typeof forwardedCleanup === 'function') { | ||||||
| forwardedCleanup(); | ||||||
| } else { | ||||||
| assignRef(ref, null); | ||||||
| } | ||||||
| detachRef(childRef, childCleanup); | ||||||
| detachRef(hostRef, hostCleanup); | ||||||
| }; | ||||||
| }, | ||||||
| [childRef, ref] | ||||||
| ); | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| override render() { | ||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
| let child: any; | ||||||
|
|
||||||
| try { | ||||||
| child = React.Children.only(this.props.children); | ||||||
| } catch (e) { | ||||||
| throw new Error( | ||||||
| tagMessage( | ||||||
| `VirtualGestureDetector expects exactly one React element as its child. To use a gesture with multiple views, wrap them in a single parent view and attach the gesture to that.` | ||||||
| ) | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| return React.cloneElement( | ||||||
| child, | ||||||
| { collapsable: false, ref: attachRef }, | ||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||||||
| child.props.children | ||||||
| ); | ||||||
| }; | ||||||
| const childRef = child.props.ref as WrapRef; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,120p' packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/Wrap.tsx
sed -n '1,195p' packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/VirtualDetector.tsx
rg -n '"react"|reactVersion|React 19|React 18|VirtualDetector' packages/react-native-gesture-handler/package.json packages/react-native-gesture-handler/src/v3 packages/react-native-gesture-handler/src/__tests__/Errors.test.tsx | head -90Repository: software-mansion/react-native-gesture-handler Length of output: 12002 🏁 Script executed: rg -n -i -e 'react.?18' -e 'react 19' -e 'peerDependencies' -e 'react-version' -- packages/react-native-gesture-handler/package.json packages/react-native-gesture-handler/src/v3 docs README.md apps/ .github 2>/dev/null || test "$?" -eq 1
sed -n '95,115p' packages/react-native-gesture-handler/package.jsonRepository: software-mansion/react-native-gesture-handler Length of output: 1098 Make the v3 child-ref lookup version-aware, or document React 19 as a v3 requirement.
Suggested fix--- "a/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/Wrap.tsx"
+++ "b/packages/react-native-gesture-handler/src/v3/detectors/VirtualDetector/Wrap.tsx"
@@ -58,7 +58,7 @@
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- const childRef = child.props.ref as WrapRef;
+ const childRef = (child.props.ref ?? child.ref) as WrapRef;
const { hostRef } = this.props;
// A new callback whenever either ref changes, so React re-attaches it.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| const { hostRef } = this.props; | ||||||
|
|
||||||
| // A new callback whenever either ref changes, so React re-attaches it. | ||||||
| if ( | ||||||
| this.attachRef === null || | ||||||
| childRef !== this.childRef || | ||||||
| hostRef !== this.hostRef | ||||||
| ) { | ||||||
| this.childRef = childRef; | ||||||
| this.hostRef = hostRef; | ||||||
| this.attachRef = this.createAttachRef(childRef, hostRef); | ||||||
| } | ||||||
|
|
||||||
| return React.cloneElement( | ||||||
| child, | ||||||
| { collapsable: false, ref: this.attachRef }, | ||||||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||||||
| child.props.children | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this cause a strict mode error?