Skip to content

Commit 2e98457

Browse files
authored
Remove forwardRef (#4093)
## Description Since React 19 it is not necessary to use `forwardRef`. I've run `npx eslint --fix` and it removed them, though it required some adjustments. This PR also contains few other fixes, like not using `.Provider` in context, or some imports which were not previously caught by linter. ## Test plan `yarn lint-js` && `yarn ts-check` <details> <summary>Refs tested on the following code:</summary> ```tsx import type { ComponentType } from 'react'; import { useRef, useState } from 'react'; import type { FlatList as RNFlatList, ScrollView as RNScrollView, } from 'react-native'; import { StyleSheet, Text, View } from 'react-native'; import { FlatList, GestureHandlerRootView, LegacyBaseButton, LegacyBorderlessButton, LegacyFlatList, LegacyRectButton, LegacyScrollView, RectButton, ScrollView, TouchableWithoutFeedback, } from 'react-native-gesture-handler'; import ReanimatedDrawerLayout, { DrawerPosition, } from 'react-native-gesture-handler/ReanimatedDrawerLayout'; const DATA = Array.from({ length: 30 }, (_, i) => ({ key: `item-${i}`, title: `Item ${i}`, })); function showRef(ref: React.RefObject<unknown>) { console.log(ref.current); } // ────────────────────────────────────────────── // 1. Legacy ScrollView ref // ────────────────────────────────────────────── function LegacyScrollViewRefTest() { const ref = useRef<RNScrollView>(null); return ( <View style={styles.section}> <Text style={styles.heading}>Legacy ScrollView ref</Text> <RectButton style={styles.btn} onPress={() => showRef(ref)}> <Text>Check ref</Text> </RectButton> <RectButton style={styles.btn} onPress={() => ref.current?.scrollTo({ y: 200, animated: true })}> <Text>scrollTo(200)</Text> </RectButton> <LegacyScrollView ref={ref} style={styles.scrollBox}> {DATA.map((d) => ( <Text key={d.key} style={styles.item}> {d.title} </Text> ))} </LegacyScrollView> </View> ); } // ────────────────────────────────────────────── // 2. V3 ScrollView ref // ────────────────────────────────────────────── function V3ScrollViewRefTest() { const ref = useRef<RNScrollView>(null); return ( <View style={styles.section}> <Text style={styles.heading}>V3 ScrollView ref</Text> <RectButton style={styles.btn} onPress={() => showRef(ref)}> <Text>Check ref</Text> </RectButton> <RectButton style={styles.btn} onPress={() => ref.current?.scrollTo({ y: 200, animated: true })}> <Text>scrollTo(200)</Text> </RectButton> <ScrollView ref={ref} style={styles.scrollBox}> {DATA.map((d) => ( <Text key={d.key} style={styles.item}> {d.title} </Text> ))} </ScrollView> </View> ); } // ────────────────────────────────────────────── // 3. Legacy FlatList ref // ────────────────────────────────────────────── function LegacyFlatListRefTest() { const ref = useRef<RNFlatList>(null); return ( <View style={styles.section}> <Text style={styles.heading}>Legacy FlatList ref</Text> <RectButton style={styles.btn} onPress={() => showRef(ref)}> <Text>Check ref</Text> </RectButton> <RectButton style={styles.btn} onPress={() => ref.current?.scrollToIndex({ index: 10, animated: true }) }> <Text>scrollToIndex(10)</Text> </RectButton> <LegacyFlatList ref={ref} style={styles.scrollBox} data={DATA} renderItem={({ item }) => <Text style={styles.item}>{item.title}</Text>} /> </View> ); } // ────────────────────────────────────────────── // 4. V3 FlatList ref // ────────────────────────────────────────────── function V3FlatListRefTest() { const ref = useRef<RNFlatList>(null); return ( <View style={styles.section}> <Text style={styles.heading}>V3 FlatList ref</Text> <RectButton style={styles.btn} onPress={() => showRef(ref)}> <Text>Check ref</Text> </RectButton> <RectButton style={styles.btn} onPress={() => ref.current?.scrollToIndex({ index: 10, animated: true }) }> <Text>scrollToIndex(10)</Text> </RectButton> <FlatList ref={ref} style={styles.scrollBox} data={DATA} renderItem={({ item }) => <Text style={styles.item}>{item.title}</Text>} /> </View> ); } // ────────────────────────────────────────────── // 5. Legacy Buttons refs // ────────────────────────────────────────────── function LegacyButtonRefsTest() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const baseRef = useRef<ComponentType<any>>(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const rectRef = useRef<ComponentType<any>>(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const borderlessRef = useRef<ComponentType<any>>(null); return ( <View style={styles.section}> <Text style={styles.heading}>Legacy Button refs</Text> <LegacyBaseButton ref={baseRef} style={styles.btn} onPress={() => showRef(baseRef)}> <Text>BaseButton - tap to check ref</Text> </LegacyBaseButton> <LegacyRectButton ref={rectRef} style={styles.btn} onPress={() => showRef(rectRef)}> <Text>RectButton - tap to check ref</Text> </LegacyRectButton> <LegacyBorderlessButton ref={borderlessRef} style={styles.btn} onPress={() => showRef(borderlessRef)}> <Text>BorderlessButton - tap to check ref</Text> </LegacyBorderlessButton> </View> ); } // ────────────────────────────────────────────── // 6. TouchableWithoutFeedback ref // ────────────────────────────────────────────── function TouchableRefTest() { const ref = useRef(null); return ( <View style={styles.section}> <Text style={styles.heading}>TouchableWithoutFeedback ref</Text> <TouchableWithoutFeedback ref={ref} onPress={() => showRef(ref)}> <View style={styles.btn}> <Text>Tap to check ref</Text> </View> </TouchableWithoutFeedback> </View> ); } // ────────────────────────────────────────────── // 7. DrawerLayout ref (imperative open/close) // ────────────────────────────────────────────── function DrawerLayoutRefTest() { const drawerRef = useRef<React.ComponentRef<typeof ReanimatedDrawerLayout>>(null); return ( <View style={styles.section}> <Text style={styles.heading}>DrawerLayout ref</Text> <RectButton style={styles.btn} onPress={() => showRef(drawerRef)}> <Text>Check ref</Text> </RectButton> <RectButton style={styles.btn} onPress={() => drawerRef.current?.openDrawer()}> <Text>Open drawer</Text> </RectButton> <ReanimatedDrawerLayout ref={drawerRef} drawerWidth={200} drawerPosition={DrawerPosition.LEFT} renderNavigationView={() => ( <View style={styles.drawer}> <Text style={styles.heading}>Drawer content</Text> <RectButton style={styles.btn} onPress={() => drawerRef.current?.closeDrawer()}> <Text>Close drawer</Text> </RectButton> </View> )}> <View style={styles.drawerContent}> <Text>Main content - swipe right to open</Text> </View> </ReanimatedDrawerLayout> </View> ); } // ────────────────────────────────────────────── // 8. Callback ref test // ────────────────────────────────────────────── function CallbackRefTest() { const [status, setStatus] = useState('waiting...'); return ( <View style={styles.section}> <Text style={styles.heading}>Callback ref</Text> <Text>ScrollView callback ref: {status}</Text> <ScrollView ref={(node) => { setStatus(node ? 'OK' : 'NULL'); }} style={styles.scrollBox}> {DATA.slice(0, 5).map((d) => ( <Text key={d.key} style={styles.item}> {d.title} </Text> ))} </ScrollView> </View> ); } // ────────────────────────────────────────────── // Main app // ────────────────────────────────────────────── export default function App() { return ( <GestureHandlerRootView style={styles.root}> <ScrollView style={styles.container} contentContainerStyle={styles.content}> <Text style={styles.title}>Ref Reproduction Tests</Text> <Text style={styles.subtitle}> Tap &quot;Check ref&quot; buttons - expect OK, not NULL.{'\n'} Test scrollTo / scrollToIndex to verify imperative methods. </Text> <LegacyScrollViewRefTest /> <V3ScrollViewRefTest /> <LegacyFlatListRefTest /> <V3FlatListRefTest /> <LegacyButtonRefsTest /> <TouchableRefTest /> <DrawerLayoutRefTest /> <CallbackRefTest /> </ScrollView> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: '#f5f5f5' }, container: { flex: 1 }, content: { padding: 16, paddingBottom: 80 }, title: { fontSize: 22, fontWeight: 'bold', color: '#333', marginBottom: 4 }, subtitle: { fontSize: 13, color: '#666', marginBottom: 16 }, section: { marginBottom: 24, backgroundColor: '#fff', borderRadius: 12, padding: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 3, elevation: 2, }, heading: { fontSize: 16, fontWeight: '600', color: '#333', marginBottom: 8 }, btn: { backgroundColor: '#e8e8ff', padding: 10, borderRadius: 8, marginBottom: 8, alignItems: 'center' as const, }, scrollBox: { height: 120, backgroundColor: '#fafafa', borderRadius: 8 }, item: { padding: 10, borderBottomWidth: 1, borderBottomColor: '#eee' }, drawer: { flex: 1, backgroundColor: '#dde', padding: 16 }, drawerContent: { flex: 1, justifyContent: 'center' as const, alignItems: 'center' as const, minHeight: 150, }, }); ``` </details>
1 parent 59983a5 commit 2e98457

15 files changed

Lines changed: 507 additions & 527 deletions

File tree

apps/basic-example/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
*/
44

55
import { AppRegistry } from 'react-native';
6-
import App from './src/App';
6+
77
import { name as appName } from './app.json';
8+
import App from './src/App';
89

910
AppRegistry.registerComponent(appName, () => App);

apps/common-app/App.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
1-
import { useEffect, useState } from 'react';
2-
import { Text, View, StyleSheet, Platform, Dimensions } from 'react-native';
3-
import {
4-
createStackNavigator,
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
2+
import type { ParamListBase } from '@react-navigation/native';
3+
import { NavigationContainer } from '@react-navigation/native';
4+
import type {
55
StackNavigationProp,
66
StackScreenProps,
77
} from '@react-navigation/stack';
8-
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
8+
import { createStackNavigator } from '@react-navigation/stack';
9+
import { Icon } from '@swmansion/icons';
10+
import { useEffect, useState } from 'react';
11+
import { Dimensions, Platform, StyleSheet, Text, View } from 'react-native';
912
import {
1013
GestureHandlerRootView,
1114
RectButton,
1215
Switch,
1316
} from 'react-native-gesture-handler';
1417
import { useSafeAreaInsets } from 'react-native-safe-area-context';
15-
import AsyncStorage from '@react-native-async-storage/async-storage';
1618

1719
import { COLORS } from './src/common';
18-
19-
import { Icon } from '@swmansion/icons';
20-
2120
import { OLD_EXAMPLES } from './src/legacy';
22-
import { NEW_EXAMPLES } from './src/new_api';
2321
import { TouchableExample } from './src/legacy/release_tests/touchables';
2422
import { ListWithHeader } from './src/ListWithHeader';
23+
import { NEW_EXAMPLES } from './src/new_api';
24+
2525
const OPEN_LAST_EXAMPLE_KEY = 'openLastExample';
2626
const SHOW_LEGACY_EXAMPLES_KEY = 'showLegacyExamples';
2727
const LAST_EXAMPLE_KEY = 'lastExample';

apps/expo-example/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import '@expo/metro-runtime';
2-
import App from './App';
2+
33
import { registerRootComponent } from 'expo';
44

5+
import App from './App';
6+
57
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
68
// It also ensures that whether you load the app in Expo Go or in a native build,
79
// the environment is set up appropriately

apps/macos-example/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import App from 'common-app';
66
import { AppRegistry } from 'react-native';
7+
78
import { name as appName } from './app.json';
89

910
AppRegistry.registerComponent(appName, () => App);

packages/react-native-gesture-handler/src/components/GestureButtons.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,14 @@ export const LegacyBaseButton = ({
147147
ref,
148148
...props
149149
}: Omit<LegacyBaseButtonProps, 'innerRef'> & {
150-
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;
150+
ref?: React.Ref<React.ComponentType<any>> | undefined;
151151
}) => <InnerBaseButton innerRef={ref} {...props} />;
152152

153153
const AnimatedBaseButton = ({
154154
ref,
155155
...props
156156
}: Animated.AnimatedProps<BaseButtonWithRefProps> & {
157-
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;
157+
ref?: React.Ref<React.ComponentType<any>> | undefined;
158158
}) => <AnimatedInnerBaseButton innerRef={ref} {...props} />;
159159

160160
const btnStyles = StyleSheet.create({
@@ -228,7 +228,7 @@ export const LegacyRectButton = ({
228228
ref,
229229
...props
230230
}: Omit<LegacyRectButtonProps, 'innerRef'> & {
231-
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;
231+
ref?: React.Ref<React.ComponentType<any>> | undefined;
232232
}) => <InnerRectButton innerRef={ref} {...props} />;
233233

234234
class InnerBorderlessButton extends React.Component<BorderlessButtonWithRefProps> {
@@ -276,7 +276,7 @@ export const LegacyBorderlessButton = ({
276276
ref,
277277
...props
278278
}: Omit<LegacyBorderlessButtonProps, 'innerRef'> & {
279-
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;
279+
ref?: React.Ref<React.ComponentType<any>> | undefined;
280280
}) => <InnerBorderlessButton innerRef={ref} {...props} />;
281281

282282
export { default as LegacyPureNativeButton } from './GestureHandlerButton';

packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export interface LegacyRawButtonProps
9595
testOnly_onLongPress?: Function | null | undefined;
9696
}
9797
interface ButtonWithRefProps {
98-
innerRef?: React.ForwardedRef<React.ComponentType<any>> | undefined;
98+
innerRef?: React.Ref<React.ComponentType<any>> | undefined;
9999
}
100100

101101
/**

packages/react-native-gesture-handler/src/components/GestureComponents.tsx

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import type {
2-
ForwardedRef,
3-
PropsWithChildren,
4-
ReactElement,
5-
RefAttributes,
6-
} from 'react';
1+
import type { PropsWithChildren, ReactElement } from 'react';
72
import * as React from 'react';
83
import type {
94
DrawerLayoutAndroidProps as RNDrawerLayoutAndroidProps,
@@ -49,18 +44,18 @@ const GHScrollView = createNativeWrapper<PropsWithChildren<RNScrollViewProps>>(
4944
/**
5045
* @deprecated use `ScrollView` instead
5146
*/
52-
export const LegacyScrollView = React.forwardRef<
53-
RNScrollView,
54-
RNScrollViewProps & NativeViewGestureHandlerProps
55-
>((props, ref) => {
47+
export const LegacyScrollView = (
48+
props: RNScrollViewProps &
49+
NativeViewGestureHandlerProps & {
50+
ref?: React.Ref<RNScrollView | null>;
51+
}
52+
) => {
5653
const refreshControlGestureRef = React.useRef<LegacyRefreshControl>(null);
5754
const { refreshControl, waitFor, ...rest } = props;
5855

5956
return (
6057
<GHScrollView
6158
{...rest}
62-
// @ts-ignore `ref` exists on `GHScrollView`
63-
ref={ref}
6459
waitFor={[...toArray(waitFor ?? []), refreshControlGestureRef]}
6560
// @ts-ignore we don't pass `refreshing` prop as we only want to override the ref
6661
refreshControl={
@@ -73,7 +68,7 @@ export const LegacyScrollView = React.forwardRef<
7368
}
7469
/>
7570
);
76-
});
71+
};
7772

7873
// Backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457
7974
// include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them.
@@ -112,7 +107,14 @@ export type LegacyDrawerLayoutAndroid = typeof LegacyDrawerLayoutAndroid &
112107
/**
113108
* @deprecated use `FlatList` instead
114109
*/
115-
export const LegacyFlatList = React.forwardRef((props, ref) => {
110+
export const LegacyFlatList = <ItemT,>(
111+
props: PropsWithChildren<
112+
Omit<RNFlatListProps<ItemT>, 'renderScrollComponent'> &
113+
NativeViewGestureHandlerProps & {
114+
ref?: React.Ref<RNFlatList<ItemT> | null>;
115+
}
116+
>
117+
): ReactElement | null => {
116118
const refreshControlGestureRef = React.useRef<LegacyRefreshControl>(null);
117119

118120
const { waitFor, refreshControl, ...rest } = props;
@@ -135,7 +137,6 @@ export const LegacyFlatList = React.forwardRef((props, ref) => {
135137
return (
136138
// @ts-ignore - this function cannot have generic type so we have to ignore this error
137139
<RNFlatList
138-
ref={ref}
139140
{...flatListProps}
140141
renderScrollComponent={(scrollProps) => (
141142
<LegacyScrollView
@@ -157,14 +158,8 @@ export const LegacyFlatList = React.forwardRef((props, ref) => {
157158
}
158159
/>
159160
);
160-
}) as <ItemT = any>(
161-
props: PropsWithChildren<
162-
Omit<RNFlatListProps<ItemT>, 'renderScrollComponent'> &
163-
RefAttributes<LegacyFlatList<ItemT>> &
164-
NativeViewGestureHandlerProps
165-
>,
166-
ref?: ForwardedRef<LegacyFlatList<ItemT>>
167-
) => ReactElement | null;
161+
};
162+
168163
// eslint-disable-next-line @typescript-eslint/no-redeclare
169164
export type LegacyFlatList<ItemT = any> = typeof LegacyFlatList &
170165
RNFlatList<ItemT>;

packages/react-native-gesture-handler/src/components/GestureComponents.web.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,11 @@ export const LegacyRefreshControl = createNativeWrapper(View);
5050
/**
5151
* @deprecated use `FlatList` instead
5252
*/
53-
export const LegacyFlatList = React.forwardRef(
54-
<ItemT,>(props: FlatListProps<ItemT>, ref: any) => (
55-
<RNFlatList
56-
ref={ref}
57-
{...props}
58-
renderScrollComponent={(scrollProps) => (
59-
<LegacyScrollView {...scrollProps} />
60-
)}
61-
/>
62-
)
53+
export const LegacyFlatList = <ItemT,>(props: FlatListProps<ItemT>) => (
54+
<RNFlatList
55+
{...props}
56+
renderScrollComponent={(scrollProps) => (
57+
<LegacyScrollView {...scrollProps} />
58+
)}
59+
/>
6360
);

packages/react-native-gesture-handler/src/components/GestureHandlerRootView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export default function GestureHandlerRootView({
1313
...rest
1414
}: GestureHandlerRootViewProps) {
1515
return (
16-
<GestureHandlerRootViewContext.Provider value>
16+
<GestureHandlerRootViewContext value>
1717
<View style={style ?? styles.container} {...rest} />
18-
</GestureHandlerRootViewContext.Provider>
18+
</GestureHandlerRootViewContext>
1919
);
2020
}
2121

packages/react-native-gesture-handler/src/components/GestureHandlerRootView.web.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export default function GestureHandlerRootView({
1313
...rest
1414
}: GestureHandlerRootViewProps) {
1515
return (
16-
<GestureHandlerRootViewContext.Provider value>
16+
<GestureHandlerRootViewContext value>
1717
<View style={style ?? styles.container} {...rest} />
18-
</GestureHandlerRootViewContext.Provider>
18+
</GestureHandlerRootViewContext>
1919
);
2020
}
2121

0 commit comments

Comments
 (0)