Skip to content

Commit 2573b5b

Browse files
committed
feat: add Link component as useLinkTo hook for navigating to links
The `Link` component can be used to navigate to URLs. On web, it'll use an `a` tag for proper accessibility. On React Native, it'll use a `Text`. Example: ```js <Link to="/feed/hot">Go to 🔥</Link> ``` Sometimes we might want more complex styling and more control over the behaviour, or navigate to a URL programmatically. The `useLinkTo` hook can be used for that. Example: ```js function LinkButton({ to, ...rest }) { const linkTo = useLinkTo(); return ( <Button {...rest} href={to} onPress={(e) => { e.preventDefault(); linkTo(to); }} /> ); } ```
1 parent 2697355 commit 2573b5b

12 files changed

+444
-61
lines changed

example/src/Screens/LinkComponent.tsx

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as React from 'react';
2+
import { View, StyleSheet, ScrollView } from 'react-native';
3+
import { Button } from 'react-native-paper';
4+
import {
5+
Link,
6+
RouteProp,
7+
ParamListBase,
8+
useLinkTo,
9+
} from '@react-navigation/native';
10+
import {
11+
createStackNavigator,
12+
StackNavigationProp,
13+
} from '@react-navigation/stack';
14+
import Article from '../Shared/Article';
15+
import Albums from '../Shared/Albums';
16+
17+
type SimpleStackParams = {
18+
Article: { author: string };
19+
Album: undefined;
20+
};
21+
22+
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
23+
24+
const LinkButton = ({
25+
to,
26+
...rest
27+
}: React.ComponentProps<typeof Button> & { to: string }) => {
28+
const linkTo = useLinkTo();
29+
30+
return <Button onPress={() => linkTo(to)} {...rest} />;
31+
};
32+
33+
const ArticleScreen = ({
34+
navigation,
35+
route,
36+
}: {
37+
navigation: SimpleStackNavigation;
38+
route: RouteProp<SimpleStackParams, 'Article'>;
39+
}) => {
40+
return (
41+
<ScrollView>
42+
<View style={styles.buttons}>
43+
<Link
44+
to="/link-component/Album"
45+
style={[styles.button, { padding: 8 }]}
46+
>
47+
Go to /link-component/Album
48+
</Link>
49+
<LinkButton
50+
to="/link-component/Album"
51+
mode="contained"
52+
style={styles.button}
53+
>
54+
Go to /link-component/Album
55+
</LinkButton>
56+
<Button
57+
mode="outlined"
58+
onPress={() => navigation.goBack()}
59+
style={styles.button}
60+
>
61+
Go back
62+
</Button>
63+
</View>
64+
<Article author={{ name: route.params.author }} scrollEnabled={false} />
65+
</ScrollView>
66+
);
67+
};
68+
69+
const AlbumsScreen = ({
70+
navigation,
71+
}: {
72+
navigation: SimpleStackNavigation;
73+
}) => {
74+
return (
75+
<ScrollView>
76+
<View style={styles.buttons}>
77+
<Link
78+
to="/link-component/Article?author=Babel"
79+
style={[styles.button, { padding: 8 }]}
80+
>
81+
Go to /link-component/Article
82+
</Link>
83+
<LinkButton
84+
to="/link-component/Article?author=Babel"
85+
mode="contained"
86+
style={styles.button}
87+
>
88+
Go to /link-component/Article
89+
</LinkButton>
90+
<Button
91+
mode="outlined"
92+
onPress={() => navigation.goBack()}
93+
style={styles.button}
94+
>
95+
Go back
96+
</Button>
97+
</View>
98+
<Albums scrollEnabled={false} />
99+
</ScrollView>
100+
);
101+
};
102+
103+
const SimpleStack = createStackNavigator<SimpleStackParams>();
104+
105+
type Props = Partial<React.ComponentProps<typeof SimpleStack.Navigator>> & {
106+
navigation: StackNavigationProp<ParamListBase>;
107+
};
108+
109+
export default function SimpleStackScreen({ navigation, ...rest }: Props) {
110+
navigation.setOptions({
111+
headerShown: false,
112+
});
113+
114+
return (
115+
<SimpleStack.Navigator {...rest}>
116+
<SimpleStack.Screen
117+
name="Article"
118+
component={ArticleScreen}
119+
options={({ route }) => ({
120+
title: `Article by ${route.params.author}`,
121+
})}
122+
initialParams={{ author: 'Gandalf' }}
123+
/>
124+
<SimpleStack.Screen
125+
name="Album"
126+
component={AlbumsScreen}
127+
options={{ title: 'Album' }}
128+
/>
129+
</SimpleStack.Navigator>
130+
);
131+
}
132+
133+
const styles = StyleSheet.create({
134+
buttons: {
135+
padding: 8,
136+
},
137+
button: {
138+
margin: 8,
139+
},
140+
});

example/src/index.tsx

+37-31
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ import {
2222
Appbar,
2323
List,
2424
Divider,
25+
Text,
2526
} from 'react-native-paper';
2627
import {
2728
InitialState,
28-
useLinking,
2929
NavigationContainerRef,
3030
NavigationContainer,
3131
DefaultTheme,
@@ -55,6 +55,7 @@ import DynamicTabs from './Screens/DynamicTabs';
5555
import AuthFlow from './Screens/AuthFlow';
5656
import CompatAPI from './Screens/CompatAPI';
5757
import MasterDetail from './Screens/MasterDetail';
58+
import LinkComponent from './Screens/LinkComponent';
5859

5960
YellowBox.ignoreWarnings(['Require cycle:', 'Warning: Async Storage']);
6061

@@ -113,6 +114,10 @@ const SCREENS = {
113114
title: 'Compat Layer',
114115
component: CompatAPI,
115116
},
117+
LinkComponent: {
118+
title: '<Link />',
119+
component: LinkComponent,
120+
},
116121
};
117122

118123
const Drawer = createDrawerNavigator<RootDrawerParamList>();
@@ -126,34 +131,6 @@ Asset.loadAsync(StackAssets);
126131
export default function App() {
127132
const containerRef = React.useRef<NavigationContainerRef>(null);
128133

129-
// To test deep linking on, run the following in the Terminal:
130-
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
131-
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
132-
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
133-
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
134-
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
135-
const { getInitialState } = useLinking(containerRef, {
136-
prefixes: LinkingPrefixes,
137-
config: {
138-
Root: {
139-
path: '',
140-
initialRouteName: 'Home',
141-
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
142-
(acc, name) => {
143-
// Convert screen names such as SimpleStack to kebab case (simple-stack)
144-
acc[name] = name
145-
.replace(/([A-Z]+)/g, '-$1')
146-
.replace(/^-/, '')
147-
.toLowerCase();
148-
149-
return acc;
150-
},
151-
{ Home: '' }
152-
),
153-
},
154-
},
155-
});
156-
157134
const [theme, setTheme] = React.useState(DefaultTheme);
158135

159136
const [isReady, setIsReady] = React.useState(false);
@@ -164,12 +141,13 @@ export default function App() {
164141
React.useEffect(() => {
165142
const restoreState = async () => {
166143
try {
167-
let state = await getInitialState();
144+
let state;
168145

169146
if (Platform.OS !== 'web' && state === undefined) {
170147
const savedState = await AsyncStorage.getItem(
171148
NAVIGATION_PERSISTENCE_KEY
172149
);
150+
173151
state = savedState ? JSON.parse(savedState) : undefined;
174152
}
175153

@@ -190,7 +168,7 @@ export default function App() {
190168
};
191169

192170
restoreState();
193-
}, [getInitialState]);
171+
}, []);
194172

195173
const paperTheme = React.useMemo(() => {
196174
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;
@@ -239,6 +217,34 @@ export default function App() {
239217
)
240218
}
241219
theme={theme}
220+
linking={{
221+
// To test deep linking on, run the following in the Terminal:
222+
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
223+
// iOS: xcrun simctl openurl booted exp://127.0.0.1:19000/--/simple-stack
224+
// Android (bare): adb shell am start -a android.intent.action.VIEW -d "rne://127.0.0.1:19000/--/simple-stack"
225+
// iOS (bare): xcrun simctl openurl booted rne://127.0.0.1:19000/--/simple-stack
226+
// The first segment of the link is the the scheme + host (returned by `Linking.makeUrl`)
227+
prefixes: LinkingPrefixes,
228+
config: {
229+
Root: {
230+
path: '',
231+
initialRouteName: 'Home',
232+
screens: Object.keys(SCREENS).reduce<{ [key: string]: string }>(
233+
(acc, name) => {
234+
// Convert screen names such as SimpleStack to kebab case (simple-stack)
235+
acc[name] = name
236+
.replace(/([A-Z]+)/g, '-$1')
237+
.replace(/^-/, '')
238+
.toLowerCase();
239+
240+
return acc;
241+
},
242+
{ Home: '' }
243+
),
244+
},
245+
},
246+
}}
247+
fallback={<Text>Loading…</Text>}
242248
>
243249
<Drawer.Navigator drawerType={isLargeScreen ? 'permanent' : undefined}>
244250
<Drawer.Screen

packages/native/src/Link.tsx

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react';
2+
import { Text, TextProps, GestureResponderEvent, Platform } from 'react-native';
3+
import useLinkTo from './useLinkTo';
4+
5+
type Props = {
6+
to: string;
7+
target?: string;
8+
} & (TextProps & { children: React.ReactNode });
9+
10+
export default function Link({ to, children, ...rest }: Props) {
11+
const linkTo = useLinkTo();
12+
13+
const onPress = (e: GestureResponderEvent | undefined) => {
14+
if ('onPress' in rest) {
15+
rest.onPress?.(e as GestureResponderEvent);
16+
}
17+
18+
const event = (e?.nativeEvent as any) as
19+
| React.MouseEvent<HTMLAnchorElement, MouseEvent>
20+
| undefined;
21+
22+
if (Platform.OS !== 'web' || !event) {
23+
linkTo(to);
24+
return;
25+
}
26+
27+
event.preventDefault();
28+
29+
if (
30+
!event.defaultPrevented && // onPress prevented default
31+
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) && // ignore clicks with modifier keys
32+
(event.button == null || event.button === 0) && // ignore everything but left clicks
33+
(rest.target == null || rest.target === '_self') // let browser handle "target=_blank" etc.
34+
) {
35+
event.preventDefault();
36+
linkTo(to);
37+
}
38+
};
39+
40+
const props = {
41+
href: to,
42+
onPress,
43+
accessibilityRole: 'link' as const,
44+
...rest,
45+
};
46+
47+
return <Text {...props}>{children}</Text>;
48+
}
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as React from 'react';
2+
import { LinkingOptions } from './types';
3+
4+
const LinkingContext = React.createContext<() => LinkingOptions | undefined>(
5+
() => {
6+
throw new Error(
7+
"Couldn't find a linking context. Have you wrapped your app with 'NavigationContainer'?"
8+
);
9+
}
10+
);
11+
12+
export default LinkingContext;

0 commit comments

Comments
 (0)