@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 +422 -0
- package/package.json +44 -0
- package/src/__tests__/navigation-reducer.test.tsx +348 -0
- package/src/contexts.tsx +8 -0
- package/src/index.ts +3 -0
- package/src/navigation-reducer.ts +467 -0
- package/src/navigation-store.ts +55 -0
- package/src/navigation.tsx +141 -0
- package/src/stack.tsx +255 -0
- package/src/tabs.tsx +297 -0
- package/src/types.ts +48 -0
- package/src/utils.ts +14 -0
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
|
+
}
|