Skip to content

🐛 Bug: Callbacks skipped when using triggerOnce with merged refs #746

Description

@djk01281

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",()=>{constMultipleHooksWithTriggerOnce=()=>{const{ref: ref1,inView: inView1}=useInView({triggerOnce: true});const{ref: ref2,inView: inView2}=useInView({triggerOnce: true});const{ref: ref3,inView: inView3}=useInView({triggerOnce: true});constsetRefs=useCallback((node: Element|null)=>{ref1(node);ref2(node);ref3(node);},[ref1,ref2,ref3],);return(<divref={setRefs}><divdata-testid="item-1"data-inview={inView1.toString()}>{inView1.toString()}</div><divdata-testid="item-2"data-inview={inView2.toString()}>{inView2.toString()}</div><divdata-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");});
Image

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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions