fix: resolve race conditions in Bible reader SEO URLs

- Add request cancellation with AbortController for books and verses fetching
- Implement debouncing (300ms) for version changes to prevent rapid API calls
- Fix conflicting useEffect hooks that caused incorrect Bible text display
- Add enhanced error handling and loading state management
- Prevent empty states when switching Bible versions quickly
- Improve user experience with immediate loading feedback

Fixes issue where selecting Bible versions rapidly would cause:
- 500 API errors from concurrent requests
- Wrong Bible text being displayed
- Empty content states
- Poor user experience during filtering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-29 19:46:19 +00:00
parent 61a5180e2f
commit b337b82fde

View File

@@ -179,6 +179,9 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
const [versionsLoading, setVersionsLoading] = useState(true)
const [showAllVersions, setShowAllVersions] = useState(false)
// Debounced version state to prevent rapid API calls
const [debouncedVersion, setDebouncedVersion] = useState<string>('')
// UI state
const [settingsOpen, setSettingsOpen] = useState(false)
const [preferences, setPreferences] = useState<ReadingPreferences>(defaultPreferences)
@@ -425,29 +428,65 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
router.replace(newUrl, { scroll: false })
}, [locale, selectedVersion, selectedBook, selectedChapter, router, versions, books])
// Fetch books when version changes
// Debounce version changes to prevent rapid API calls
useEffect(() => {
if (selectedVersion) {
const timer = setTimeout(() => {
setDebouncedVersion(selectedVersion)
}, 300) // 300ms delay
return () => clearTimeout(timer)
}, [selectedVersion])
// Show loading immediately when selectedVersion changes (before debounce)
useEffect(() => {
if (selectedVersion && selectedVersion !== debouncedVersion) {
setLoading(true)
fetch(`/api/bible/books?locale=${locale}&version=${selectedVersion}`)
.then(res => res.json())
}
}, [selectedVersion, debouncedVersion])
// Fetch books when debounced version changes
useEffect(() => {
if (debouncedVersion) {
// Create an AbortController for this request
const abortController = new AbortController()
fetch(`/api/bible/books?locale=${locale}&version=${debouncedVersion}`, {
signal: abortController.signal
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return res.json()
})
.then(data => {
// Only update state if the request wasn't aborted
if (!abortController.signal.aborted) {
setBooks(data.books || [])
if (data.books && data.books.length > 0) {
if (data.books && data.books.length > 0 && !initialBook) {
setSelectedBook(data.books[0].id)
}
setLoading(false)
}
})
.catch(err => {
// Only log error if it's not an abort error
if (!abortController.signal.aborted) {
console.error('Error fetching books:', err)
setLoading(false)
})
}
}, [locale, selectedVersion])
})
// Handle URL parameters
// Cleanup function to abort the request if component unmounts or dependencies change
return () => {
abortController.abort()
}
}
}, [locale, debouncedVersion, initialBook])
// Handle URL parameters (only when not using initial props from SEO URLs)
useEffect(() => {
if (books.length > 0 && versions.length > 0) {
if (books.length > 0 && versions.length > 0 && !initialVersion && !initialBook && !initialChapter) {
const bookParam = searchParams.get('book')
const chapterParam = searchParams.get('chapter')
const verseParam = searchParams.get('verse')
@@ -481,7 +520,7 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
}
}
}
}, [books, versions, searchParams])
}, [books, versions, searchParams, initialVersion, initialBook, initialChapter])
// Fetch verses when book/chapter changes
useEffect(() => {
@@ -491,9 +530,21 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
// Store scroll position to prevent jumping
const scrollPosition = window.pageYOffset
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`)
.then(res => res.json())
// Create an AbortController for this request
const abortController = new AbortController()
fetch(`/api/bible/verses?bookId=${selectedBook}&chapter=${selectedChapter}`, {
signal: abortController.signal
})
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`)
}
return res.json()
})
.then(data => {
// Only update state if the request wasn't aborted
if (!abortController.signal.aborted) {
const newVerses = data.verses || []
// Store previous verses before updating
@@ -516,11 +567,20 @@ export default function BibleReaderNew({ initialVersion, initialBook, initialCha
}
}, 50) // Small delay for smoother transition
})
}
})
.catch(err => {
// Only log error if it's not an abort error
if (!abortController.signal.aborted) {
console.error('Error fetching verses:', err)
setLoading(false)
}
})
// Cleanup function to abort the request if component unmounts or dependencies change
return () => {
abortController.abort()
}
}
}, [selectedBook, selectedChapter])