Skip to content

Commit 79185c1

Browse files
authored
Fix race on opening task detail page (#174)
* Fix race in tab creation by waiting for initial task poll * Fix useTask polling by removing error from deps and final check * Fix stale error closure in useTask by tracking a local error flag --------- Co-authored-by: Chris Tate <ctate@users.noreply.github.com>
1 parent 429826f commit 79185c1

File tree

1 file changed

+50
-12
lines changed

1 file changed

+50
-12
lines changed

lib/hooks/use-task.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,85 @@
11
'use client'
22

3-
import { useState, useEffect, useCallback } from 'react'
3+
import { useState, useEffect, useCallback, useRef } from 'react'
44
import { Task } from '@/lib/db/schema'
55

66
export function useTask(taskId: string) {
77
const [task, setTask] = useState<Task | null>(null)
88
const [isLoading, setIsLoading] = useState(true)
99
const [error, setError] = useState<string | null>(null)
10+
const attemptCountRef = useRef(0)
11+
const hasFoundTaskRef = useRef(false)
1012

1113
const fetchTask = useCallback(async () => {
14+
let errorOccurred = false
1215
try {
1316
const response = await fetch(`/api/tasks/${taskId}`)
1417
if (response.ok) {
1518
const data = await response.json()
1619
setTask(data.task)
1720
setError(null)
21+
hasFoundTaskRef.current = true
1822
} 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
2132
} else {
2233
setError('Failed to fetch task')
34+
errorOccurred = true
2335
}
2436
} catch (err) {
2537
console.error('Error fetching task:', err)
2638
setError('Failed to fetch task')
39+
errorOccurred = true
2740
} 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+
}
2945
}
3046
}, [taskId])
3147

32-
// Initial fetch
48+
// Initial fetch with retry logic
3349
useEffect(() => {
50+
attemptCountRef.current = 0
51+
hasFoundTaskRef.current = false
52+
setIsLoading(true)
53+
setError(null)
54+
55+
// Fetch immediately
3456
fetchTask()
35-
}, [fetchTask])
3657

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
3873
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)
4279

43-
return () => clearInterval(interval)
44-
}, [fetchTask])
80+
return () => clearInterval(interval)
81+
}
82+
}, [fetchTask, isLoading])
4583

4684
return { task, isLoading, error, refetch: fetchTask }
4785
}

0 commit comments

Comments
 (0)