- Created useSearch hook for search state and debouncing - Created useFilters hook for filter state management - Created useOptimisticUpdate hook for optimistic UI updates - Created useScrollPosition hook for scroll preservation - Updated SearchBar, GlobalSearch, and ListDetailView to use new hooks - Added comprehensive documentation for all hooks Validates: Requirements 6.4, 7.3, 7.4, 8.3 Task: 9. Implement client-side state management
209 lines
5.8 KiB
TypeScript
209 lines
5.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useCallback } from 'react'
|
|
|
|
interface UseScrollPositionOptions {
|
|
key: string
|
|
enabled?: boolean
|
|
debounceMs?: number
|
|
}
|
|
|
|
/**
|
|
* Custom hook for preserving scroll position across navigation
|
|
* Validates: Requirements 7.4
|
|
*/
|
|
export function useScrollPosition(options: UseScrollPositionOptions) {
|
|
const { key, enabled = true, debounceMs = 100 } = options
|
|
|
|
const scrollPositionRef = useRef<number>(0)
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
const isRestoringRef = useRef(false)
|
|
|
|
// Save scroll position to sessionStorage
|
|
const saveScrollPosition = useCallback((position: number) => {
|
|
if (!enabled) return
|
|
|
|
try {
|
|
sessionStorage.setItem(`scroll-${key}`, position.toString())
|
|
scrollPositionRef.current = position
|
|
} catch (error) {
|
|
console.warn('Failed to save scroll position:', error)
|
|
}
|
|
}, [key, enabled])
|
|
|
|
// Restore scroll position from sessionStorage
|
|
const restoreScrollPosition = useCallback(() => {
|
|
if (!enabled) return
|
|
|
|
try {
|
|
const savedPosition = sessionStorage.getItem(`scroll-${key}`)
|
|
if (savedPosition !== null) {
|
|
const position = parseInt(savedPosition, 10)
|
|
if (!isNaN(position)) {
|
|
isRestoringRef.current = true
|
|
window.scrollTo(0, position)
|
|
scrollPositionRef.current = position
|
|
|
|
// Reset flag after a short delay
|
|
setTimeout(() => {
|
|
isRestoringRef.current = false
|
|
}, 100)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to restore scroll position:', error)
|
|
}
|
|
}, [key, enabled])
|
|
|
|
// Clear saved scroll position
|
|
const clearScrollPosition = useCallback(() => {
|
|
try {
|
|
sessionStorage.removeItem(`scroll-${key}`)
|
|
scrollPositionRef.current = 0
|
|
} catch (error) {
|
|
console.warn('Failed to clear scroll position:', error)
|
|
}
|
|
}, [key])
|
|
|
|
// Handle scroll events with debouncing
|
|
useEffect(() => {
|
|
if (!enabled) return
|
|
|
|
const handleScroll = () => {
|
|
// Don't save position if we're currently restoring
|
|
if (isRestoringRef.current) return
|
|
|
|
// Clear existing timeout
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
}
|
|
|
|
// Debounce the save operation
|
|
timeoutRef.current = setTimeout(() => {
|
|
const position = window.scrollY || window.pageYOffset
|
|
saveScrollPosition(position)
|
|
}, debounceMs)
|
|
}
|
|
|
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
|
|
|
return () => {
|
|
window.removeEventListener('scroll', handleScroll)
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
}
|
|
}
|
|
}, [enabled, debounceMs, saveScrollPosition])
|
|
|
|
// Restore scroll position on mount
|
|
useEffect(() => {
|
|
if (enabled) {
|
|
// Use requestAnimationFrame to ensure DOM is ready
|
|
requestAnimationFrame(() => {
|
|
restoreScrollPosition()
|
|
})
|
|
}
|
|
}, [enabled, restoreScrollPosition])
|
|
|
|
return {
|
|
saveScrollPosition,
|
|
restoreScrollPosition,
|
|
clearScrollPosition,
|
|
currentPosition: scrollPositionRef.current
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook for preserving scroll position within a specific container element
|
|
*/
|
|
export function useContainerScrollPosition(options: UseScrollPositionOptions) {
|
|
const { key, enabled = true, debounceMs = 100 } = options
|
|
|
|
const containerRef = useRef<HTMLElement | null>(null)
|
|
const scrollPositionRef = useRef<number>(0)
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
const isRestoringRef = useRef(false)
|
|
|
|
// Save scroll position
|
|
const saveScrollPosition = useCallback((position: number) => {
|
|
if (!enabled) return
|
|
|
|
try {
|
|
sessionStorage.setItem(`container-scroll-${key}`, position.toString())
|
|
scrollPositionRef.current = position
|
|
} catch (error) {
|
|
console.warn('Failed to save container scroll position:', error)
|
|
}
|
|
}, [key, enabled])
|
|
|
|
// Restore scroll position
|
|
const restoreScrollPosition = useCallback(() => {
|
|
if (!enabled || !containerRef.current) return
|
|
|
|
try {
|
|
const savedPosition = sessionStorage.getItem(`container-scroll-${key}`)
|
|
if (savedPosition !== null) {
|
|
const position = parseInt(savedPosition, 10)
|
|
if (!isNaN(position)) {
|
|
isRestoringRef.current = true
|
|
containerRef.current.scrollTop = position
|
|
scrollPositionRef.current = position
|
|
|
|
setTimeout(() => {
|
|
isRestoringRef.current = false
|
|
}, 100)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to restore container scroll position:', error)
|
|
}
|
|
}, [key, enabled])
|
|
|
|
// Clear saved scroll position
|
|
const clearScrollPosition = useCallback(() => {
|
|
try {
|
|
sessionStorage.removeItem(`container-scroll-${key}`)
|
|
scrollPositionRef.current = 0
|
|
} catch (error) {
|
|
console.warn('Failed to clear container scroll position:', error)
|
|
}
|
|
}, [key])
|
|
|
|
// Set up scroll listener on container
|
|
useEffect(() => {
|
|
if (!enabled || !containerRef.current) return
|
|
|
|
const container = containerRef.current
|
|
|
|
const handleScroll = () => {
|
|
if (isRestoringRef.current) return
|
|
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
}
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
const position = container.scrollTop
|
|
saveScrollPosition(position)
|
|
}, debounceMs)
|
|
}
|
|
|
|
container.addEventListener('scroll', handleScroll, { passive: true })
|
|
|
|
return () => {
|
|
container.removeEventListener('scroll', handleScroll)
|
|
if (timeoutRef.current) {
|
|
clearTimeout(timeoutRef.current)
|
|
}
|
|
}
|
|
}, [enabled, debounceMs, saveScrollPosition])
|
|
|
|
return {
|
|
containerRef,
|
|
saveScrollPosition,
|
|
restoreScrollPosition,
|
|
clearScrollPosition,
|
|
currentPosition: scrollPositionRef.current
|
|
}
|
|
}
|