mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(journey): stop the load-more observer rebuilding itself on every render
ScrollTrigger kept onVisible in its effect dependencies, and both callers hand in a fresh arrow on every render: the picker its loadMorePhotos, the gallery its setVisibleCount step. So every unrelated re-render, a photo being ticked included, disconnected the IntersectionObserver and observed again, and each observe() replays the initial callback. The callback moves into a ref. loading stays a dependency, because re-observing once a page has landed is the only thing that still produces a callback when the sentinel never left the 200px margin, so folding that into a ref too would leave paging dead after the first page. FE-COMP-JSCROLLTRIG-009 pins that, 007 and 008 pin the rebuild itself; both are red on the old component. Markup, classes and the signature are untouched.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-COMP-JSCROLLTRIG-001 to FE-COMP-JSCROLLTRIG-006
|
||||
// FE-COMP-JSCROLLTRIG-001 to FE-COMP-JSCROLLTRIG-009
|
||||
|
||||
import { render } from '../../../tests/helpers/render'
|
||||
import { resetAllStores } from '../../../tests/helpers/store'
|
||||
@@ -87,4 +87,44 @@ describe('ScrollTrigger', () => {
|
||||
unmount()
|
||||
expect(observers[0].disconnect).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-COMP-JSCROLLTRIG-007: a fresh callback identity alone does not rebuild the observer', () => {
|
||||
// Both callers hand in a new arrow every render; rebuilding for that churn
|
||||
// replays the initial callback and pulls pages nobody asked for.
|
||||
const { rerender } = render(<ScrollTrigger onVisible={vi.fn()} loading={false} />)
|
||||
expect(observers).toHaveLength(1)
|
||||
|
||||
rerender(<ScrollTrigger onVisible={vi.fn()} loading={false} />)
|
||||
rerender(<ScrollTrigger onVisible={vi.fn()} loading={false} />)
|
||||
|
||||
expect(observers).toHaveLength(1)
|
||||
expect(observers[0].disconnect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('FE-COMP-JSCROLLTRIG-008: the surviving observer calls the newest callback, not the one it was built with', () => {
|
||||
const first = vi.fn()
|
||||
const second = vi.fn()
|
||||
const { rerender } = render(<ScrollTrigger onVisible={first} loading={false} />)
|
||||
rerender(<ScrollTrigger onVisible={second} loading={false} />)
|
||||
|
||||
intersect(observers[0], true)
|
||||
|
||||
expect(first).not.toHaveBeenCalled()
|
||||
expect(second).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('FE-COMP-JSCROLLTRIG-009: a settled page really re-observes the sentinel', () => {
|
||||
// A browser observer only fires on a threshold crossing plus once per
|
||||
// observe(). A sentinel that never leaves the 200px margin therefore only
|
||||
// gets another callback because the loading flip rebuilds the observer, so
|
||||
// loading must stay a dependency rather than moving into a ref as well.
|
||||
const onVisible = vi.fn()
|
||||
const { rerender } = render(<ScrollTrigger onVisible={onVisible} loading />)
|
||||
expect(observers).toHaveLength(1)
|
||||
|
||||
rerender(<ScrollTrigger onVisible={onVisible} loading={false} />)
|
||||
|
||||
expect(observers).toHaveLength(2)
|
||||
expect(observers[0].disconnect).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,13 +2,24 @@ import { useEffect, useRef } from 'react'
|
||||
|
||||
export function ScrollTrigger({ onVisible, loading }: { onVisible: () => void; loading: boolean }) {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
// Both callers hand in a fresh arrow on every render — the picker its
|
||||
// loadMorePhotos, the gallery its setVisibleCount step. With onVisible in the
|
||||
// effect deps, every unrelated re-render tore the observer down and observed
|
||||
// again, and each observe() replays the initial callback. The ref keeps the
|
||||
// observer out of that churn.
|
||||
//
|
||||
// loading stays a dependency on purpose: re-observing after a page has landed
|
||||
// is the only thing that still produces a callback when the sentinel never
|
||||
// left the 200px margin.
|
||||
const onVisibleRef = useRef(onVisible)
|
||||
onVisibleRef.current = onVisible
|
||||
useEffect(() => {
|
||||
const el = ref.current
|
||||
if (!el) return
|
||||
const obs = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !loading) onVisible() }, { rootMargin: '200px' })
|
||||
const obs = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !loading) onVisibleRef.current() }, { rootMargin: '200px' })
|
||||
obs.observe(el)
|
||||
return () => obs.disconnect()
|
||||
}, [onVisible, loading])
|
||||
}, [loading])
|
||||
return (
|
||||
<div ref={ref} className="flex justify-center py-4 mt-2">
|
||||
<div className="w-5 h-5 border-2 border-zinc-300 border-t-zinc-900 dark:border-zinc-600 dark:border-t-white rounded-full animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user