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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ Atomic state management with fine-grained reactivity. Excellent for component-le
- Great performance
- Flexible architecture

See [State Management Guide](docs/state-management.md) for detailed Jotai documentation.

### How to Use

To try out any of these state management solutions:
Expand Down
10 changes: 8 additions & 2 deletions app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
Snackbar,
} from 'react-native-paper';
import { router } from 'expo-router';
import { authService } from '@/services/auth';
import { useSetAtom } from 'jotai';
import { loginAtom } from '@/atoms';

export default function LoginScreen() {
const theme = useTheme();
Expand All @@ -19,6 +20,10 @@ export default function LoginScreen() {
const [error, setError] = useState<string | null>(null);
const [snackbarVisible, setSnackbarVisible] = useState(false);

// Use Jotai atoms: useSetAtom for write-only access to loginAtom
// This demonstrates Jotai's atomic state approach
const login = useSetAtom(loginAtom);

const handleLogin = async () => {
if (!email || !password) {
setError('Please fill in all fields');
Expand All @@ -30,7 +35,8 @@ export default function LoginScreen() {
setError(null);

try {
await authService.login({ email, password });
// Call login atom - it will update userAtom and tokenAtom atomically
await login({ email, password });
// Navigate to main app after successful login
router.replace('/(tabs)');
} catch (err) {
Expand Down
67 changes: 41 additions & 26 deletions app/(tabs)/explore.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
/**
* Explore Screen
* Example screen demonstrating API integration with loading, error, and success states
* Uses the useFetch hook for simplified data fetching
* Example screen demonstrating Jotai state management
* Uses Jotai atoms for todos state with loading, error, and success states
* Demonstrates useAtomValue for read-only access and useSetAtom for actions
*/
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 { useAtomValue, useSetAtom } from 'jotai';
import {
todosAtom,
todosLoadingAtom,
todosErrorAtom,
fetchTodosAtom,
} from '@/atoms';
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 Jotai atoms: useAtomValue for read-only access
// This demonstrates Jotai's atomic state approach - components only re-render
// when the specific atoms they subscribe to change
const todos = useAtomValue(todosAtom);
const loading = useAtomValue(todosLoadingAtom);
const error = useAtomValue(todosErrorAtom);

// useSetAtom for write-only access to fetchTodosAtom
const fetchTodos = useSetAtom(fetchTodosAtom);

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

// Limit to 10 todos for demo display
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 Down Expand Up @@ -58,7 +70,7 @@ export default function ExploreScreen() {
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={refetch}
onPress={() => fetchTodos()}
disabled={loading}
icon="refresh"
>
Expand Down Expand Up @@ -92,12 +104,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})
</Text>
{todos.map(todo => (
{displayedTodos.map(todo => (
<Card key={todo.id} style={styles.todoCard}>
<Card.Content>
<View style={styles.todoHeader}>
Expand Down Expand Up @@ -132,11 +144,14 @@ export default function ExploreScreen() {
)}

{/* Empty State */}
{!loading && !error && todos && todos.length === 0 && (
<View style={styles.centerContainer}>
<Text variant="bodyMedium">No todos found</Text>
</View>
)}
{!loading &&
!error &&
displayedTodos &&
displayedTodos.length === 0 && (
<View style={styles.centerContainer}>
<Text variant="bodyMedium">No todos found</Text>
</View>
)}
</ScrollView>
</SafeAreaView>
);
Expand Down
9 changes: 6 additions & 3 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ import { useColorScheme } from 'react-native';
import { getTheme } from '@/constants/Theme';
import { Stack } from 'expo-router';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { Provider as JotaiProvider } from 'jotai';

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

return (
<ErrorBoundary>
<PaperProvider theme={theme}>
<Stack screenOptions={{ headerShown: false }} />
</PaperProvider>
<JotaiProvider>
<PaperProvider theme={theme}>
<Stack screenOptions={{ headerShown: false }} />
</PaperProvider>
</JotaiProvider>
</ErrorBoundary>
);
}
64 changes: 64 additions & 0 deletions atoms/authAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Auth Atoms
* Jotai atoms for authentication state management
* Demonstrates primitive atoms, derived atoms, and async write atoms
*/
import { atom } from 'jotai';
import { authService, type AuthResponse } from '@/services/auth';

/**
* Primitive atom: User data
* Stores the current authenticated user information
*/
export const userAtom = atom<AuthResponse['user'] | null>(null);

/**
* Primitive atom: Auth token
* Stores the authentication token
*/
export const tokenAtom = atom<string | null>(null);

/**
* Derived atom: Is authenticated
* Computed from tokenAtom - automatically updates when token changes
* This demonstrates Jotai's atomic composition approach
*/
export const isAuthenticatedAtom = atom(get => {
const token = get(tokenAtom);
return token !== null && token.length > 0;
});

/**
* Async write atom: Login
* Handles login operation and updates both userAtom and tokenAtom
* This demonstrates Jotai's async atom pattern
*/
export const loginAtom = atom(
null,
async (get, set, credentials: { email: string; password: string }) => {
const response = await authService.login(credentials);
// Update both atoms atomically
set(userAtom, response.user);
set(tokenAtom, response.token);
return response;
}
);

/**
* Write atom: Logout
* Action atom that clears auth state
* This demonstrates Jotai's write-only atom pattern
*/
export const logoutAtom = atom(null, async (get, set) => {
try {
await authService.logout();
// Clear both atoms atomically
set(userAtom, null);
set(tokenAtom, null);
} catch (error) {
// Log error but still clear local state
console.error('Logout error:', error);
set(userAtom, null);
set(tokenAtom, null);
}
});
28 changes: 28 additions & 0 deletions atoms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Atoms Index
* Central export point for all Jotai atoms
* Grouped by feature for better organization
*/

// Auth atoms
export {
userAtom,
tokenAtom,
isAuthenticatedAtom,
loginAtom,
logoutAtom,
} from './authAtoms';

// Todos atoms
export {
todosAtom,
todosLoadingAtom,
todosErrorAtom,
fetchTodosAtom,
addTodoAtom,
toggleTodoAtom,
removeTodoAtom,
filteredTodosAtom,
completedTodosCountAtom,
totalTodosCountAtom,
} from './todosAtoms';
114 changes: 114 additions & 0 deletions atoms/todosAtoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Todos Atoms
* Jotai atoms for todos state management
* Demonstrates array atoms, loading/error states, async atoms, and derived atoms
*/
import { atom } from 'jotai';
import { todosApi } from '@/services/api';
import type { Todo } from '@/types/api';

/**
* Primitive atom: Todos array
* Stores the list of todos
*/
export const todosAtom = atom<Todo[]>([]);

/**
* Primitive atom: Loading state
* Tracks whether a fetch operation is in progress
*/
export const todosLoadingAtom = atom<boolean>(false);

/**
* Primitive atom: Error state
* Stores error message if fetch fails
*/
export const todosErrorAtom = atom<string | null>(null);

/**
* Async write atom: Fetch todos
* Handles fetching todos from API and updates todosAtom, loadingAtom, and errorAtom
* This demonstrates Jotai's async write atom pattern with multiple state updates
*/
export const fetchTodosAtom = atom(null, async (get, set) => {
set(todosLoadingAtom, true);
set(todosErrorAtom, null);

try {
const todos = await todosApi.getAll();
set(todosAtom, todos);
set(todosLoadingAtom, false);
return todos;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to fetch todos';
set(todosErrorAtom, errorMessage);
set(todosLoadingAtom, false);
throw error;
}
});

/**
* Write atom: Add todo
* Adds a new todo to the todos array
* This demonstrates updating array atoms
*/
export const addTodoAtom = atom(null, (get, set, todo: Todo) => {
const currentTodos = get(todosAtom);
set(todosAtom, [...currentTodos, todo]);
});

/**
* Write atom: Toggle todo completion
* Updates a todo's completed status
*/
export const toggleTodoAtom = atom(null, (get, set, todoId: number) => {
const currentTodos = get(todosAtom);
set(
todosAtom,
currentTodos.map(todo =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
)
);
});

/**
* Write atom: Remove todo
* Removes a todo from the array
*/
export const removeTodoAtom = atom(null, (get, set, todoId: number) => {
const currentTodos = get(todosAtom);
set(
todosAtom,
currentTodos.filter(todo => todo.id !== todoId)
);
});

/**
* Derived atom: Filtered todos
* Computed from todosAtom - automatically updates when todos change
* This demonstrates Jotai's derived atom pattern
*/
export const filteredTodosAtom = atom(get => {
const todos = get(todosAtom);
// Example: filter completed todos
return todos.filter(todo => !todo.completed);
});

/**
* Derived atom: Completed todos count
* Computed from todosAtom
*/
export const completedTodosCountAtom = atom(get => {
const todos = get(todosAtom);
return todos.filter(todo => todo.completed).length;
});

/**
* Derived atom: Total todos count
* Computed from todosAtom
*/
export const totalTodosCountAtom = atom(get => {
const todos = get(todosAtom);
return todos.length;
});
Loading