diff --git a/README.md b/README.md index ac747c0..4ad69af 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This starter includes everything you need to build a production-ready React Nati - ✅ **Error Boundary** - Global error handling component - ✅ **Loading States** - Built-in loading screen component - ✅ **Authentication Example** - Complete login flow with token management +- ✅ **State Management** - React Context API with TypeScript and useReducer patterns - ✅ **TypeScript** - Full type safety throughout - ✅ **ESLint + Prettier** - Code quality and formatting tools - ✅ **Example Screens** - See features in action @@ -338,7 +339,8 @@ Expo Go is a free app for testing your app on physical devices: - **[API and Storage](docs/api-and-storage.md)** - Backend integration 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 states +- **[State Management with Context API](docs/state-management-context.md)** - React Context patterns ### Additional Guides @@ -351,6 +353,27 @@ Expo Go is a free app for testing your app on physical devices: - [Store Data](docs/store-data.md) - [Environment Variables](docs/environment-variables.md) +## State Management + +This starter includes **React Context API** for state management with production-ready patterns: + +- ✅ **AuthContext** - Authentication state with login/logout +- ✅ **TodosContext** - CRUD operations with async handling +- ✅ **useReducer pattern** - Complex state management +- ✅ **TypeScript support** - Full type safety +- ✅ **Custom hooks** - `useAuth()` and `useTodos()` for easy consumption +- ✅ **Performance optimized** - Memoized values and actions + +See the [State Management with Context API](docs/state-management-context.md) guide for: + +- When to use Context vs other solutions +- Best practices and patterns +- Performance optimization +- Integration with services +- Common pitfalls and solutions + +**Note:** This branch demonstrates Context API patterns. For other state management solutions (Redux, Zustand, etc.), see their respective branches. + ## Resources - [Expo Documentation](https://docs.expo.dev/) diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx index b5ab213..ec33dec 100644 --- a/app/(auth)/login.tsx +++ b/app/(auth)/login.tsx @@ -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 { @@ -9,37 +9,34 @@ import { Snackbar, } from 'react-native-paper'; import { router } from 'expo-router'; -import { authService } from '@/services/auth'; +import { useAuth } from '@/contexts'; export default function LoginScreen() { const theme = useTheme(); + const { login, isLoading, error, isAuthenticated, clearError } = useAuth(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [snackbarVisible, setSnackbarVisible] = useState(false); + const [validationError, setValidationError] = useState(''); + + // Navigate to main app when authenticated + useEffect(() => { + if (isAuthenticated) { + router.replace('/(tabs)'); + } + }, [isAuthenticated]); const handleLogin = async () => { if (!email || !password) { - setError('Please fill in all fields'); - setSnackbarVisible(true); + setValidationError('Please enter both email and password'); return; } - setLoading(true); - setError(null); - + setValidationError(''); 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); + await login({ email, password }); + // Navigation will happen automatically via useEffect when isAuthenticated changes + } catch { + // Error is already handled by context } }; @@ -89,8 +86,8 @@ export default function LoginScreen() { - {/* Error State */} - {error && !loading && ( - - + + + {!showContextExample ? ( + // Pattern 1: useFetch Hook + <> + + + Pattern 1: useFetch Hook + - Error + Local state management with custom hook + + + + {/* Retry Button */} + + + + + {/* Error State */} + {error && !loading && ( + + + + Error + + + {error} + + + + )} + + {/* Success State - Todo List */} + {!loading && !error && todos && todos.length > 0 && ( + + + Todos ({todos.length}) + + {todos.slice(0, 10).map(todo => ( + + + + + {todo.title} + + {todo.completed && ( + + ✓ Done + + )} + + + User ID: {todo.userId} • ID: {todo.id} + + + + ))} + + )} + + {/* Empty State */} + {!loading && !error && todos && todos.length === 0 && ( + + No todos found + + )} + + ) : ( + // Pattern 2: Context API + <> + + + Pattern 2: Context API - {error} + Global state management with React Context - - - )} + - {/* Success State - Todo List */} - {!loading && !error && todos && todos.length > 0 && ( - - - Todos ({todos.length}) - - {todos.map(todo => ( - + {/* Retry Button */} + + + + + {/* Error State */} + {contextError && !contextLoading && ( + - - - {todo.title} - - {todo.completed && ( - - ✓ Done - - )} - - - User ID: {todo.userId} • ID: {todo.id} + + Error + + + {contextError} - ))} - - )} + )} + + {/* Success State - Todo List from Context */} + {!contextLoading && + !contextError && + contextTodos && + contextTodos.length > 0 && ( + + + Todos from Context ({contextTodos.length}) + + {contextTodos.slice(0, 10).map(todo => ( + + + + + {todo.title} + + {todo.completed && ( + + ✓ Done + + )} + + + User ID: {todo.userId} • ID: {todo.id} + + + + ))} + + )} - {/* Empty State */} - {!loading && !error && todos && todos.length === 0 && ( - - No todos found - + {/* Empty State */} + {!contextLoading && + !contextError && + contextTodos && + contextTodos.length === 0 && ( + + No todos found + + )} + )} @@ -161,6 +317,12 @@ const styles = StyleSheet.create({ marginBottom: 24, alignItems: 'center', }, + divider: { + marginVertical: 24, + }, + sectionHeader: { + marginBottom: 16, + }, centerContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/app/_layout.tsx b/app/_layout.tsx index 02dbc05..bb5b56a 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -3,6 +3,8 @@ import { useColorScheme } from 'react-native'; import { getTheme } from '@/constants/Theme'; import { Stack } from 'expo-router'; import { ErrorBoundary } from '@/components/ErrorBoundary'; +import { AuthProvider } from '@/contexts/AuthContext'; +import { TodosProvider } from '@/contexts/TodosContext'; export default function RootLayout() { const colorScheme = useColorScheme(); @@ -11,7 +13,11 @@ export default function RootLayout() { return ( - + + + + + ); diff --git a/contexts/AuthContext.tsx b/contexts/AuthContext.tsx new file mode 100644 index 0000000..ff7f22d --- /dev/null +++ b/contexts/AuthContext.tsx @@ -0,0 +1,274 @@ +/** + * Auth Context + * Production-ready authentication context with TypeScript, async operations, + * and proper error handling. Uses useReducer for complex state management. + */ +import React, { + createContext, + useContext, + useReducer, + useCallback, + useEffect, + ReactNode, +} from 'react'; +import { + authService, + type AuthResponse, + type LoginCredentials, +} from '@/services/auth'; +import { getItem, STORAGE_KEYS } from '@/services/storage'; + +// ============================================================================ +// Types +// ============================================================================ + +interface AuthState { + user: AuthResponse['user'] | null; + token: string | null; + isAuthenticated: boolean; + isLoading: boolean; + isInitializing: boolean; + error: string | null; +} + +type AuthAction = + | { type: 'AUTH_START' } + | { + type: 'AUTH_SUCCESS'; + payload: { user: AuthResponse['user']; token: string }; + } + | { type: 'AUTH_FAILURE'; payload: { error: string } } + | { type: 'AUTH_LOGOUT' } + | { type: 'AUTH_INIT_START' } + | { + type: 'AUTH_INIT_SUCCESS'; + payload: { user: AuthResponse['user'] | null; token: string | null }; + } + | { type: 'AUTH_INIT_FAILURE' } + | { type: 'SET_USER'; payload: { user: AuthResponse['user'] } } + | { type: 'CLEAR_ERROR' }; + +export interface AuthContextValue extends AuthState { + login: (credentials: LoginCredentials) => Promise; + logout: () => Promise; + setUser: (user: AuthResponse['user']) => void; + clearError: () => void; +} + +// ============================================================================ +// Initial State +// ============================================================================ + +const initialState: AuthState = { + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + isInitializing: true, + error: null, +}; + +// ============================================================================ +// Reducer +// ============================================================================ + +function authReducer(state: AuthState, action: AuthAction): AuthState { + switch (action.type) { + case 'AUTH_START': + return { + ...state, + isLoading: true, + error: null, + }; + + case 'AUTH_SUCCESS': + return { + ...state, + user: action.payload.user, + token: action.payload.token, + isAuthenticated: true, + isLoading: false, + error: null, + }; + + case 'AUTH_FAILURE': + return { + ...state, + isLoading: false, + error: action.payload.error, + isAuthenticated: false, + }; + + case 'AUTH_LOGOUT': + return { + ...state, + user: null, + token: null, + isAuthenticated: false, + isLoading: false, + error: null, + }; + + case 'AUTH_INIT_START': + return { + ...state, + isInitializing: true, + }; + + case 'AUTH_INIT_SUCCESS': + return { + ...state, + user: action.payload.user, + token: action.payload.token, + isAuthenticated: action.payload.token !== null, + isInitializing: false, + error: null, + }; + + case 'AUTH_INIT_FAILURE': + return { + ...state, + isInitializing: false, + isAuthenticated: false, + }; + + case 'SET_USER': + return { + ...state, + user: action.payload.user, + }; + + case 'CLEAR_ERROR': + return { + ...state, + error: null, + }; + + default: + return state; + } +} + +// ============================================================================ +// Context +// ============================================================================ + +const AuthContext = createContext(undefined); + +// ============================================================================ +// Provider +// ============================================================================ + +interface AuthProviderProps { + children: ReactNode; +} + +export function AuthProvider({ children }: AuthProviderProps) { + const [state, dispatch] = useReducer(authReducer, initialState); + + // Initialize auth state from storage on mount + useEffect(() => { + async function initializeAuth() { + dispatch({ type: 'AUTH_INIT_START' }); + + try { + const [user, token] = await Promise.all([ + authService.getCurrentUser(), + getItem(STORAGE_KEYS.AUTH_TOKEN), + ]); + + dispatch({ + type: 'AUTH_INIT_SUCCESS', + payload: { user, token }, + }); + } catch (error) { + console.error('Failed to initialize auth:', error); + dispatch({ type: 'AUTH_INIT_FAILURE' }); + } + } + + initializeAuth(); + }, []); + + // Login action + const login = useCallback(async (credentials: LoginCredentials) => { + dispatch({ type: 'AUTH_START' }); + + try { + const response = await authService.login(credentials); + dispatch({ + type: 'AUTH_SUCCESS', + payload: { + user: response.user, + token: response.token, + }, + }); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Login failed. Please try again.'; + dispatch({ + type: 'AUTH_FAILURE', + payload: { error: errorMessage }, + }); + throw error; // Re-throw to allow component-level error handling + } + }, []); + + // Logout action + const logout = useCallback(async () => { + try { + await authService.logout(); + dispatch({ type: 'AUTH_LOGOUT' }); + } catch (error) { + console.error('Logout error:', error); + // Even if logout fails, clear local state + dispatch({ type: 'AUTH_LOGOUT' }); + } + }, []); + + // Set user action (for updating user profile, etc.) + const setUser = useCallback((user: AuthResponse['user']) => { + dispatch({ type: 'SET_USER', payload: { user } }); + }, []); + + // Clear error action + const clearError = useCallback(() => { + dispatch({ type: 'CLEAR_ERROR' }); + }, []); + + // Memoize context value to prevent unnecessary re-renders + const contextValue: AuthContextValue = React.useMemo( + () => ({ + ...state, + login, + logout, + setUser, + clearError, + }), + [state, login, logout, setUser, clearError] + ); + + return ( + {children} + ); +} + +// ============================================================================ +// Custom Hook +// ============================================================================ + +/** + * Hook to access auth context + * @throws Error if used outside AuthProvider + */ +export function useAuth(): AuthContextValue { + const context = useContext(AuthContext); + + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} diff --git a/contexts/TodosContext.tsx b/contexts/TodosContext.tsx new file mode 100644 index 0000000..50ef9da --- /dev/null +++ b/contexts/TodosContext.tsx @@ -0,0 +1,371 @@ +/** + * Todos Context + * Production-ready todos context with TypeScript, CRUD operations, + * async operations, and proper error handling. Uses useReducer for complex state. + */ +import React, { + createContext, + useContext, + useReducer, + useCallback, + useEffect, + ReactNode, +} from 'react'; +import { todosApi } from '@/services/api'; +import type { Todo } from '@/types/api'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TodosState { + todos: Todo[]; + isLoading: boolean; + error: string | null; + lastFetched: number | null; +} + +type TodosAction = + | { type: 'TODOS_FETCH_START' } + | { type: 'TODOS_FETCH_SUCCESS'; payload: { todos: Todo[] } } + | { type: 'TODOS_FETCH_FAILURE'; payload: { error: string } } + | { type: 'TODO_ADD_START' } + | { type: 'TODO_ADD_SUCCESS'; payload: { todo: Todo } } + | { type: 'TODO_ADD_FAILURE'; payload: { error: string } } + | { type: 'TODO_UPDATE_START' } + | { type: 'TODO_UPDATE_SUCCESS'; payload: { todo: Todo } } + | { type: 'TODO_UPDATE_FAILURE'; payload: { error: string } } + | { type: 'TODO_DELETE_START' } + | { type: 'TODO_DELETE_SUCCESS'; payload: { id: number } } + | { type: 'TODO_DELETE_FAILURE'; payload: { error: string } } + | { type: 'CLEAR_ERROR' }; + +export interface TodosContextValue extends TodosState { + fetchTodos: () => Promise; + addTodo: (todo: Omit) => Promise; + updateTodo: (id: number, updates: Partial) => Promise; + deleteTodo: (id: number) => Promise; + clearError: () => void; + refetch: () => Promise; +} + +// ============================================================================ +// Initial State +// ============================================================================ + +const initialState: TodosState = { + todos: [], + isLoading: false, + error: null, + lastFetched: null, +}; + +// ============================================================================ +// Reducer +// ============================================================================ + +function todosReducer(state: TodosState, action: TodosAction): TodosState { + switch (action.type) { + case 'TODOS_FETCH_START': + return { + ...state, + isLoading: true, + error: null, + }; + + case 'TODOS_FETCH_SUCCESS': + return { + ...state, + todos: action.payload.todos, + isLoading: false, + error: null, + lastFetched: Date.now(), + }; + + case 'TODOS_FETCH_FAILURE': + return { + ...state, + isLoading: false, + error: action.payload.error, + }; + + case 'TODO_ADD_START': + return { + ...state, + isLoading: true, + error: null, + }; + + case 'TODO_ADD_SUCCESS': + return { + ...state, + todos: [...state.todos, action.payload.todo], + isLoading: false, + error: null, + }; + + case 'TODO_ADD_FAILURE': + return { + ...state, + isLoading: false, + error: action.payload.error, + }; + + case 'TODO_UPDATE_START': + return { + ...state, + isLoading: true, + error: null, + }; + + case 'TODO_UPDATE_SUCCESS': + return { + ...state, + todos: state.todos.map(todo => + todo.id === action.payload.todo.id ? action.payload.todo : todo + ), + isLoading: false, + error: null, + }; + + case 'TODO_UPDATE_FAILURE': + return { + ...state, + isLoading: false, + error: action.payload.error, + }; + + case 'TODO_DELETE_START': + return { + ...state, + isLoading: true, + error: null, + }; + + case 'TODO_DELETE_SUCCESS': + return { + ...state, + todos: state.todos.filter(todo => todo.id !== action.payload.id), + isLoading: false, + error: null, + }; + + case 'TODO_DELETE_FAILURE': + return { + ...state, + isLoading: false, + error: action.payload.error, + }; + + case 'CLEAR_ERROR': + return { + ...state, + error: null, + }; + + default: + return state; + } +} + +// ============================================================================ +// Context +// ============================================================================ + +const TodosContext = createContext(undefined); + +// ============================================================================ +// Provider +// ============================================================================ + +interface TodosProviderProps { + children: ReactNode; + autoFetch?: boolean; // Option to auto-fetch on mount +} + +export function TodosProvider({ + children, + autoFetch = false, +}: TodosProviderProps) { + const [state, dispatch] = useReducer(todosReducer, initialState); + + // Auto-fetch todos on mount if enabled + useEffect(() => { + if (autoFetch && state.todos.length === 0 && !state.isLoading) { + fetchTodos(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoFetch]); + + // Fetch all todos + const fetchTodos = useCallback(async () => { + dispatch({ type: 'TODOS_FETCH_START' }); + + try { + const todos = await todosApi.getAll(); + dispatch({ + type: 'TODOS_FETCH_SUCCESS', + payload: { todos }, + }); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to fetch todos. Please try again.'; + dispatch({ + type: 'TODOS_FETCH_FAILURE', + payload: { error: errorMessage }, + }); + throw error; // Re-throw to allow component-level error handling + } + }, []); + + // Add a new todo + const addTodo = useCallback( + async (todoData: Omit): Promise => { + dispatch({ type: 'TODO_ADD_START' }); + + try { + // Note: JSONPlaceholder API doesn't actually create todos, + // so we'll simulate it by generating an ID and adding to local state + // In a real app, you'd call: const newTodo = await todosApi.create(todoData); + const newTodo: Todo = { + ...todoData, + id: Date.now(), // Temporary ID generation + }; + + dispatch({ + type: 'TODO_ADD_SUCCESS', + payload: { todo: newTodo }, + }); + + return newTodo; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to add todo. Please try again.'; + dispatch({ + type: 'TODO_ADD_FAILURE', + payload: { error: errorMessage }, + }); + throw error; + } + }, + [] + ); + + // Update an existing todo + const updateTodo = useCallback( + async (id: number, updates: Partial): Promise => { + dispatch({ type: 'TODO_UPDATE_START' }); + + try { + // Note: JSONPlaceholder API doesn't actually update todos, + // so we'll simulate it by updating local state + // In a real app, you'd call: const updatedTodo = await todosApi.update(id, updates); + const existingTodo = state.todos.find(todo => todo.id === id); + + if (!existingTodo) { + throw new Error(`Todo with id ${id} not found`); + } + + const updatedTodo: Todo = { + ...existingTodo, + ...updates, + }; + + dispatch({ + type: 'TODO_UPDATE_SUCCESS', + payload: { todo: updatedTodo }, + }); + + return updatedTodo; + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to update todo. Please try again.'; + dispatch({ + type: 'TODO_UPDATE_FAILURE', + payload: { error: errorMessage }, + }); + throw error; + } + }, + [state.todos] + ); + + // Delete a todo + const deleteTodo = useCallback(async (id: number): Promise => { + dispatch({ type: 'TODO_DELETE_START' }); + + try { + // Note: JSONPlaceholder API doesn't actually delete todos, + // so we'll simulate it by removing from local state + // In a real app, you'd call: await todosApi.delete(id); + dispatch({ + type: 'TODO_DELETE_SUCCESS', + payload: { id }, + }); + } catch (error) { + const errorMessage = + error instanceof Error + ? error.message + : 'Failed to delete todo. Please try again.'; + dispatch({ + type: 'TODO_DELETE_FAILURE', + payload: { error: errorMessage }, + }); + throw error; + } + }, []); + + // Clear error + const clearError = useCallback(() => { + dispatch({ type: 'CLEAR_ERROR' }); + }, []); + + // Refetch (alias for fetchTodos for consistency with useFetch hook) + const refetch = useCallback(async () => { + await fetchTodos(); + }, [fetchTodos]); + + // Memoize context value to prevent unnecessary re-renders + const contextValue: TodosContextValue = React.useMemo( + () => ({ + ...state, + fetchTodos, + addTodo, + updateTodo, + deleteTodo, + clearError, + refetch, + }), + [state, fetchTodos, addTodo, updateTodo, deleteTodo, clearError, refetch] + ); + + return ( + + {children} + + ); +} + +// ============================================================================ +// Custom Hook +// ============================================================================ + +/** + * Hook to access todos context + * @throws Error if used outside TodosProvider + */ +export function useTodos(): TodosContextValue { + const context = useContext(TodosContext); + + if (context === undefined) { + throw new Error('useTodos must be used within a TodosProvider'); + } + + return context; +} diff --git a/contexts/index.ts b/contexts/index.ts new file mode 100644 index 0000000..e923e8f --- /dev/null +++ b/contexts/index.ts @@ -0,0 +1,12 @@ +/** + * Contexts Index + * Central export point for all contexts and their hooks + */ + +// Auth Context +export { AuthProvider, useAuth } from './AuthContext'; +export type { AuthContextValue } from './AuthContext'; + +// Todos Context +export { TodosProvider, useTodos } from './TodosContext'; +export type { TodosContextValue } from './TodosContext'; diff --git a/docs/state-management-context.md b/docs/state-management-context.md new file mode 100644 index 0000000..638b49a --- /dev/null +++ b/docs/state-management-context.md @@ -0,0 +1,660 @@ +# State Management with React Context API + +This guide covers advanced React Context patterns for state management in React Native applications. The Context API is a built-in React feature that provides a way to share state across components without prop drilling. + +## Table of Contents + +- [Why Context API?](#why-context-api) +- [When to Use Context vs Other Solutions](#when-to-use-context-vs-other-solutions) +- [Context Best Practices](#context-best-practices) +- [Creating Contexts](#creating-contexts) +- [useReducer Pattern](#usereducer-pattern) +- [Performance Optimization](#performance-optimization) +- [TypeScript Patterns](#typescript-patterns) +- [Custom Hooks for Context](#custom-hooks-for-context) +- [Provider Composition](#provider-composition) +- [Integration with Services](#integration-with-services) +- [Common Pitfalls](#common-pitfalls) +- [When to Split Contexts](#when-to-split-contexts) + +## Why Context API? + +The Context API is ideal for: + +- **Global state** that needs to be accessed by many components +- **Theme and UI preferences** (dark mode, language, etc.) +- **Authentication state** (user, token, login status) +- **Feature-specific state** (shopping cart, form state, etc.) +- **Avoiding prop drilling** through multiple component levels + +### Advantages + +- ✅ **Built-in** - No external dependencies +- ✅ **TypeScript support** - Full type safety +- ✅ **Simple API** - Easy to understand and use +- ✅ **React DevTools** - Built-in debugging support +- ✅ **Lightweight** - No bundle size impact + +### Limitations + +- ⚠️ **Re-render performance** - All consumers re-render when context value changes +- ⚠️ **Not for high-frequency updates** - Better for relatively stable state +- ⚠️ **No middleware** - Unlike Redux, no built-in middleware support +- ⚠️ **No time-travel debugging** - No built-in devtools like Redux DevTools + +## When to Use Context vs Other Solutions + +### Use Context API When: + +- State is needed by many components at different nesting levels +- State updates are infrequent (auth, theme, user preferences) +- You want a simple solution without external dependencies +- State is domain-specific (auth, todos, cart) + +### Use Redux/Zustand When: + +- You need time-travel debugging +- State updates are very frequent +- You need middleware (logging, persistence, etc.) +- Complex state logic with many reducers +- Large applications with many developers + +### Use Local State When: + +- State is only needed in one component +- State doesn't need to be shared +- Simple form state or UI state + +## Context Best Practices + +### 1. Split Contexts by Domain + +**❌ Bad:** One giant context for everything + +```tsx +// Don't do this +const AppContext = createContext({ + user: null, + todos: [], + theme: 'light', + cart: [], + // ... everything +}); +``` + +**✅ Good:** Separate contexts by domain + +```tsx +// Separate contexts +const AuthContext = createContext({...}); +const TodosContext = createContext({...}); +const ThemeContext = createContext({...}); +``` + +### 2. Use Custom Hooks + +**❌ Bad:** Direct context consumption + +```tsx +function Component() { + const context = useContext(AuthContext); + if (!context) throw new Error('...'); + return
{context.user.name}
; +} +``` + +**✅ Good:** Custom hook with error handling + +```tsx +function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +} + +function Component() { + const { user } = useAuth(); + return
{user.name}
; +} +``` + +### 3. Memoize Context Values + +**❌ Bad:** New object on every render + +```tsx +function Provider({ children }) { + const [state, setState] = useState({...}); + return ( + + {children} + + ); +} +``` + +**✅ Good:** Memoized value + +```tsx +function Provider({ children }) { + const [state, setState] = useState({...}); + const value = useMemo( + () => ({ state, setState }), + [state] + ); + return ( + + {children} + + ); +} +``` + +### 4. Use useReducer for Complex State + +**❌ Bad:** Multiple useState calls + +```tsx +const [user, setUser] = useState(null); +const [loading, setLoading] = useState(false); +const [error, setError] = useState(null); +const [token, setToken] = useState(null); +// ... many more +``` + +**✅ Good:** useReducer for related state + +```tsx +const [state, dispatch] = useReducer(authReducer, initialState); +``` + +## Creating Contexts + +### Basic Context Structure + +```tsx +// 1. Define types +interface ContextState { + // state properties +} + +interface ContextValue extends ContextState { + // actions +} + +// 2. Create context +const Context = createContext(undefined); + +// 3. Create provider +export function Provider({ children }: { children: ReactNode }) { + // state management + const value: ContextValue = { + // state and actions + }; + + return {children}; +} + +// 4. Create custom hook +export function useContext() { + const context = useContext(Context); + if (!context) { + throw new Error('useContext must be used within Provider'); + } + return context; +} +``` + +### Example: AuthContext + +See `contexts/AuthContext.tsx` for a complete example with: + +- TypeScript types +- useReducer for state management +- Async operations (login, logout) +- Error handling +- Initialization from storage +- Memoized context value + +## useReducer Pattern + +`useReducer` is ideal for complex state with multiple related values and actions. + +### Benefits + +- **Centralized logic** - All state updates in one place +- **Predictable updates** - Actions clearly define state changes +- **Easier testing** - Reducer is a pure function +- **Better performance** - Single state update instead of multiple + +### Pattern + +```tsx +// 1. Define state +interface State { + user: User | null; + loading: boolean; + error: string | null; +} + +// 2. Define actions +type Action = + | { type: 'LOGIN_START' } + | { type: 'LOGIN_SUCCESS'; payload: { user: User } } + | { type: 'LOGIN_FAILURE'; payload: { error: string } } + | { type: 'LOGOUT' }; + +// 3. Create reducer +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'LOGIN_START': + return { ...state, loading: true, error: null }; + case 'LOGIN_SUCCESS': + return { ...state, user: action.payload.user, loading: false }; + case 'LOGIN_FAILURE': + return { ...state, error: action.payload.error, loading: false }; + case 'LOGOUT': + return { ...state, user: null }; + default: + return state; + } +} + +// 4. Use in provider +function Provider({ children }) { + const [state, dispatch] = useReducer(reducer, initialState); + + const login = async credentials => { + dispatch({ type: 'LOGIN_START' }); + try { + const user = await authService.login(credentials); + dispatch({ type: 'LOGIN_SUCCESS', payload: { user } }); + } catch (error) { + dispatch({ type: 'LOGIN_FAILURE', payload: { error: error.message } }); + } + }; + + return ( + {children} + ); +} +``` + +## Performance Optimization + +### 1. Split Contexts by Update Frequency + +If you have state that updates frequently and state that doesn't, split them: + +```tsx +// Slow-changing state +const UserContext = createContext({ user: null }); + +// Fast-changing state +const UIContext = createContext({ theme: 'light' }); +``` + +### 2. Memoize Context Value + +Always memoize the context value to prevent unnecessary re-renders: + +```tsx +const value = useMemo( + () => ({ + state, + actions: { + login, + logout, + // ... + }, + }), + [state, login, logout] // Include all dependencies +); +``` + +### 3. Memoize Actions with useCallback + +Memoize action functions to keep context value stable: + +```tsx +const login = useCallback(async credentials => { + // ... +}, []); // Empty deps if no external dependencies + +const logout = useCallback(async () => { + // ... +}, []); +``` + +### 4. Split State and Actions + +For very large contexts, consider splitting state and actions: + +```tsx +// State context (updates frequently) +const StateContext = createContext(state); + +// Actions context (stable) +const ActionsContext = createContext(actions); +``` + +### 5. Use React.memo for Consumers + +Memoize components that consume context to prevent unnecessary re-renders: + +```tsx +const TodoItem = React.memo(({ todo }) => { + const { updateTodo } = useTodos(); + // ... +}); +``` + +## TypeScript Patterns + +### Strong Typing + +```tsx +// Define state interface +interface AuthState { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; +} + +// Define context value interface +interface AuthContextValue extends AuthState { + login: (credentials: LoginCredentials) => Promise; + logout: () => Promise; + clearError: () => void; +} + +// Create typed context +const AuthContext = createContext(undefined); +``` + +### Type-Safe Actions + +```tsx +// Use discriminated unions for actions +type AuthAction = + | { type: 'LOGIN_START' } + | { type: 'LOGIN_SUCCESS'; payload: { user: User; token: string } } + | { type: 'LOGIN_FAILURE'; payload: { error: string } } + | { type: 'LOGOUT' }; + +// Type-safe reducer +function authReducer(state: AuthState, action: AuthAction): AuthState { + // TypeScript knows the shape of each action + switch (action.type) { + case 'LOGIN_SUCCESS': + // action.payload is { user: User; token: string } + return { ...state, user: action.payload.user }; + // ... + } +} +``` + +## Custom Hooks for Context + +Always create custom hooks for context consumption: + +### Benefits + +- **Error handling** - Throws if used outside provider +- **Type safety** - Ensures context is defined +- **Clean API** - Simple import and usage +- **Consistency** - Same pattern across all contexts + +### Pattern + +```tsx +export function useAuth(): AuthContextValue { + const context = useContext(AuthContext); + + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} +``` + +### Usage + +```tsx +function Component() { + const { user, login, logout } = useAuth(); + // TypeScript knows all properties are defined +} +``` + +## Provider Composition + +### Nesting Providers + +```tsx +function RootLayout() { + return ( + + + + + + + + + + ); +} +``` + +### Provider Composition Helper + +For cleaner code with many providers: + +```tsx +function composeProviders(...providers) { + return ({ children }) => + providers.reduceRight( + (acc, Provider) => {acc}, + children + ); +} + +const AppProviders = composeProviders( + AuthProvider, + TodosProvider, + ThemeProvider +); + +function RootLayout() { + return ( + + + + ); +} +``` + +## Integration with Services + +Contexts should integrate with existing services, not replace them: + +### Pattern + +```tsx +// Context uses service, doesn't duplicate logic +export function AuthProvider({ children }) { + const [state, dispatch] = useReducer(authReducer, initialState); + + const login = useCallback(async (credentials: LoginCredentials) => { + dispatch({ type: 'LOGIN_START' }); + try { + // Use existing service + const response = await authService.login(credentials); + dispatch({ type: 'LOGIN_SUCCESS', payload: response }); + } catch (error) { + dispatch({ type: 'LOGIN_FAILURE', payload: { error: error.message } }); + } + }, []); + + // ... +} +``` + +### Benefits + +- **Separation of concerns** - Services handle API, context handles state +- **Reusability** - Services can be used outside context +- **Testability** - Services can be tested independently +- **Consistency** - Same service used everywhere + +## Common Pitfalls + +### 1. Creating Context Value on Every Render + +**❌ Bad:** + +```tsx + +``` + +**✅ Good:** + +```tsx +const value = useMemo(() => ({ state, setState }), [state]); + +``` + +### 2. Not Memoizing Actions + +**❌ Bad:** + +```tsx +const login = async credentials => { + /* ... */ +}; +const value = useMemo(() => ({ login }), [state]); +``` + +**✅ Good:** + +```tsx +const login = useCallback(async credentials => { + /* ... */ +}, []); +const value = useMemo(() => ({ login }), [login]); +``` + +### 3. Using Context for Everything + +**❌ Bad:** Context for local component state + +```tsx +const [isOpen, setIsOpen] = useState(false); +// Don't put this in context if only one component needs it +``` + +**✅ Good:** Context for shared state + +```tsx +// Only use context for state that needs to be shared +const { user } = useAuth(); // Shared across app +``` + +### 4. Not Handling Provider Missing + +**❌ Bad:** + +```tsx +const context = useContext(AuthContext); +// context might be undefined! +``` + +**✅ Good:** + +```tsx +function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +} +``` + +### 5. Putting Everything in One Context + +**❌ Bad:** + +```tsx +const AppContext = createContext({ + user: null, + todos: [], + theme: 'light', + // ... everything +}); +``` + +**✅ Good:** + +```tsx +// Split by domain +const AuthContext = createContext({...}); +const TodosContext = createContext({...}); +const ThemeContext = createContext({...}); +``` + +## When to Split Contexts + +### Split When: + +1. **Different update frequencies** - Auth updates rarely, UI updates frequently +2. **Different domains** - Auth, todos, theme are separate concerns +3. **Different consumers** - Not all components need all state +4. **Performance** - Large context causes unnecessary re-renders + +### Keep Together When: + +1. **Closely related** - State that always changes together +2. **Same consumers** - Components always need both pieces +3. **Simple state** - Small, simple state doesn't need splitting + +## Example: Complete Context Implementation + +See the following files for complete, production-ready examples: + +- `contexts/AuthContext.tsx` - Authentication context with useReducer +- `contexts/TodosContext.tsx` - Todos context with CRUD operations +- `contexts/index.ts` - Clean exports + +### Usage Example + +```tsx +// In a component +import { useAuth, useTodos } from '@/contexts'; + +function MyComponent() { + const { user, login, logout, isLoading } = useAuth(); + const { todos, fetchTodos, addTodo } = useTodos(); + + // Use context values and actions +} +``` + +## Summary + +React Context API is a powerful tool for state management when used correctly: + +- ✅ Split contexts by domain +- ✅ Use useReducer for complex state +- ✅ Memoize context values and actions +- ✅ Create custom hooks for consumption +- ✅ Integrate with existing services +- ✅ Handle errors and edge cases +- ✅ Optimize for performance + +When used with these patterns, Context API can handle most state management needs in React Native applications without external dependencies.