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:
@@ -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())
|
||||
.then(data => {
|
||||
setBooks(data.books || [])
|
||||
if (data.books && data.books.length > 0) {
|
||||
setSelectedBook(data.books[0].id)
|
||||
}
|
||||
}, [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 && !initialBook) {
|
||||
setSelectedBook(data.books[0].id)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching books:', err)
|
||||
setLoading(false)
|
||||
// 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,36 +530,57 @@ 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 => {
|
||||
const newVerses = data.verses || []
|
||||
// Only update state if the request wasn't aborted
|
||||
if (!abortController.signal.aborted) {
|
||||
const newVerses = data.verses || []
|
||||
|
||||
// Store previous verses before updating
|
||||
setPreviousVerses(verses)
|
||||
// Store previous verses before updating
|
||||
setPreviousVerses(verses)
|
||||
|
||||
// Use requestAnimationFrame to ensure smooth transition
|
||||
requestAnimationFrame(() => {
|
||||
setVerses(newVerses)
|
||||
// Use requestAnimationFrame to ensure smooth transition
|
||||
requestAnimationFrame(() => {
|
||||
setVerses(newVerses)
|
||||
|
||||
// Small delay to allow content to render before removing loading state
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setPreviousVerses([]) // Clear previous content after transition
|
||||
// Small delay to allow content to render before removing loading state
|
||||
setTimeout(() => {
|
||||
setLoading(false)
|
||||
setPreviousVerses([]) // Clear previous content after transition
|
||||
|
||||
// Restore scroll position if we're not navigating to a specific verse
|
||||
const urlVerse = new URLSearchParams(window.location.search).get('verse')
|
||||
if (!urlVerse) {
|
||||
// Maintain scroll position for better UX
|
||||
window.scrollTo({ top: Math.min(scrollPosition, document.body.scrollHeight), behavior: 'auto' })
|
||||
}
|
||||
}, 50) // Small delay for smoother transition
|
||||
})
|
||||
// Restore scroll position if we're not navigating to a specific verse
|
||||
const urlVerse = new URLSearchParams(window.location.search).get('verse')
|
||||
if (!urlVerse) {
|
||||
// Maintain scroll position for better UX
|
||||
window.scrollTo({ top: Math.min(scrollPosition, document.body.scrollHeight), behavior: 'auto' })
|
||||
}
|
||||
}, 50) // Small delay for smoother transition
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error fetching verses:', err)
|
||||
setLoading(false)
|
||||
// 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])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user