Skip to content

handleScroll throws "getComputedStyle ... parameter 1 is not of type 'Element'" when the lock unmounts mid-teardown (null lockRef, e.g. Radix Dialog unmounted by a route change) #165

Description

@JSummerer

Versions: react-remove-scroll 2.7.2 (latest), consumed via @radix-ui/react-dialog 1.1.15, React 18.2, Chrome/Chromium.

Error

TypeError: Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.
    at handleScroll (react-remove-scroll/dist/.../handleScroll.js)
    at shouldCancelEvent (SideEffect: handleScroll(cancelingAxis, parent, event, ...))
    at scrollWheel / onScrollCapture (React-dispatched)

Mechanism

SideEffect's React-attached capture handlers call shouldCancelEvent(event, props.lockRef.current), and handleScroll immediately does:

const directionFactor = getDirectionFactor(axis, window.getComputedStyle(endTarget).direction);

with no guard on endTarget. When the component owning the scroll lock is unmounted abruptly while open — the concrete case: a Radix Dialog open when an SPA route change unmounts the page containing it — a scroll/wheel event can still be dispatched through the React tree during teardown after lockRef.current has already been nulled, so getComputedStyle(null) throws.

This is distinct from #108: that report was about ShadowRoot nodes reaching elementCanBeScrolled (and the guard added there fixed it). Here the very first getComputedStyle call in handleScroll receives null, before any of the guarded helpers run.

Reproduction sketch

  1. React app with client-side routing (React Router), Radix Dialog (modal, default RemoveScroll integration).
  2. Open the dialog.
  3. Navigate to another route via a <Link> inside the open dialog (so the dialog unmounts abruptly while open, no exit transition).
  4. Console shows the TypeError (in our app it fires twice per navigation, from the scroll and wheel capture paths). Reproducible in both dev and production bundles.

A graceful controlled close (open={false}, exit animation completes) never throws — only unmount-while-open does.

Impact

Functionally near-harmless (the throw happens in a React-guarded event handler during teardown), but it lands in window.onerror, so it pollutes error monitoring (one ingested error per dialog→navigation) and fails any console-clean assertion in e2e suites. "Close before navigating" is not a viable userland workaround: React 18 batches the close and the navigation state updates into one commit, so the unmount is abrupt either way.

Suggested fix

A null/Element guard at the top of handleScroll (or in shouldCancelEvent before calling it), e.g.:

export const handleScroll = (axis, endTarget, event, sourceDelta, noOverscroll) => {
  if (!(endTarget instanceof Element)) {
    return true; // conservative: treat as "should cancel" so lock semantics don't loosen mid-teardown
  }
  ...

Happy to open a PR if the conservative return value is the behavior you'd want.

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