Skip to content

Commit 6bdc844

Browse files
committed
feat: 🎸 add expo router lessons
1 parent 77a6899 commit 6bdc844

3 files changed

Lines changed: 262 additions & 190 deletions

File tree

challenges/expo-router/03.md

Lines changed: 3 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -237,193 +237,6 @@ export default function PlusScreen() {
237237

238238
## 👽 Bonus
239239

240-
### Add the Bottom Tab Navigator
241-
242-
- [ ] Create `app/(app)/(tabs)/_layout.tsx` for the Spacecraft tabs:
243-
244-
```javascript
245-
// app/(app)/(tabs)/_layout.tsx
246-
import { Tabs } from "expo-router";
247-
import { useTheme } from "react-native-paper";
248-
import { Ionicons } from "@expo/vector-icons";
249-
250-
export default function TabLayout() {
251-
const theme = useTheme();
252-
253-
return (
254-
<Tabs
255-
screenOptions={{
256-
headerShown: false,
257-
tabBarActiveTintColor: theme.colors.primary,
258-
}}
259-
>
260-
<Tabs.Screen
261-
name="starships"
262-
options={{
263-
title: "Starships",
264-
tabBarIcon: ({ color }) => (
265-
<Ionicons name="rocket" size={24} color={color} />
266-
),
267-
}}
268-
/>
269-
<Tabs.Screen
270-
name="pilots"
271-
options={{
272-
title: "Pilots",
273-
tabBarIcon: ({ color }) => (
274-
<Ionicons name="people" size={24} color={color} />
275-
),
276-
}}
277-
/>
278-
<Tabs.Screen
279-
name="planets"
280-
options={{
281-
title: "Planets",
282-
tabBarIcon: ({ color }) => (
283-
<Ionicons name="planet" size={24} color={color} />
284-
),
285-
}}
286-
/>
287-
<Tabs.Screen
288-
name="plus"
289-
options={{
290-
title: "Plus",
291-
tabBarIcon: ({ color }) => (
292-
<Ionicons name="add-circle" size={24} color={color} />
293-
),
294-
}}
295-
/>
296-
</Tabs>
297-
);
298-
}
299-
```
300-
301-
### Nested stack for each tab
302-
303-
- [ ] Create a nested layout for the starships tab with details modal:
304-
305-
```javascript
306-
// app/(app)/(tabs)/starships/_layout.tsx
307-
import { Stack } from "expo-router";
308-
309-
export default function StarshipsLayout() {
310-
return (
311-
<Stack>
312-
<Stack.Screen
313-
name="index"
314-
options={{ title: "Starships" }}
315-
/>
316-
<Stack.Screen
317-
name="[id]"
318-
options={{
319-
presentation: "modal",
320-
title: "Starship Details",
321-
}}
322-
/>
323-
</Stack>
324-
);
325-
}
326-
```
327-
328-
### Persist authentication with expo-secure-store
329-
330-
- [ ] Install `expo-secure-store` and persist the user token:
331-
332-
```bash
333-
npx expo install expo-secure-store
334-
```
335-
336-
```javascript
337-
// src/context/AuthContext.tsx
338-
import * as SecureStore from "expo-secure-store";
339-
import { useEffect } from "react";
340-
341-
export function AuthProvider({ children }: { children: ReactNode }) {
342-
const [user, setUser] = useState<boolean | null>(null); // null = loading
343-
344-
const signIn = async () => {
345-
await SecureStore.setItemAsync("userToken", "authenticated");
346-
setUser(true);
347-
};
348-
349-
const signOut = async () => {
350-
await SecureStore.deleteItemAsync("userToken");
351-
setUser(false);
352-
};
353-
354-
// Check for existing token on app load
355-
useEffect(() => {
356-
const checkToken = async () => {
357-
const token = await SecureStore.getItemAsync("userToken");
358-
setUser(!!token);
359-
};
360-
checkToken();
361-
}, []);
362-
363-
// Don't render until we know the auth state
364-
if (user === null) {
365-
return null; // Or a loading spinner
366-
}
367-
368-
return (
369-
<AuthContext.Provider value={{ user, signIn, signOut }}>
370-
{children}
371-
</AuthContext.Provider>
372-
);
373-
}
374-
```
375-
376-
### Add a splash screen during auth check
377-
378-
- [ ] Show a splash screen while checking authentication:
379-
380-
```javascript
381-
// app/_layout.tsx
382-
import { Slot, SplashScreen } from "expo-router";
383-
import { useEffect, useState } from "react";
384-
import * as SecureStore from "expo-secure-store";
385-
386-
// Prevent the splash screen from auto-hiding
387-
SplashScreen.preventAutoHideAsync();
388-
389-
export default function RootLayout() {
390-
const [isReady, setIsReady] = useState(false);
391-
392-
useEffect(() => {
393-
async function prepare() {
394-
try {
395-
// Check auth state, load fonts, fetch initial data...
396-
await SecureStore.getItemAsync("userToken");
397-
} finally {
398-
setIsReady(true);
399-
SplashScreen.hideAsync();
400-
}
401-
}
402-
prepare();
403-
}, []);
404-
405-
if (!isReady) {
406-
return null;
407-
}
408-
409-
return (
410-
<QueryClientProvider client={queryClient}>
411-
<PaperProvider theme={theme}>
412-
<AuthProvider>
413-
<Slot />
414-
</AuthProvider>
415-
</PaperProvider>
416-
</QueryClientProvider>
417-
);
418-
}
419-
```
420-
421-
### Test the protected routes
422-
423-
- [ ] Verify these scenarios work correctly:
424-
425-
1. **Unauthenticated user** visits `/starships` → redirected to `/`
426-
2. **Unauthenticated user** visits `/pilots` → redirected to `/`
427-
3. **Authenticated user** visits `/` → redirected to `/starships`
428-
4. **Authenticated user** signs out → redirected to `/`
429-
5. **Refresh the app** → stays logged in (with persist)
240+
- [ ] Add a "Remember me" checkbox on the login screen that stores the preference.
241+
- [ ] Create a `/forgot-password` screen in the `(auth)` group with a simple form.
242+
- [ ] Add a loading spinner on the login button while `signIn` is in progress.

challenges/expo-router/04.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Tab Navigation
2+
3+
## 📡 What you will learn
4+
5+
- Build a Bottom Tab Navigator with Expo Router.
6+
- Create nested stacks within tabs.
7+
- Present screens as modals.
8+
9+
## 👾 Before we start the exercise
10+
11+
- Make sure you have completed [Exercise 3](./03.md) with the authentication flow.
12+
- Check the [Tabs documentation](https://docs.expo.dev/router/layouts/#tabs).
13+
14+
## 👨‍🚀 Exercise 4
15+
16+
### Add the Bottom Tab Navigator
17+
18+
- [ ] Create `app/(app)/(tabs)/_layout.tsx` for the Spacecraft tabs:
19+
20+
```javascript
21+
// app/(app)/(tabs)/_layout.tsx
22+
import { Tabs } from "expo-router";
23+
import { useTheme } from "react-native-paper";
24+
import { Ionicons } from "@expo/vector-icons";
25+
26+
export default function TabLayout() {
27+
const theme = useTheme();
28+
29+
return (
30+
<Tabs
31+
screenOptions={{
32+
headerShown: false,
33+
tabBarActiveTintColor: theme.colors.primary,
34+
}}
35+
>
36+
<Tabs.Screen
37+
name="starships"
38+
options={{
39+
title: "Starships",
40+
tabBarIcon: ({ color }) => (
41+
<Ionicons name="rocket" size={24} color={color} />
42+
),
43+
}}
44+
/>
45+
<Tabs.Screen
46+
name="pilots"
47+
options={{
48+
title: "Pilots",
49+
tabBarIcon: ({ color }) => (
50+
<Ionicons name="people" size={24} color={color} />
51+
),
52+
}}
53+
/>
54+
<Tabs.Screen
55+
name="planets"
56+
options={{
57+
title: "Planets",
58+
tabBarIcon: ({ color }) => (
59+
<Ionicons name="planet" size={24} color={color} />
60+
),
61+
}}
62+
/>
63+
<Tabs.Screen
64+
name="plus"
65+
options={{
66+
title: "Plus",
67+
tabBarIcon: ({ color }) => (
68+
<Ionicons name="add-circle" size={24} color={color} />
69+
),
70+
}}
71+
/>
72+
</Tabs>
73+
);
74+
}
75+
```
76+
77+
### Nested stack for each tab
78+
79+
Each tab can have its own navigation stack, allowing you to navigate to detail screens while keeping the tab bar visible.
80+
81+
- [ ] Create a nested layout for the starships tab with details modal:
82+
83+
```javascript
84+
// app/(app)/(tabs)/starships/_layout.tsx
85+
import { Stack } from "expo-router";
86+
87+
export default function StarshipsLayout() {
88+
return (
89+
<Stack>
90+
<Stack.Screen
91+
name="index"
92+
options={{ title: "Starships" }}
93+
/>
94+
<Stack.Screen
95+
name="[id]"
96+
options={{
97+
presentation: "modal",
98+
title: "Starship Details",
99+
}}
100+
/>
101+
</Stack>
102+
);
103+
}
104+
```
105+
106+
- [ ] Create similar layouts for `pilots`, `planets`, and `plus` tabs.
107+
108+
## 👽 Bonus
109+
110+
- [ ] Add a badge on the "Plus" tab icon showing a notification count.
111+
- [ ] Customize the tab bar style with a custom background color and shadow.
112+
- [ ] Hide the tab bar on detail screens using `tabBarStyle: { display: 'none' }` in screen options.

0 commit comments

Comments
 (0)