diff --git a/README.md b/README.md index ac747c0..520bd9b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index b5ab213..9232b39 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -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 { @@ -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(null); const [snackbarVisible, setSnackbarVisible] = useState(false); + const dismissedErrorRef = useRef(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 ( @@ -109,11 +122,11 @@ export default function LoginScreen() { setSnackbarVisible(false)} + onDismiss={handleDismissSnackbar} duration={4000} action={{ label: 'Dismiss', - onPress: () => setSnackbarVisible(false), + onPress: handleDismissSnackbar, }} > {error || 'An error occurred'} diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index 26521bb..5ce2ec1 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -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(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) { @@ -52,13 +74,19 @@ export default function ExploreScreen() { > Fetching todos from JSONPlaceholder API + + Using Redux Toolkit for state management + {/* Retry Button */}