Skip to content

Commit af9dcc4

Browse files
makingclaude
andcommitted
Implement SWR for HTTP calls
- Added SWR package for data fetching - Refactored TodoList component to use SWR hooks - Implemented custom fetcher with error handling - Added data revalidation after mutations - Improved loading and error states 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent cfb3cc2 commit af9dcc4

File tree

3 files changed

+96
-55
lines changed

3 files changed

+96
-55
lines changed

todo-frontend/ui/package-lock.json

Lines changed: 30 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

todo-frontend/ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
},
1212
"dependencies": {
1313
"react": "^18.3.1",
14-
"react-dom": "^18.3.1"
14+
"react-dom": "^18.3.1",
15+
"swr": "^2.2.5"
1516
},
1617
"devDependencies": {
1718
"@eslint/js": "^9.13.0",

todo-frontend/ui/src/TodoList.tsx

Lines changed: 64 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useEffect, FormEvent } from 'react';
22
import { FaCheck, FaTrashAlt, FaPlus, FaSpinner } from 'react-icons/fa';
3+
import useSWR, { useSWRConfig } from 'swr';
34
import {
45
Container,
56
Header,
@@ -21,55 +22,63 @@ interface Todo {
2122
finished: boolean;
2223
}
2324

25+
interface UserData {
26+
name: string;
27+
csrfToken: string;
28+
}
29+
30+
// Custom fetcher function with error handling
31+
const fetcher = async (url: string) => {
32+
const response = await fetch(url);
33+
34+
if (!response.ok) {
35+
const error = new Error('An error occurred while fetching the data.');
36+
error.message = `Failed to fetch: ${response.status} ${response.statusText}`;
37+
throw error;
38+
}
39+
40+
return response.json();
41+
};
42+
2443
const TodoList = () => {
25-
const [todos, setTodos] = useState<Todo[]>([]);
26-
const [username, setUsername] = useState<string>('');
27-
const [csrfToken, setCsrfToken] = useState<string>('');
2844
const [newTodoTitle, setNewTodoTitle] = useState<string>('');
2945
const [hideCompleted, setHideCompleted] = useState<boolean>(false);
3046
const [error, setError] = useState<string | null>(null);
31-
const [loading, setLoading] = useState<boolean>(true);
3247
const [submitting, setSubmitting] = useState<boolean>(false);
48+
const { mutate } = useSWRConfig();
3349

34-
useEffect(() => {
35-
const fetchData = async () => {
36-
setLoading(true);
37-
try {
38-
// Fetch todos and username in parallel
39-
const [todosResponse, userResponse] = await Promise.all([
40-
fetch('/api/todos'),
41-
fetch('/whoami')
42-
]);
43-
44-
if (!todosResponse.ok) {
45-
throw new Error('Failed to fetch todos');
46-
}
47-
48-
if (!userResponse.ok) {
49-
throw new Error('Failed to fetch user data');
50-
}
51-
52-
const todosData: Todo[] = await todosResponse.json();
53-
const userData = await userResponse.json();
54-
55-
setTodos(todosData);
56-
setUsername(userData.name);
57-
setCsrfToken(userData.csrfToken);
58-
setError(null);
59-
} catch (error) {
60-
console.error('Error fetching data:', error);
61-
setError('Failed to load data. Please refresh the page.');
62-
} finally {
63-
setLoading(false);
64-
}
65-
};
50+
// Using SWR to fetch todos
51+
const { data: todos, error: todosError, isLoading: isTodosLoading } = useSWR<Todo[]>('/api/todos', fetcher, {
52+
revalidateOnFocus: true,
53+
onError: (err) => {
54+
console.error('Error fetching todos:', err);
55+
setError('Failed to load todos. Please refresh the page.');
56+
}
57+
});
58+
59+
// Using SWR to fetch user data
60+
const { data: userData, error: userError, isLoading: isUserLoading } = useSWR<UserData>('/whoami', fetcher, {
61+
revalidateOnFocus: false,
62+
onError: (err) => {
63+
console.error('Error fetching user data:', err);
64+
setError('Failed to load user data. Please refresh the page.');
65+
}
66+
});
6667

67-
fetchData();
68-
}, []);
68+
// Combined loading state
69+
const isLoading = isTodosLoading || isUserLoading;
70+
71+
// Make sure we show error from any source (local state or SWR errors)
72+
// We use a side effect to set the local error state from SWR errors
73+
useEffect(() => {
74+
if (todosError || userError) {
75+
setError('Failed to load data. Please refresh the page.');
76+
}
77+
}, [todosError, userError]);
6978

7079
const handleAddTodo = async (event: FormEvent) => {
7180
event.preventDefault();
72-
if (!newTodoTitle || submitting) return;
81+
if (!newTodoTitle || submitting || !userData) return;
7382

7483
setSubmitting(true);
7584
setError(null);
@@ -79,7 +88,7 @@ const TodoList = () => {
7988
method: 'POST',
8089
headers: {
8190
'Content-Type': 'application/json',
82-
'X-CSRF-TOKEN': csrfToken,
91+
'X-CSRF-TOKEN': userData.csrfToken,
8392
},
8493
body: JSON.stringify({ todoTitle: newTodoTitle }),
8594
});
@@ -88,8 +97,8 @@ const TodoList = () => {
8897
throw new Error('Failed to create todo');
8998
}
9099

91-
const newTodo: Todo = await response.json();
92-
setTodos(prevTodos => [...prevTodos, newTodo]);
100+
// Revalidate todos data after successful addition
101+
await mutate('/api/todos');
93102
setNewTodoTitle('');
94103
} catch (error) {
95104
console.error('Error creating todo:', error);
@@ -100,28 +109,32 @@ const TodoList = () => {
100109
};
101110

102111
const handleDeleteTodo = async (todoId: number) => {
112+
if (!userData) return;
103113
setError(null);
104114

105115
try {
106116
const response = await fetch(`/api/todos/${todoId}`, {
107117
method: 'DELETE',
108118
headers: {
109-
'X-CSRF-TOKEN': csrfToken,
119+
'X-CSRF-TOKEN': userData.csrfToken,
110120
},
111121
});
112122

113123
if (!response.ok) {
114124
throw new Error('Failed to delete todo');
115125
}
116126

117-
setTodos(prevTodos => prevTodos.filter(todo => todo.todoId !== todoId));
127+
// Revalidate todos data after successful deletion
128+
await mutate('/api/todos');
118129
} catch (error) {
119130
console.error('Error deleting todo:', error);
120131
setError('Failed to delete todo. Please try again.');
121132
}
122133
};
123134

124135
const handleToggleFinished = async (todoId: number) => {
136+
if (!todos || !userData) return;
137+
125138
const todo = todos.find(todo => todo.todoId === todoId);
126139
if (!todo) return;
127140

@@ -132,7 +145,7 @@ const TodoList = () => {
132145
method: 'PATCH',
133146
headers: {
134147
'Content-Type': 'application/json',
135-
'X-CSRF-TOKEN': csrfToken,
148+
'X-CSRF-TOKEN': userData.csrfToken,
136149
},
137150
body: JSON.stringify({ finished: !todo.finished }),
138151
});
@@ -141,10 +154,8 @@ const TodoList = () => {
141154
throw new Error('Failed to update todo');
142155
}
143156

144-
const updatedTodo: Todo = await response.json();
145-
setTodos(prevTodos =>
146-
prevTodos.map(t => (t.todoId === todoId ? updatedTodo : t))
147-
);
157+
// Revalidate todos data after successful update
158+
await mutate('/api/todos');
148159
} catch (error) {
149160
console.error('Error updating todo:', error);
150161
setError('Failed to update todo. Please try again.');
@@ -163,7 +174,7 @@ const TodoList = () => {
163174
}).format(date);
164175
};
165176

166-
if (loading) {
177+
if (isLoading) {
167178
return (
168179
<Container>
169180
<div className="flex flex-col items-center justify-center min-h-[50vh]">
@@ -178,7 +189,7 @@ const TodoList = () => {
178189
<Container>
179190
<Header>Todo List</Header>
180191

181-
{username && <WelcomeMessage username={username} />}
192+
{userData && <WelcomeMessage username={userData.name} />}
182193

183194
<div className="mb-8 bg-white p-6 rounded-lg shadow-md">
184195
<form
@@ -228,7 +239,7 @@ const TodoList = () => {
228239
</label>
229240
</div>
230241
<div className="text-sm text-gray-500">
231-
{todos.length} {todos.length === 1 ? 'task' : 'tasks'} total
242+
{todos?.length || 0} {todos?.length === 1 ? 'task' : 'tasks'} total
232243
</div>
233244
</div>
234245

@@ -238,7 +249,7 @@ const TodoList = () => {
238249
</div>
239250
)}
240251

241-
{todos.length === 0 ? (
252+
{!todos || todos.length === 0 ? (
242253
<div className="bg-white p-8 text-center rounded-lg shadow-md animate-fade-in">
243254
<p className="text-gray-500 mb-4">You don't have any tasks yet.</p>
244255
<p className="text-gray-400 text-sm">Add a new task to get started!</p>

0 commit comments

Comments
 (0)