@rn-tools/navigation 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,422 @@
1
+ # @rn-tools/navigation
2
+
3
+ A set of useful navigation components for React Native. Built with `react-native-screens`. Designed with flexibility in mind.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ yarn expo install @rn-tools/navigation react-native-screens
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ For basic usage, the exported `Stack.Navigator` and `Tabs.Navigator` will get you up and running quickly. The [Guides](#guides) section covers how to use lower-level `Stack` and `Tabs` components in a variety of navigation patterns.
14
+
15
+ ### Stack Navigator
16
+
17
+ The `Stack.Navigator` component manages stacks of screens. Under the hood this is using `react-native-screens` to handle pushing and popping natively.
18
+
19
+ Screens are pushed and popped by the exported navigation methods:
20
+
21
+ - `navigation.pushScreen(screenElement: React.ReactElement<ScreenProps>, options?: PushScreenOptions) => void`
22
+
23
+ - `navigation.popScreen(numberOfScreens: number) => void`
24
+
25
+ ```tsx
26
+ import { Stack, navigation } from "@rn-tools/navigation";
27
+ import * as React from "react";
28
+ import { View, Text, Button } from "react-native";
29
+
30
+ export function BasicStack() {
31
+ return <Stack.Navigator rootScreen={<MyScreen title="Root Screen" />} />;
32
+ }
33
+
34
+ function MyScreen({
35
+ title,
36
+ showPopButton = false,
37
+ }: {
38
+ title: string;
39
+ showPopButton?: boolean;
40
+ }) {
41
+ function pushScreen() {
42
+ navigation.pushScreen(
43
+ <Stack.Screen>
44
+ <MyScreen title="Pushed screen" showPopButton />
45
+ </Stack.Screen>
46
+ );
47
+ }
48
+
49
+ function popScreen() {
50
+ navigation.popScreen();
51
+ }
52
+
53
+ return (
54
+ <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
55
+ <Text>{title}</Text>
56
+ <Button title="Push screen" onPress={pushScreen} />
57
+ {showPopButton && <Button title="Pop screen" onPress={popScreen} />}
58
+ </View>
59
+ );
60
+ }
61
+ ```
62
+
63
+ **Note**: The components passed to `navigation.pushScreen` need to be wrapped in a `Stack.Screen` component. Create a wrapper to simplify your usage if you'd like:
64
+
65
+ ```tsx
66
+ function myPushScreen(
67
+ element: React.ReactElement<unknown>,
68
+ options?: PushScreenOptions
69
+ ) {
70
+ navigation.pushScreen(<Stack.Screen>{element}</Stack.Screen>, options);
71
+ }
72
+ ```
73
+
74
+ ### Tab Navigator
75
+
76
+ The `Tabs.Navigator` component also uses `react-native-screens` to handle the tab switching natively. The active tab can be changed via the `navigation.setTabIndex` method, however the build in tabbar already handles switching between screens.
77
+
78
+ ```tsx
79
+ import {
80
+ Stack,
81
+ Tabs,
82
+ navigation,
83
+ Stack,
84
+ defaultTabbarStyle,
85
+ } from "@rn-tools/navigation";
86
+ import * as React from "react";
87
+ import { View, Text, Button } from "react-native";
88
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
89
+
90
+ export function BasicTabs() {
91
+ return <Stack.Navigator rootScreen={<MyTabs />} />;
92
+ }
93
+
94
+ function MyTabs() {
95
+ // This hook requires you to wrap your app in a SafeAreaProvider component - see the `react-native-safe-area-context` package
96
+ let insets = useSafeAreaInsets();
97
+
98
+ let tabbarStyle = React.useMemo(() => {
99
+ return {
100
+ ...defaultTabbarStyle,
101
+ bottom: insets.bottom,
102
+ };
103
+ }, [insets.bottom]);
104
+
105
+ return (
106
+ <Tabs.Navigator
107
+ tabbarPosition="bottom"
108
+ tabbarStyle={tabbarStyle}
109
+ screens={[
110
+ {
111
+ key: "1",
112
+ screen: <MyScreen title="Screen 1" bg="red" />,
113
+ tab: ({ isActive }) => <MyTab isActive={isActive}>1</MyTab>,
114
+ },
115
+ {
116
+ key: "2",
117
+ screen: <MyScreen title="Screen 2" bg="blue" />,
118
+ tab: ({ isActive }) => <MyTab isActive={isActive}>2</MyTab>,
119
+ },
120
+ {
121
+ key: "3",
122
+ screen: <MyScreen title="Screen 3" bg="purple" />,
123
+ tab: ({ isActive }) => <MyTab isActive={isActive}>3</MyTab>,
124
+ },
125
+ ]}
126
+ />
127
+ );
128
+ }
129
+
130
+ function MyTab({
131
+ children,
132
+ isActive,
133
+ }: {
134
+ children: React.ReactNode;
135
+ isActive: boolean;
136
+ }) {
137
+ return (
138
+ <View style={{ padding: 16, alignItems: "center" }}>
139
+ <Text
140
+ style={isActive ? { fontWeight: "bold" } : { fontWeight: "normal" }}
141
+ >
142
+ {children}
143
+ </Text>
144
+ </View>
145
+ );
146
+ }
147
+
148
+ function MyScreen({
149
+ title,
150
+ showPopButton = false,
151
+ bg,
152
+ }: {
153
+ title: string;
154
+ showPopButton?: boolean;
155
+ bg?: string;
156
+ }) {
157
+ function pushScreen() {
158
+ navigation.pushScreen(
159
+ <Stack.Screen>
160
+ <MyScreen title="Pushed screen" showPopButton />
161
+ </Stack.Screen>
162
+ );
163
+ }
164
+
165
+ function popScreen() {
166
+ navigation.popScreen();
167
+ }
168
+
169
+ return (
170
+ <View
171
+ style={{
172
+ flex: 1,
173
+ justifyContent: "center",
174
+ alignItems: "center",
175
+ backgroundColor: bg || "white",
176
+ }}
177
+ >
178
+ <Text>{title}</Text>
179
+ <Button title="Push screen" onPress={pushScreen} />
180
+ {showPopButton && <Button title="Pop screen" onPress={popScreen} />}
181
+ </View>
182
+ );
183
+ }
184
+ ```
185
+
186
+ ### Rendering a stack inside of a tabbed screen
187
+
188
+ Each tab can have its own stack by nesting the `Stack.Navigator` component.
189
+
190
+ - `navigation.pushScreen` will still work relative by pushing to the relative parent stack of the screen. See the next section for how to push a screen onto a specific stack.
191
+
192
+ ```tsx
193
+ function MyTabs() {
194
+ return (
195
+ <Tabs.Navigator
196
+ screens={[
197
+ {
198
+ key: "1",
199
+ // Wrap the screen in a Stack.Navigator:
200
+ screen: (
201
+ <Stack.Navigator
202
+ rootScreen={<MyScreen title="Screen 1" bg="red" />}
203
+ />
204
+ ),
205
+ tab: ({ isActive }) => <MyTab isActive={isActive}>1</MyTab>,
206
+ },
207
+ // ...other screens
208
+ ]}
209
+ />
210
+ );
211
+ }
212
+ ```
213
+
214
+ ### Targeting a specific stack
215
+
216
+ Provide an `id` prop to a stack and target when pushing the screen.
217
+
218
+ ```tsx
219
+ let MAIN_STACK_ID = "mainStack";
220
+
221
+ function App() {
222
+ return (
223
+ <Stack.Navigator
224
+ id={MAIN_STACK_ID}
225
+ rootScreen={<MyScreen title="Root Screen" />}
226
+ />
227
+ );
228
+ }
229
+
230
+ function pushToMainStack(
231
+ screenElement: React.ReactElement<unknown>,
232
+ options: PushScreenOptions
233
+ ) {
234
+ navigation.pushScreen(<Stack.Screen>{screenElement}</Stack.Screen>, {
235
+ ...options,
236
+ stackId: MAIN_STACK_ID,
237
+ });
238
+ }
239
+ ```
240
+
241
+ ### Pushing a screen once
242
+
243
+ Provide a `screenId` option to only push the screen once. Screen ids are unique across all stacks.
244
+
245
+ ```tsx
246
+ function pushThisScreenOnce() {
247
+ navigation.pushScreen(
248
+ <Stack.Screen>
249
+ <MyScreen title="Pushed screen" />
250
+ </Stack.Screen>,
251
+ {
252
+ // This screen will only be pushed once
253
+ screenId: "unique-key",
254
+ }
255
+ );
256
+ }
257
+ ```
258
+
259
+ ### Targeting specific tabs
260
+
261
+ Similar to `Stack.Navigator`, pass an `id` prop to a `Tabs.Navigator` and target a navigator expliclity when setting the active tab.
262
+
263
+ ```tsx
264
+ let MAIN_TAB_ID = "mainTabs";
265
+
266
+ function App() {
267
+ return <Tabs.Navigator id={MAIN_TAB_ID} screens={tabs} />;
268
+ }
269
+
270
+ function switchMainTabsToTab(tabIndex: number) {
271
+ navigation.setTabIndex(tabIndex, { tabId: MAIN_TAB_ID });
272
+ }
273
+ ```
274
+
275
+ ## Components
276
+
277
+ The `Navigator` components in the previous examples are convenience wrappers around other lower level `Stack` and `Tabs` components. This section will briefly cover each of the underlying components so that you can build your own wrappers if needed
278
+
279
+ ### Stack
280
+
281
+ This is the implementation of the exported `Stack.Navigator` component:
282
+
283
+ ```tsx
284
+ type StackNavigatorProps = Omit<StackRootProps, "children"> & {
285
+ rootScreen: React.ReactElement<unknown>;
286
+ };
287
+
288
+ export function StackNavigator({
289
+ rootScreen,
290
+ ...rootProps
291
+ }: Stack.NavigatorProps) {
292
+ return (
293
+ <Stack.Root {...rootProps}>
294
+ <Stack.Screens>
295
+ <Stack.Screen>{rootScreen}</Stack.Screen>
296
+ <Stack.Slot />
297
+ </Stack.Screens>
298
+ </Stack.Root>
299
+ );
300
+ }
301
+ ```
302
+
303
+ - `Stack.Root` - The root component for a stack navigator.
304
+ - `Stack.Screens` - The container for all screens in a stack.
305
+ - This is a `react-native-screens` StackScreenContainer component under the hood.
306
+ - All UI rendered children should be `Stack.Screen` or `Stack.Slot` components.
307
+ - You can still render contexts and other non-UI components directly under `Stack.Screens`. See the Authentication guide for examples of this
308
+ - `Stack.Screen` - A screen in a stack.
309
+ - This is a `react-native-screens` StackScreen component under the hood.
310
+ - Notable props include `gestureEnabled`, `stackPresentation` and `preventNativeDismiss` to control how the screen can be interacted with.
311
+ - Reference for props that can be passed: [Screen Props](https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md#screen)
312
+ - `Stack.Slot` - A slot for screens to be pushed into.
313
+ - This component is used to render screens that are pushed using `navigation.pushScreen` - don't forget to render this somewhere in `Stack.Screens`!
314
+
315
+ ## Tabs
316
+
317
+ This is the implementation of the exported `Tabs.Navigator` component:
318
+
319
+ ```tsx
320
+ type TabsNavigatorProps = Omit<TabsRootProps, "children"> & {
321
+ screens: Tabs.NavigatorScreenOptions[];
322
+ tabbarPosition?: "top" | "bottom";
323
+ tabbarStyle?: ViewProps["style"];
324
+ };
325
+
326
+ type TabsNavigatorScreenOptions = {
327
+ key: string;
328
+ screen: React.ReactElement<ScreenProps>;
329
+ tab: (props: { isActive: boolean; onPress: () => void }) => React.ReactNode;
330
+ };
331
+
332
+ export function Tabs.Navigator({
333
+ screens,
334
+ tabbarPosition = "bottom",
335
+ tabbarStyle,
336
+ ...rootProps
337
+ }: TabsNavigatorProps) {
338
+ return (
339
+ <Tabs.Root {...rootProps}>
340
+ {tabbarPosition === "top" && (
341
+ <Tabs.Tabbar style={tabbarStyle}>
342
+ {screens.map((screen) => {
343
+ return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
344
+ })}
345
+ </Tabs.Tabbar>
346
+ )}
347
+
348
+ <Tabs.Screens>
349
+ {screens.map((screen) => {
350
+ return <Tabs.Screen key={screen.key}>{screen.screen}</Tabs.Screen>;
351
+ })}
352
+ </Tabs.Screens>
353
+
354
+ {tabbarPosition === "bottom" && (
355
+ <Tabs.Tabbar style={tabbarStyle}>
356
+ {screens.map((screen) => {
357
+ return <Tabs.Tab key={screen.key}>{screen.tab}</Tabs.Tab>;
358
+ })}
359
+ </Tabs.Tabbar>
360
+ )}
361
+ </Tabs.Root>
362
+ );
363
+ }
364
+ ```
365
+
366
+ - `Tabs.Root` - The root component for a tabs navigator.
367
+ - `Tabs.Screens` - The container for all screens in a tabs navigator.
368
+ - This is a `react-native-screens` ScreenContainer component under the hood.
369
+ - All UI rendered children should be `Tabs.Screen` components.
370
+ - `Tabs.Screen` - A screen in a tabs navigator.
371
+ - `Tabs.Tabbar` - The tab bar for a tabs navigator.
372
+ - Each child Tab of the tab bar will target the screen that corresponds to its index
373
+ - `Tabs.Tab` - A tab in a tabs navigator
374
+ - This is a Pressable component that switches the active screen
375
+
376
+ ## Guides
377
+
378
+ ### Authentication
379
+
380
+ For this example, we want to show our main app when the user is logged in, otherwise show the login screen. You can use the `Stack` component to conditionally render screens based on the user's state.
381
+
382
+ ```tsx
383
+ import * as React from "react";
384
+ import { Stack } from "@rn-tools/navigation";
385
+
386
+ function App() {
387
+ let [user, setUser] = React.useState(null);
388
+
389
+ return (
390
+ <Stack.Root>
391
+ <Stack.Screens>
392
+ <Stack.Screen>
393
+ <MyLoginScreen onLoginSuccess={(user) => setUser(user)} />
394
+ </Stack.Screen>
395
+
396
+ {user != null && (
397
+ <UserContext.Provider value={user}>
398
+ <Stack.Screen gestureEnabled={false}>
399
+ <MyAuthenticatedApp />
400
+ </Stack.Screen>
401
+ <Stack.Slot />
402
+ </UserContext.Provider>
403
+ )}
404
+ </Stack.Screens>
405
+ </Stack.Root>
406
+ );
407
+ }
408
+
409
+ let UserContext = React.createContext<User | null>(null);
410
+
411
+ let useUser = () => {
412
+ let user = React.useContext(UserContext);
413
+
414
+ if (user == null) {
415
+ throw new Error("User not found");
416
+ }
417
+
418
+ return user;
419
+ };
420
+ ```
421
+
422
+ **Note:** Screens that are pushed using `pushScreen` are rendered in the `Slot` component
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@rn-tools/navigation",
3
+ "version": "2.0.0",
4
+ "main": "./src/index.ts",
5
+ "license": "MIT",
6
+ "files": [
7
+ "src/*"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "jest .",
12
+ "lint": "eslint -c ./.eslintrc.js ."
13
+ },
14
+ "devDependencies": {
15
+ "@rn-tools/eslint-config": "*",
16
+ "@types/jest": "^27.0.3",
17
+ "@types/react": "~18.2.79",
18
+ "@types/react-native": "^0.72.8",
19
+ "@typescript-eslint/eslint-plugin": "^6.8.0",
20
+ "@typescript-eslint/parser": "^6.8.0",
21
+ "babel-preset-expo": "~11.0.0",
22
+ "eslint": "^8.56.0",
23
+ "expo": "^51.0.8",
24
+ "jest": "^29.7.0",
25
+ "jest-expo": "^51.0.2",
26
+ "lint-staged": "^15.2.0",
27
+ "prettier": "^3.2.1",
28
+ "typescript": "~5.3.3"
29
+ },
30
+ "peerDependencies": {
31
+ "react": "*",
32
+ "react-native": "*",
33
+ "react-native-screens": "*"
34
+ },
35
+ "dependencies": {
36
+ "zustand": "^4.5.2"
37
+ },
38
+ "jest": {
39
+ "preset": "jest-expo",
40
+ "transformIgnorePatterns": [
41
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
42
+ ]
43
+ }
44
+ }