-
Notifications
You must be signed in to change notification settings - Fork 190
Description
Description
When multiple useInView({ triggerOnce: true }) hooks are merged on the same element, only some callbacks are executed. This seems to happen because the forEach loop in observe.ts is iterating over an array that is being modified during iteration via splice.
Reproduction
Add this test to the end of src/__tests__/useInView.test.tsx:
test("should trigger all hooks when using triggerOnce with merged refs", () => {
const MultipleHooksWithTriggerOnce = () => {
const { ref: ref1, inView: inView1 } = useInView({ triggerOnce: true });
const { ref: ref2, inView: inView2 } = useInView({ triggerOnce: true });
const { ref: ref3, inView: inView3 } = useInView({ triggerOnce: true });
const setRefs = useCallback(
(node: Element | null) => {
ref1(node);
ref2(node);
ref3(node);
},
[ref1, ref2, ref3],
);
return (
<div ref={setRefs}>
<div data-testid="item-1" data-inview={inView1.toString()}>
{inView1.toString()}
</div>
<div data-testid="item-2" data-inview={inView2.toString()}>
{inView2.toString()}
</div>
<div data-testid="item-3" data-inview={inView3.toString()}>
{inView3.toString()}
</div>
</div>
);
};
const { getByTestId } = render(<MultipleHooksWithTriggerOnce />);
mockAllIsIntersecting(true);
expect(getByTestId("item-1").getAttribute("data-inview")).toBe("true");
expect(getByTestId("item-2").getAttribute("data-inview")).toBe("true"); // ❌ FAILS: expected "true" but got "false"
expect(getByTestId("item-3").getAttribute("data-inview")).toBe("true");
});
Expected behavior: All three hooks should receive inView: true when the element enters the viewport.
Actual behavior: The test fails on item-2. Only the first and third hooks are triggered, the second hook remains false.
Root Cause
In src/observe.ts:86-88:
elements.get(entry.target)?.forEach((callback) => {
callback(inView, entry);
});When a callback executes with triggerOnce: true, it calls unobserve() which performs:
callbacks.splice(callbacks.indexOf(callback), 1);This modifies the array during the forEach iteration, causing subsequent callbacks to be skipped.
Proposed Solution
Copy the callbacks array before iterating using the spread operator:
// src/observe.ts:86
[...elements.get(entry.target) ?? []].forEach((callback) => {
callback(inView, entry);
});Alternative: use .slice():
elements.get(entry.target)?.slice().forEach((callback) => {
callback(inView, entry);
});Environment
- Version: v10.0.2 (current main branch)