Skip to content

fix(switch): forward ref to the root view - #5065

Open
giaBaoJS wants to merge 2 commits into
callstack:mainfrom
giaBaoJS:fix/switch-forward-ref
Open

fix(switch): forward ref to the root view#5065
giaBaoJS wants to merge 2 commits into
callstack:mainfrom
giaBaoJS:fix/switch-forward-ref

Conversation

@giaBaoJS

Copy link
Copy Markdown

Motivation

Switch accepts no ref. Props does not declare one, the component signature never destructures one, and the root <View style={[styles.wrapper, style]}> is not given one — so ref.current stays null and there is no way to measure() or otherwise reach the switch's host view.

This looks like a leftover from the Reanimated/Pressable rewrite: the surrounding components carry the prop over correctly (Appbar/AppbarBackAction.tsx, FAB/Extended.tsx, Surface.tsx, List/ListItem.tsx, TouchableRipple), Switch just did not.

Scope / correction to the issue

#3729 names both Avatar and Switch. I checked Avatar and it is not affected: AvatarText, AvatarIcon and AvatarImage all spread ...rest onto their root View, so a ref passed to them does land on the host view (I verified ref.current is non-null for all three). Their Props types just do not advertise ref, which is a typing gap rather than a runtime bug — happy to add the declarations in a follow-up if you want.

So this PR only fixes Switch. It does not close #3729 on its own, since that issue asks for the whole surface. One thing I did notice while looking: Banner.tsx declares ref?: React.RefObject<View> in its Props but never uses the value, so Banner drops refs too — again, happy to do that separately.

Note on the ref type

Peers use ref?: React.Ref<View>, but those all forward to a component whose props already carry React.RefAttributes<View> (Animated.View, Pressable, TouchableRipple). Switch's root is a plain <View>, and putting a React.Ref<View> on it fails yarn typecheck:

src/components/Switch/Switch.tsx(351,11): error TS2769: No overload matches this call.
  Type 'import(".../node_modules/@types/react/index").Ref<View> | undefined' is not
  assignable to type 'React.Ref<View> | undefined'.
    ... Two different types with this name exist, but they are unrelated.

tsc -b also builds docs/, which has its own copy of @types/react (same version, separate directory), and the unique symbol behind VoidOrUndefinedOnly in the callback-ref signature does not unify across the two copies. React.RefObject<View | null> is structural and unaffected, matches what React.createRef<View>() and useRef<View>(null) produce, and follows the existing declaration in Banner.tsx. Glad to switch it to React.Ref<View> if you'd rather fix the duplicate-types setup instead.

Test plan

Two cases added to src/components/__tests__/Switch.test.tsx:

  • ref.current is not null
  • ref.current.measure is a function (i.e. it is the host view, not some other object)

Both fail before the change (Received: null / Received: "undefined") and pass after.

yarn test: 55 suites, 737 passed / 1 skipped, 169 snapshots — no snapshot churn. yarn lint and yarn typecheck clean.

Refs #3729.

@MikitasK MikitasK left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

look good overall 👌
just 2 non-blocking suggestions to consider before merge:

Comment thread src/components/Switch/Switch.tsx Outdated
* Accessibility label for the switch. This is read by the screen reader when the user focuses the switch.
*/
'aria-label'?: string;
ref?: React.RefObject<View | null>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about widening ref type to support callback & forwarded refs too?
React.RefObject currently accepts only object refs so passing React.RefCallback<View> / ForwardedRef<View> results in TS error

Suggested change
ref?: React.RefObject<View | null>;
ref?:
| React.RefObject<View | null>
| ((instance: View | null) => void)
| null;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken as written. Worth recording why the shorter spelling does not work here, in case it comes up again.

The repo's usual form for this prop is React.Ref<View> (Surface.tsx:56, Button.tsx:138, TouchableRipple.tsx:73), so I tried that first. It does not compile on Switch:

src/components/Switch/Switch.tsx(351,11): error TS2769: No overload matches this call.
  Overload 1 of 2, '(props: ViewProps): View', gave the following error.
    Type '(instance: View | null) => void | (() => VoidOrUndefinedOnly)' is not assignable to type 'Ref<View> | undefined'.
      ... Two different types with this name exist, but they are unrelated.

React 19's RefCallback may return a cleanup function, and @types/react declares that return type in two files (index.d.ts and ts5.0/index.d.ts, picked by typesVersions). Switch attaches the ref to a bare RN <View>, which is where the two declarations meet and fail to unify. The other components avoid it because they forward to Animated.View or to their own wrapper, not to a raw View.

Your explicit (instance: View | null) => void sidesteps that, since a void return position accepts a function returning anything. So the suggestion is used verbatim.

await render(<Switch value ref={ref} />);

expect(typeof ref.current?.measure).toBe('function');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add coverage for a callback ref as well?
both current tests use React.createRef, so they only exercise object refs & wouldn’t catch this type/API regression

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added accepts a callback ref, typed as React.RefCallback<View> so it exercises the widened type rather than only the runtime path. Switch.test.tsx goes from 12 to 13.

Verified both halves separately, since a type widening is not covered by a runtime assertion:

Type: with the new test in place but the prop still React.RefObject<View | null>, tsc -b fails with src/components/__tests__/Switch.test.tsx(104,32): error TS2322: Type '(instance: View | null) => void | (() => VoidOrUndefinedOnly)' is not assignable to type 'RefObject<View | null>'. Widening the prop clears it.

Runtime: with Switch.tsx reverted to its pre-PR state, all three ref tests fail, the new one with expect(received).not.toBeNull() / Received: null.

Widen the ref prop so callback and forwarded refs typecheck, and cover
the callback ref path in tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Not all componetns can receive ref

2 participants