Skip to content

feat: rewrite core with function component and IntersectionObserver sentinel#422

Merged
iamdarshshah merged 11 commits intomasterfrom
feat/intersection-observer-rewrite
Apr 12, 2026
Merged

feat: rewrite core with function component and IntersectionObserver sentinel#422
iamdarshshah merged 11 commits intomasterfrom
feat/intersection-observer-rewrite

Conversation

@iamdarshshah
Copy link
Copy Markdown
Collaborator

@iamdarshshah iamdarshshah commented Apr 11, 2026

Summary

  • Rewrites InfiniteScroll from a class component to a function component using useRef/useEffect/useState
  • Replaces the throttled scroll event listener with an IntersectionObserver sentinel pattern, a small invisible div at the end of the list is observed; next() is called when it enters the viewport
  • Drops throttle-debounce as a runtime dependency (zero runtime deps)
  • Upgrades build tooling: Rollup 1 → 4, TypeScript 4 → 5, rollup plugins updated to @rollup/* scoped packages
  • Fixes scrollableTarget prop type (ReactNodeHTMLElement | string | null)
  • Rewrites the full test suite against the IO-based implementation; adds IntersectionObserver mock for jsdom, new test files for buildRootMargin, noScrollbar, and scrollableTarget

Why IntersectionObserver

IO is purpose-built for visibility detection, it runs off the main thread and doesn't require throttling. Benchmarks show ~23% scripting time vs ~29% for throttled scroll listeners. All major pure infinite scroll libraries have moved to IO; virtualization libraries (react-window, TanStack Virtual) remain on scroll events because they need real-time scroll position data, which is a different problem.

What is unchanged for consumers

All existing props work identically, next, hasMore, dataLength, loader, endMessage, scrollThreshold, height, scrollableTarget, inverse, pullDownToRefresh, onScroll, initialScrollY, className. No props were removed or renamed. This is a v7 minor.

Test plan

  • yarn test: 54 tests, all passing
  • yarn build: CJS, ESM, and IIFE outputs produced
  • yarn size: bundle within the 6 kB limits set by the quality gates
  • Manual smoke test: window scroll, fixed-height container, scrollableTarget, pull-to-refresh, inverse mode

Replace rollup.config.js with rollup.config.mjs, swap rollup v1 plugins
for @rollup/plugin-node-resolve and @rollup/plugin-typescript, and
remove the throttle-debounce runtime dependency.
…ntinel

Replace class component + scroll listeners with a function component that
uses an IntersectionObserver on an invisible sentinel div. Adds
buildRootMargin utility to convert scrollThreshold into a CSS rootMargin
string. Drops throttle-debounce usage entirely.
Migrate all tests to @testing-library/react, add IntersectionObserver
mock in setup/, and add new test files for buildRootMargin utility and
no-scrollbar behaviour.
The previous code pushed a nested array instead of spreading items,
causing incorrect data shape in the Storybook window scroll story.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 94.52055% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.03%. Comparing base (e6d8fda) to head (615d177).
⚠️ Report is 11 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##           master     #422       +/-   ##
===========================================
+ Coverage   79.33%   95.03%   +15.69%     
===========================================
  Files           2        3        +1     
  Lines         150      161       +11     
  Branches       56       57        +1     
===========================================
+ Hits          119      153       +34     
+ Misses         24        4       -20     
+ Partials        7        4        -3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 11, 2026

Size Change: -417 B (-4.85%) ✅

Total Size: 8.19 kB

📦 View Changed
Filename Size Change
dist/index.es.js 4.09 kB -203 B (-4.73%)
dist/index.js 4.1 kB -214 B (-4.97%)

compressed-size-action

… null scrollableTarget, and missing refreshFunction
… mode, initialScrollY via scrollableTarget

Adds 7 tests covering:
- PTR: scrollTop > 0 guard blocks drag start
- PTR: upward pull (currentY < startY) ignored
- PTR: delta capped at 1.5× maxPullDownDistance
- PTR: releaseToRefreshContent shown when threshold breached
- PTR: window-scroll mode (no height) — listeners on window, exercises document.documentElement.scrollTop branch
- onScroll: window fallback when no height or scrollableTarget
- initialScrollY: scrolls scrollableTarget element (no height prop path)
- Fix inverse sentinel placement: sentinel now renders before children
  when inverse=true so the IO top-margin fires correctly when scrolling up
- Add SSR guard: typeof IntersectionObserver === 'undefined' check in
  Effect 2b prevents crash in Next.js App Router server components
- Fix onScroll prop type: MouseEvent -> UIEvent (scroll events are UIEvents)
- Fix defaultThreshold.value: 0.8 -> 80 (was computing 99.2% rootMargin
  instead of 20% on invalid scrollThreshold input)
- Add exports field to package.json for Node ESM and Next.js 13+ App Router
  subpath resolution; main/module kept for older bundler fallback
- Bump version to 7.1.0
- Fix pre-existing ts-check breakage: replace global with globalThis in
  test files, add resolveJsonModule for JSON import, exclude stories from
  tsconfig (storybook types not installed in devDependencies)
- rollup.config.mjs: switch tsconfig from tsconfig.json to tsconfig.lib.json
  tsconfig.json includes root-level JS files (lint-staged.config.js, jest.config.js),
  making TypeScript compute rootDir as the project root and emit to dist/src/index.js.
  @rollup/plugin-typescript uses ts.getOutputFileNames which returns dist/index.js,
  so emittedFiles lookup always misses and the plugin falls through without transpiling.
  tsconfig.lib.json includes only src files, rootDir is inferred as src/, and both
  paths agree on dist/index.js.

- .eslintrc.js: ignore src/stories since tsconfig.json now excludes it;
  @typescript-eslint/parser requires project-included files to parse type-aware rules.

- pullDown.test.tsx: global -> globalThis to fix no-unsafe-member-access lint error.
@iamdarshshah iamdarshshah merged commit 810f915 into master Apr 12, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant