Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This starter includes everything you need to build a production-ready React Nati
- ✅ **TypeScript** - Full type safety throughout
- ✅ **ESLint + Prettier** - Code quality and formatting tools
- ✅ **Example Screens** - See features in action
- ✅ **Redux Toolkit** - State management with TypeScript support (redux branch)

## State Management Options

Expand Down Expand Up @@ -325,6 +326,24 @@ Expo Go is a free app for testing your app on physical devices:

> **Note:** Expo Go is great for learning and prototyping, but not recommended for production apps. Use development builds instead.

## State Management Options

This starter provides multiple state management implementations across different branches:

- **main branch** - React hooks and Context API (built-in)
- **redux branch** - Redux Toolkit for complex state management
- **zustand branch** - Zustand for lightweight global state
- **react-context branch** - Advanced Context API patterns

Choose the branch that fits your project needs:

- **Use main branch** for simple apps with minimal shared state
- **Use redux branch** for complex apps requiring predictable state management, time-travel debugging, and team collaboration
- **Use zustand branch** for lightweight global state without Redux boilerplate
- **Use react-context branch** for apps that need Context API patterns with performance optimizations

**📖 For Redux Toolkit implementation details, see [State Management with Redux Toolkit](docs/state-management-redux.md)**

## Documentation

### Essential Guides
Expand All @@ -339,6 +358,7 @@ Expo Go is a free app for testing your app on physical devices:
- **[UI Library](docs/ui-library.md)** - React Native Paper components
- **[Color Themes](docs/color-themes.md)** - Theming and dark mode
- **[Error and Loading Handling](docs/error-and-loading.md)** - State management
- **[State Management with Redux Toolkit](docs/state-management-redux.md)** - Redux Toolkit guide (redux branch)

### Additional Guides

Expand Down
57 changes: 35 additions & 22 deletions app/(auth)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Expand All @@ -9,38 +9,51 @@ import {
Snackbar,
} from 'react-native-paper';
import { router } from 'expo-router';
import { authService } from '@/services/auth';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { loginAsync, selectAuth, clearError } from '@/store/slices/authSlice';

export default function LoginScreen() {
const theme = useTheme();
const dispatch = useAppDispatch();
const { loading, error, isAuthenticated } = useAppSelector(selectAuth);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [snackbarVisible, setSnackbarVisible] = useState(false);
const dismissedErrorRef = useRef<string | null>(null);

const handleLogin = async () => {
// Navigate to main app when authenticated
useEffect(() => {
if (isAuthenticated) {
router.replace('/(tabs)');
}
}, [isAuthenticated]);

// Show snackbar when error appears (but not if it was already dismissed)
useEffect(() => {
if (error && error !== dismissedErrorRef.current) {
// Schedule state update to avoid synchronous setState in effect
queueMicrotask(() => {
setSnackbarVisible(true);
});
}
}, [error]);

const handleLogin = () => {
if (!email || !password) {
setError('Please fill in all fields');
dispatch(clearError());
setSnackbarVisible(true);
// For validation errors, we'll show a message directly
// In a real app, you might want to add validation to the slice
return;
}

setLoading(true);
setError(null);
dispatch(loginAsync({ email, password }));
};

try {
await authService.login({ email, password });
// Navigate to main app after successful login
router.replace('/(tabs)');
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Login failed. Please try again.';
setError(errorMessage);
setSnackbarVisible(true);
} finally {
setLoading(false);
}
const handleDismissSnackbar = () => {
setSnackbarVisible(false);
dismissedErrorRef.current = error;
dispatch(clearError());
};

return (
Expand Down Expand Up @@ -109,11 +122,11 @@ export default function LoginScreen() {

<Snackbar
visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)}
onDismiss={handleDismissSnackbar}
duration={4000}
action={{
label: 'Dismiss',
onPress: () => setSnackbarVisible(false),
onPress: handleDismissSnackbar,
}}
>
{error || 'An error occurred'}
Expand Down
82 changes: 65 additions & 17 deletions app/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
/**
* Explore Screen
* Example screen demonstrating API integration with loading, error, and success states
* Uses the useFetch hook for simplified data fetching
*
* This screen demonstrates Redux Toolkit for state management.
* For the useFetch hook approach, see the main branch.
*/
import { useEffect } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Card, Text, Button, useTheme } from 'react-native-paper';
import { todosApi } from '@/services/api';
import { useFetch } from '@/hooks/useFetch';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import {
fetchTodosAsync,
selectTodos,
selectTodosLoading,
selectTodosError,
toggleTodo,
} from '@/store/slices/todosSlice';
import { LoadingScreen } from '@/components/LoadingScreen';
import type { Todo } from '@/types/api';

export default function ExploreScreen() {
const theme = useTheme();
const dispatch = useAppDispatch();

// Use useFetch hook to simplify data fetching
const {
data: todos,
loading,
error,
refetch,
} = useFetch<Todo[]>(async () => {
const data = await todosApi.getAll();
// Limit to 10 todos for demo
return data.slice(0, 10);
});
// Redux approach - Global state management
const reduxTodos = useAppSelector(selectTodos);
const reduxLoading = useAppSelector(selectTodosLoading);
const reduxError = useAppSelector(selectTodosError);

// Fetch todos from Redux on mount
useEffect(() => {
if (reduxTodos.length === 0) {
dispatch(fetchTodosAsync());
}
}, [dispatch, reduxTodos.length]);

// Use Redux state for display
const todos = reduxTodos.slice(0, 10); // Limit to 10 for demo
const loading = reduxLoading;
const error = reduxError;

const handleToggleTodo = (id: number) => {
dispatch(toggleTodo(id));
};

const handleRefetch = () => {
dispatch(fetchTodosAsync());
};

// Show full-screen loading on initial load
if (loading && !todos && !error) {
Expand Down Expand Up @@ -52,13 +74,19 @@ export default function ExploreScreen() {
>
Fetching todos from JSONPlaceholder API
</Text>
<Text
variant="bodySmall"
style={[styles.subtitle, { color: theme.colors.onSurfaceVariant }]}
>
Using Redux Toolkit for state management
</Text>
</View>

{/* Retry Button */}
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={refetch}
onPress={handleRefetch}
disabled={loading}
icon="refresh"
>
Expand Down Expand Up @@ -98,7 +126,11 @@ export default function ExploreScreen() {
Todos ({todos.length})
</Text>
{todos.map(todo => (
<Card key={todo.id} style={styles.todoCard}>
<Card
key={todo.id}
style={styles.todoCard}
onPress={() => handleToggleTodo(todo.id)}
>
<Card.Content>
<View style={styles.todoHeader}>
<Text
Expand All @@ -125,6 +157,12 @@ export default function ExploreScreen() {
<Text variant="bodySmall" style={styles.todoMeta}>
User ID: {todo.userId} • ID: {todo.id}
</Text>
<Text
variant="bodySmall"
style={[styles.tapHint, { color: theme.colors.primary }]}
>
Tap to toggle completion
</Text>
</Card.Content>
</Card>
))}
Expand Down Expand Up @@ -157,6 +195,11 @@ const styles = StyleSheet.create({
marginBottom: 8,
fontWeight: 'bold',
},
subtitle: {
marginTop: 4,
opacity: 0.7,
fontStyle: 'italic',
},
buttonContainer: {
marginBottom: 24,
alignItems: 'center',
Expand Down Expand Up @@ -202,4 +245,9 @@ const styles = StyleSheet.create({
todoMeta: {
opacity: 0.6,
},
tapHint: {
marginTop: 4,
fontSize: 10,
fontStyle: 'italic',
},
});
10 changes: 7 additions & 3 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { PaperProvider } from 'react-native-paper';
import { useColorScheme } from 'react-native';
import { Provider as ReduxProvider } from 'react-redux';
import { getTheme } from '@/constants/Theme';
import { Stack } from 'expo-router';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { store } from '@/store';

export default function RootLayout() {
const colorScheme = useColorScheme();
const theme = getTheme(colorScheme ?? null);

return (
<ErrorBoundary>
<PaperProvider theme={theme}>
<Stack screenOptions={{ headerShown: false }} />
</PaperProvider>
<ReduxProvider store={store}>
<PaperProvider theme={theme}>
<Stack screenOptions={{ headerShown: false }} />
</PaperProvider>
</ReduxProvider>
</ErrorBoundary>
);
}
Loading