iOS浏览器下useEffect依赖值异常:IntersectionObserver分页问题
在React项目中,我使用IntersectionObserver监听商品列表底部的div,当元素可见时触发数据加载,数据存储在Redux中。点击分类切换函数switchMainCategory()时,activeCategoryId、products和page应更新并触发useEffect调用loadMoreProducts()。该逻辑在桌面及安卓浏览器正常,但iOS浏览器中,loadMoreProducts()会传入旧的activeCategoryId,且page未被设为null而是递增,请问这一iOS特有问题的原因是什么?
核心组件代码
export default function Home() { const [isLoadingMore, setIsLoadingMore] = useState(false) const { activeCategoryId, products, page } = useSelector((state) => state.pages.homePage) const observerTarget = useRef(null) useEffect(() => { const loadMoreProducts = async () => { if (isLoadingMore || page === '') { return } setIsLoadingMore(true) const response = await productService.getProducts({ category: activeCategoryId, page: page, }) if (response.isOk && response.data) { dispatch( PagesActions.setHomePage({ products: [...products, ...response.data.results], page: response.data.next || '', }) ) // Stop observing if there are no more products to load if (response.data.results.length === 0) { observer.unobserve(observerTarget.current) } } setIsLoadingMore(false) } const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadMoreProducts() } }, { threshold: 1 } ) if (observerTarget.current) { observer.observe(observerTarget.current) } return () => { if (observerTarget.current) { observer.unobserve(observerTarget.current) } } }, [activeCategoryId, dispatch, page, observerTarget, products, isLoadingMore]) const switchMainCategory = (category) => { dispatch( PagesActions.setHomePage({ products: [], page: null, activeCategoryId: category?.code }) ) } return ( // here we have a list of categories, list of products and observerTarget div ) }
问题原因
1. Safari中IntersectionObserver的闭包快照特性
iOS Safari的IntersectionObserver回调函数会固定捕获闭包创建时的变量值,而非动态获取最新状态。当切换分类触发useEffect重新执行时,虽然创建了新的loadMoreProducts和Observer实例,但旧的Observer可能因为浏览器回收延迟未被彻底销毁,导致旧回调仍被触发,此时它引用的是切换前的activeCategoryId和page。
2. 状态更新与Observer触发的竞态条件
切换分类时,你通过Redux将page设为null,但iOS中可能存在时间差:旧Observer还在监听,新的useEffect销毁旧Observer之前,底部div恰好处于交叉状态,触发了旧的loadMoreProducts。旧函数中的page仍是切换前的数值(并非null),因此会用旧分类ID和旧page请求数据,导致page被错误递增。
3. page判断逻辑的不一致
loadMoreProducts中仅判断page === ''时终止执行,但切换分类时Redux把page设为null,null === ''不成立,所以旧的loadMoreProducts依然会执行请求。这个逻辑漏洞在iOS的竞态场景下被放大,加剧了问题。
内容的提问来源于stack exchange,提问作者Armin Dervić

