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
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ This starter includes everything you need to build a production-ready React Nati
- ✅ **Dark/Light Mode** - Automatic system preference detection
- ✅ **API Client** - Axios with interceptors for authentication and error handling
- ✅ **Storage Service** - AsyncStorage wrapper with TypeScript support
- ✅ **State Management** - Zustand with persistence and TypeScript support
- ✅ **Custom Hooks** - `useFetch` for data fetching with loading/error states
- ✅ **Error Boundary** - Global error handling component
- ✅ **Loading States** - Built-in loading screen component
Expand Down Expand Up @@ -156,6 +157,10 @@ react-native-starter/
│ └── LoadingScreen.tsx
├── hooks/ # Custom React hooks
│ └── useFetch.ts # Data fetching hook
├── stores/ # Zustand state management stores
│ ├── useAuthStore.ts # Authentication store
│ ├── useTodosStore.ts # Todos store
│ └── index.ts # Store exports
├── services/ # API & storage services
│ ├── api.ts # Axios client with interceptors
│ └── storage.ts # AsyncStorage wrapper
Expand All @@ -173,6 +178,7 @@ react-native-starter/
- **`app/`** - All screens go here. Files automatically become routes (Expo Router).
- **`components/`** - Reusable UI components used across screens.
- **`hooks/`** - Custom React hooks for shared logic (e.g., `useFetch`).
- **`stores/`** - Zustand state management stores (auth, todos, etc.).
- **`services/`** - API client and storage utilities.
- **`constants/`** - App-wide constants like colors and theme config.
- **`types/`** - TypeScript interfaces and types.
Expand Down Expand Up @@ -232,6 +238,39 @@ import { Image } from 'react-native';

See [Assets Guide](docs/assets.md) for more information.

### State Management (Zustand)

This project includes **Zustand** for state management - a simple, lightweight alternative to Redux with minimal boilerplate.

**Key advantages:**

- No provider needed - works globally
- Minimal boilerplate - just create and use
- Built-in persistence with AsyncStorage
- Full TypeScript support
- Better performance than Context API

**Quick example:**

```tsx
import { useAuthStore } from '@/stores';

export default function LoginScreen() {
const login = useAuthStore(state => state.login);
const loading = useAuthStore(state => state.loading);

const handleLogin = async () => {
await login({ email, password });
};

return (
// Your UI
);
}
```

See [State Management with Zustand](docs/state-management-zustand.md) for comprehensive documentation.

### Environment Variables

This project uses environment variables for configuration. Copy `.env.example` to `.env` and fill in your values:
Expand Down Expand Up @@ -336,9 +375,10 @@ Expo Go is a free app for testing your app on physical devices:
### Feature Guides

- **[API and Storage](docs/api-and-storage.md)** - Backend integration guide
- **[State Management with Zustand](docs/state-management-zustand.md)** - Zustand state management guide
- **[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
- **[Error and Loading Handling](docs/error-and-loading.md)** - Error and loading state patterns

### Additional Guides

Expand Down
54 changes: 35 additions & 19 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 } from 'react';
import { View, StyleSheet, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
Expand All @@ -9,37 +9,47 @@ import {
Snackbar,
} from 'react-native-paper';
import { router } from 'expo-router';
import { authService } from '@/services/auth';
import { useAuthStore, useAuthLoading, useAuthError } from '@/stores';

export default function LoginScreen() {
const theme = useTheme();
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);

// Use Zustand store for auth state
const login = useAuthStore(state => state.login);
const clearError = useAuthStore(state => state.clearError);
const loading = useAuthLoading();
const error = useAuthError();

// Show snackbar when error changes
useEffect(() => {
if (error) {
// Defer state update to avoid synchronous setState in effect
setTimeout(() => {
setSnackbarVisible(true);
}, 0);
}
}, [error]);

const handleLogin = async () => {
if (!email || !password) {
setError('Please fill in all fields');
// For validation errors, show snackbar directly
setSnackbarVisible(true);
return;
}

setLoading(true);
setError(null);
clearError();
setSnackbarVisible(false);

try {
await authService.login({ email, password });
await 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);
} catch {
// Error is already set in the store, snackbar will show via useEffect
// No need to set snackbarVisible here as useEffect handles it
}
};

Expand Down Expand Up @@ -102,21 +112,27 @@ export default function LoginScreen() {
>
Note: This is a demo. Update the API endpoint in{' '}
<Text style={styles.monospace}>services/auth.ts</Text> to connect to
your backend.
your backend. State is managed with Zustand (no provider needed!).
</Text>
</View>
</ScrollView>

<Snackbar
visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)}
onDismiss={() => {
setSnackbarVisible(false);
clearError();
}}
duration={4000}
action={{
label: 'Dismiss',
onPress: () => setSnackbarVisible(false),
onPress: () => {
setSnackbarVisible(false);
clearError();
},
}}
>
{error || 'An error occurred'}
{error || 'Please fill in all fields'}
</Snackbar>
</SafeAreaView>
);
Expand Down
69 changes: 48 additions & 21 deletions app/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
/**
* Explore Screen
* Example screen demonstrating API integration with loading, error, and success states
* Uses the useFetch hook for simplified data fetching
* Uses Zustand store for state management (alternative to useFetch hook)
*/
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 {
useTodosStore,
useTodos,
useTodosLoading,
useTodosError,
} from '@/stores';
import { LoadingScreen } from '@/components/LoadingScreen';
import type { Todo } from '@/types/api';

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

// 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);
});
// Use Zustand store for todos state management
// No provider needed - Zustand works globally!
const todos = useTodos();
const loading = useTodosLoading();
const error = useTodosError();
const fetchTodos = useTodosStore(state => state.fetchTodos);
const clearError = useTodosStore(state => state.clearError);

// Fetch todos on mount
useEffect(() => {
fetchTodos();
}, [fetchTodos]);

// Limit to 10 todos for demo (computed selector)
const displayedTodos = todos.slice(0, 10);

// Show full-screen loading on initial load
if (loading && !todos && !error) {
if (loading && todos.length === 0 && !error) {
return (
<SafeAreaView
style={[styles.container, { backgroundColor: theme.colors.background }]}
Expand All @@ -50,20 +58,25 @@ export default function ExploreScreen() {
variant="bodyMedium"
style={{ color: theme.colors.onSurfaceVariant }}
>
Fetching todos from JSONPlaceholder API
Fetching todos from JSONPlaceholder API using Zustand
</Text>
</View>

{/* Retry Button */}
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={refetch}
onPress={fetchTodos}
disabled={loading}
icon="refresh"
>
{loading ? 'Loading...' : 'Retry'}
</Button>
{error && (
<Button mode="text" onPress={clearError} style={styles.clearButton}>
Clear Error
</Button>
)}
</View>

{/* Error State */}
Expand Down Expand Up @@ -92,12 +105,12 @@ export default function ExploreScreen() {
)}

{/* Success State - Todo List */}
{!loading && !error && todos && todos.length > 0 && (
{!loading && !error && displayedTodos && displayedTodos.length > 0 && (
<View style={styles.todosContainer}>
<Text variant="titleMedium" style={styles.sectionTitle}>
Todos ({todos.length})
Todos ({displayedTodos.length} of {todos.length} total)
</Text>
{todos.map(todo => (
{displayedTodos.map(todo => (
<Card key={todo.id} style={styles.todoCard}>
<Card.Content>
<View style={styles.todoHeader}>
Expand Down Expand Up @@ -135,6 +148,12 @@ export default function ExploreScreen() {
{!loading && !error && todos && todos.length === 0 && (
<View style={styles.centerContainer}>
<Text variant="bodyMedium">No todos found</Text>
<Text
variant="bodySmall"
style={[styles.note, { color: theme.colors.onSurfaceVariant }]}
>
State managed with Zustand - no provider needed!
</Text>
</View>
)}
</ScrollView>
Expand Down Expand Up @@ -202,4 +221,12 @@ const styles = StyleSheet.create({
todoMeta: {
opacity: 0.6,
},
clearButton: {
marginTop: 8,
},
note: {
marginTop: 8,
textAlign: 'center',
opacity: 0.7,
},
});
Loading