11import { useState , useEffect , FormEvent } from 'react' ;
22import { FaCheck , FaTrashAlt , FaPlus , FaSpinner } from 'react-icons/fa' ;
3+ import useSWR , { useSWRConfig } from 'swr' ;
34import {
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+
2443const 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