|
1 | 1 | 'use client' |
2 | 2 |
|
3 | | -import { useState, useEffect, useCallback } from 'react' |
| 3 | +import { useState, useEffect, useCallback, useRef } from 'react' |
4 | 4 | import { Task } from '@/lib/db/schema' |
5 | 5 |
|
6 | 6 | export function useTask(taskId: string) { |
7 | 7 | const [task, setTask] = useState<Task | null>(null) |
8 | 8 | const [isLoading, setIsLoading] = useState(true) |
9 | 9 | const [error, setError] = useState<string | null>(null) |
| 10 | + const attemptCountRef = useRef(0) |
| 11 | + const hasFoundTaskRef = useRef(false) |
10 | 12 |
|
11 | 13 | const fetchTask = useCallback(async () => { |
| 14 | + let errorOccurred = false |
12 | 15 | try { |
13 | 16 | const response = await fetch(`/api/tasks/${taskId}`) |
14 | 17 | if (response.ok) { |
15 | 18 | const data = await response.json() |
16 | 19 | setTask(data.task) |
17 | 20 | setError(null) |
| 21 | + hasFoundTaskRef.current = true |
18 | 22 | } else if (response.status === 404) { |
19 | | - setError('Task not found') |
20 | | - setTask(null) |
| 23 | + // Only set error after multiple failed attempts (to handle race condition on task creation) |
| 24 | + // Wait for at least 3 attempts (up to ~6 seconds) before showing "Task not found" |
| 25 | + attemptCountRef.current += 1 |
| 26 | + if (attemptCountRef.current >= 3 || hasFoundTaskRef.current) { |
| 27 | + setError('Task not found') |
| 28 | + setTask(null) |
| 29 | + errorOccurred = true |
| 30 | + } |
| 31 | + // If we haven't hit the attempt threshold yet, keep loading state |
21 | 32 | } else { |
22 | 33 | setError('Failed to fetch task') |
| 34 | + errorOccurred = true |
23 | 35 | } |
24 | 36 | } catch (err) { |
25 | 37 | console.error('Error fetching task:', err) |
26 | 38 | setError('Failed to fetch task') |
| 39 | + errorOccurred = true |
27 | 40 | } finally { |
28 | | - setIsLoading(false) |
| 41 | + // Only stop loading after we've either found the task or exceeded attempt threshold |
| 42 | + if (hasFoundTaskRef.current || attemptCountRef.current >= 3 || errorOccurred) { |
| 43 | + setIsLoading(false) |
| 44 | + } |
29 | 45 | } |
30 | 46 | }, [taskId]) |
31 | 47 |
|
32 | | - // Initial fetch |
| 48 | + // Initial fetch with retry logic |
33 | 49 | useEffect(() => { |
| 50 | + attemptCountRef.current = 0 |
| 51 | + hasFoundTaskRef.current = false |
| 52 | + setIsLoading(true) |
| 53 | + setError(null) |
| 54 | + |
| 55 | + // Fetch immediately |
34 | 56 | fetchTask() |
35 | | - }, [fetchTask]) |
36 | 57 |
|
37 | | - // Poll for updates every 5 seconds |
| 58 | + // If task isn't found on first try, retry more aggressively initially |
| 59 | + // This handles the race condition where we navigate to the task page before the DB insert completes |
| 60 | + const retryInterval = setInterval(() => { |
| 61 | + if (!hasFoundTaskRef.current && attemptCountRef.current < 3) { |
| 62 | + fetchTask() |
| 63 | + } else { |
| 64 | + clearInterval(retryInterval) |
| 65 | + } |
| 66 | + }, 2000) // Check every 2 seconds for the first few attempts |
| 67 | + |
| 68 | + return () => clearInterval(retryInterval) |
| 69 | + // eslint-disable-next-line react-hooks/exhaustive-deps |
| 70 | + }, [taskId]) // fetchTask is intentionally not in deps to avoid recreating interval on every fetchTask change |
| 71 | + |
| 72 | + // Poll for updates every 5 seconds after initial load |
38 | 73 | useEffect(() => { |
39 | | - const interval = setInterval(() => { |
40 | | - fetchTask() |
41 | | - }, 5000) |
| 74 | + // Only start polling after we've found the task or given up |
| 75 | + if (!isLoading) { |
| 76 | + const interval = setInterval(() => { |
| 77 | + fetchTask() |
| 78 | + }, 5000) |
42 | 79 |
|
43 | | - return () => clearInterval(interval) |
44 | | - }, [fetchTask]) |
| 80 | + return () => clearInterval(interval) |
| 81 | + } |
| 82 | + }, [fetchTask, isLoading]) |
45 | 83 |
|
46 | 84 | return { task, isLoading, error, refetch: fetchTask } |
47 | 85 | } |
0 commit comments