├── .all-contributorsrc
├── .github
├── composite-actions
│ └── install
│ │ └── action.yml
└── workflows
│ ├── nextjs.yml
│ └── quality.yml
├── .gitignore
├── .husky
└── pre-commit
├── .nvmrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps
└── demo
│ ├── .eslintrc.js
│ ├── README.md
│ ├── lint-staged.config.mjs
│ ├── next-env.d.ts
│ ├── next.config.mjs
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ ├── app
│ │ ├── favicon.ico
│ │ ├── horizontal-element-scroll
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ ├── reverse-horizontal-element-scroll
│ │ │ └── page.tsx
│ │ ├── reverse-vertical-element-scroll
│ │ │ └── page.tsx
│ │ ├── reverse-window-scroll
│ │ │ └── page.tsx
│ │ ├── vertical-element-scroll
│ │ │ └── page.tsx
│ │ └── window-scroll
│ │ │ └── page.tsx
│ ├── components
│ │ ├── header.tsx
│ │ ├── layout.tsx
│ │ ├── list.tsx
│ │ └── page-title.tsx
│ ├── lib
│ │ └── utils.ts
│ └── styles
│ │ └── globals.css
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── lint-staged.config.mjs
├── package-lock.json
├── package.json
├── packages
├── eslint-config
│ ├── README.md
│ ├── next.js
│ ├── package.json
│ ├── prettier.js
│ ├── react-library.js
│ ├── typescript.js
│ └── unicorn.js
├── lint-staged-config
│ ├── generic.mjs
│ ├── next.mjs
│ └── package.json
├── react-infinite-scroll-hook
│ ├── .eslintrc.js
│ ├── LICENSE
│ ├── README.md
│ ├── lint-staged.config.mjs
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── use-infinite-scroll.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
└── typescript-config
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
├── prettier.config.mjs
├── tsconfig.json
└── turbo.json
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "react-infinite-scroll-hook",
3 | "projectOwner": "onderonur",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": ["./packages/react-infinite-scroll-hook/README.md"],
7 | "imageSize": 100,
8 | "commit": true,
9 | "commitConvention": "none",
10 | "contributors": [
11 | {
12 | "login": "eugef",
13 | "name": "Eugene Fidelin",
14 | "avatar_url": "https://avatars0.githubusercontent.com/u/895071?v=4",
15 | "profile": "https://nl.linkedin.com/in/eugef",
16 | "contributions": ["code"]
17 | },
18 | {
19 | "login": "Evanc123",
20 | "name": "Evan Cater",
21 | "avatar_url": "https://avatars.githubusercontent.com/u/4010547?v=4",
22 | "profile": "https://github.com/Evanc123",
23 | "contributions": ["doc"]
24 | },
25 | {
26 | "login": "groomain",
27 | "name": "Romain",
28 | "avatar_url": "https://avatars.githubusercontent.com/u/3601848?v=4",
29 | "profile": "https://github.com/groomain",
30 | "contributions": ["example"]
31 | }
32 | ],
33 | "contributorsPerLine": 7,
34 | "linkToUsage": true
35 | }
36 |
--------------------------------------------------------------------------------
/.github/composite-actions/install/action.yml:
--------------------------------------------------------------------------------
1 | name: Install
2 | description: Sets up Node.js and runs install
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - name: Setup Node.js
8 | uses: actions/setup-node@v4
9 | with:
10 | node-version-file: '.nvmrc'
11 | cache: npm
12 |
13 | - name: Install dependencies
14 | shell: bash
15 | run: npm ci
16 |
--------------------------------------------------------------------------------
/.github/workflows/nextjs.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml
2 | # Sample workflow for building and deploying a Next.js site to GitHub Pages
3 | #
4 | # To get started with Next.js see: https://nextjs.org/docs/getting-started
5 | #
6 | name: Deploy Next.js site to Pages
7 |
8 | on:
9 | # Runs when `Quality` workflow gets completed on `main` branch
10 | workflow_run:
11 | branches: ['main']
12 | workflows: ['Quality']
13 | types:
14 | - completed
15 | # Allows you to run this workflow manually from the Actions tab
16 | workflow_dispatch:
17 |
18 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
19 | permissions:
20 | contents: read
21 | pages: write
22 | id-token: write
23 |
24 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
25 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
26 | concurrency:
27 | group: 'pages'
28 | cancel-in-progress: false
29 |
30 | jobs:
31 | # Build job
32 | build:
33 | # Since `workflow_run` only has 3 activity types (`completed`, `requested` and `in_progress`),
34 | # even if `Quality` workflow gets completed by failing, this workflow starts to run.
35 | # To prevent this, we add a condition here.
36 | # https://github.com/orgs/community/discussions/26238#discussioncomment-3250901
37 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
38 | runs-on: ubuntu-latest
39 | steps:
40 | - name: Checkout
41 | uses: actions/checkout@v4
42 | - name: Setup Pages
43 | uses: actions/configure-pages@v5
44 | - name: Install
45 | uses: ./.github/composite-actions/install
46 | - name: Build with Next.js
47 | run: npm run build -- --filter=demo
48 | - name: Upload artifact
49 | uses: actions/upload-pages-artifact@v3
50 | with:
51 | path: ./apps/demo/out
52 |
53 | # Deployment job
54 | deploy:
55 | environment:
56 | name: github-pages
57 | url: ${{ steps.deployment.outputs.page_url }}
58 | runs-on: ubuntu-latest
59 | needs: build
60 | steps:
61 | - name: Deploy to GitHub Pages
62 | id: deployment
63 | uses: actions/deploy-pages@v4
64 |
--------------------------------------------------------------------------------
/.github/workflows/quality.yml:
--------------------------------------------------------------------------------
1 | name: Quality
2 |
3 | on:
4 | push:
5 | branches: ['main']
6 | pull_request:
7 | branches: ['main']
8 | # Allows you to run this workflow manually from the Actions tab
9 | workflow_dispatch:
10 |
11 | # To cancel previous workflow when a new one is triggered.
12 | concurrency:
13 | group: ${{ github.workflow }}-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | format:
18 | name: Format
19 | runs-on: ubuntu-latest
20 | steps:
21 | - name: Check out
22 | uses: actions/checkout@v4
23 | - name: Install
24 | uses: ./.github/composite-actions/install
25 | - name: Run format check
26 | run: npm run format
27 |
28 | lint:
29 | name: Lint
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Check out
33 | uses: actions/checkout@v4
34 | - name: Install
35 | uses: ./.github/composite-actions/install
36 | - name: Run lint check
37 | run: npm run lint
38 |
39 | typecheck:
40 | name: Typecheck
41 | runs-on: ubuntu-latest
42 | steps:
43 | - name: Check out
44 | uses: actions/checkout@v4
45 | - name: Install
46 | uses: ./.github/composite-actions/install
47 | - name: Run type check
48 | run: npm run typecheck
49 |
50 | build:
51 | name: Build
52 | runs-on: ubuntu-latest
53 | steps:
54 | - name: Check out
55 | uses: actions/checkout@v4
56 | - name: Install
57 | uses: ./.github/composite-actions/install
58 | - name: Run build
59 | run: npm run build
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Dependencies
4 | node_modules
5 | .pnp
6 | .pnp.js
7 |
8 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 | # Debug
31 | npm-debug.log*
32 | yarn-debug.log*
33 | yarn-error.log*
34 |
35 | # Misc
36 | .DS_Store
37 | *.pem
38 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
2 | npm run typecheck
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | Plesae check [Releases](https://github.com/onderonur/react-infinite-scroll-hook/releases) page for migration instructions.
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | This repository is created by using [Turborepo](https://turbo.build/repo). To understand the repository structure better, please check its [documentation](https://turbo.build/repo/docs).
4 |
5 | ## Local Development
6 |
7 | After cloning the repository, we need to install the dependencies.
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | To start the demo Next.js app which uses the local version of `react-infinite-scroll-hook`, we can run `dev` script.
14 |
15 | ```bash
16 | npm run dev
17 | ```
18 |
19 | After this, we can open `http://localhost:3000` in the browser to display the app.
20 |
21 | ## Code Quality Checks
22 |
23 | We use automated checks by using [ESLint](https://eslint.org/), [Prettier](https://prettier.io/) and [TypeScript](https://www.typescriptlang.org/) to provide the highest quality code as it can be.
24 |
25 | All checks are run automatically before committing by using [husky](https://github.com/typicode/husky) and [lint-staged](https://github.com/lint-staged/lint-staged).
26 |
27 | The checks can be run manually by running the below command too.
28 |
29 | ```bash
30 | npm run codequality
31 | ```
32 |
33 | And the same checks can be run also by enabling fixes for auto-fixable issues.
34 |
35 | ```bash
36 | npm run codequality:fix
37 | ```
38 |
39 | `codequality` scripts run underlying ESLint (`lint`), Prettier (`format`) and TypeScript (`types`) scripts. To run these tools individually, we can also use the below scripts.
40 |
41 | ```bash
42 | # ESLint checks
43 | npm run lint
44 | # ESLint fixes
45 | npm run lint:fix
46 |
47 | # Prettier checks
48 | npm run format
49 | # Prettier fixes
50 | npm run format:fix
51 |
52 | # TypeScript checks
53 | npm run typecheck
54 | # There is no auto-fix script for TypeScript.
55 | ```
56 |
57 | ## Updating Dependencies
58 |
59 | We use `npm-check-updates` package to automatically check if there are newer versions of our dependencies.
60 |
61 | To run it, we can use the below command. It starts an interactive CLI to check the dependencies of all the apps and packages, including the root dependencies.
62 |
63 | ```bash
64 | npm run updates
65 | ```
66 |
67 | ## Adding Contributors
68 |
69 | [all-contributors-cli](https://github.com/all-contributors/cli) is used for maintaining the contributors of this repository.
70 |
71 | To add a new contributor, we can run the below command and follow its instructions.
72 |
73 | ```bash
74 | npm run contributors:add
75 | ```
76 |
77 | ## Prepublish Checks
78 |
79 | To be sure everything is OK with the latest changes, we can use [publint](https://publint.dev/) and [Are the Types Wrong](https://github.com/arethetypeswrong/arethetypeswrong.github.io).
80 |
81 | Firstly, we need to build the bundle with the latest changes.
82 |
83 | ```bash
84 | npm run build:bundle
85 | ```
86 |
87 | This command will create (or update) the `packages/react-infinite-scroll-hook/dist` folder, which will be used by the clients of this package.
88 |
89 | To be sure the output is OK for ESM and CJS clients, we can run the below commands and check their outputs.
90 |
91 | ```bash
92 | # For `publint`
93 | npm run publint -w react-infinite-scroll-hook
94 |
95 | # For `Are the Types Wrong`
96 | npm run attw -w react-infinite-scroll-hook
97 | ```
98 |
99 | To see the content of the package which can be uploaded to [npm](https://www.npmjs.com/) can be seen by using the below command. It will create a tarball from `react-infinite-scroll-hook` package.
100 |
101 | ```bash
102 | npm pack -w react-infinite-scroll-hook
103 | ```
104 |
105 | Or the below command can be used to only check the tarball contents without creating it.
106 |
107 | ```bash
108 | npm pack --dry-run -w react-infinite-scroll-hook
109 | ```
110 |
111 | Lastly, we can run the below command to auto correct common errors in `package.json` of the package to be published. `npm publish` command already does these auto-fixes too.
112 |
113 | ```bash
114 | npm pkg fix -w react-infinite-scroll-hook
115 | ```
116 |
117 | ## Publishing the Package
118 |
119 | Firstly, we need to bump the package version which can be done by using the below commands.
120 |
121 | ```bash
122 | npm version patch -w react-infinite-scroll-hook
123 | # Bumps the patch number like 0.0.0 -> 0.0.1
124 |
125 | npm version minor -w react-infinite-scroll-hook
126 | # Bumps the patch number like 0.0.x -> 0.1.0
127 |
128 | npm version major -w react-infinite-scroll-hook
129 | # Bumps the patch number like 0.x.y -> 1.0.0
130 | ```
131 |
132 | And we can publish the new version now 🚀
133 |
134 | ```bash
135 | npm publish -w react-infinite-scroll-hook
136 | ```
137 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 onderonur
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | packages/react-infinite-scroll-hook/README.md
--------------------------------------------------------------------------------
/apps/demo/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ['@repo/eslint-config/next.js'],
5 | parser: '@typescript-eslint/parser',
6 | parserOptions: {
7 | project: true,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/apps/demo/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/apps/demo/lint-staged.config.mjs:
--------------------------------------------------------------------------------
1 | export { default } from '@repo/lint-staged-config/next.mjs';
2 |
--------------------------------------------------------------------------------
/apps/demo/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/apps/demo/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: 'export',
4 | basePath: '/react-infinite-scroll-hook',
5 | images: {
6 | unoptimized: true,
7 | },
8 | eslint: {
9 | dirs: [
10 | 'src',
11 | 'lint-staged.config.mjs',
12 | 'postcss.config.js',
13 | 'tailwind.config.ts',
14 | ],
15 | },
16 | };
17 |
18 | export default nextConfig;
19 |
--------------------------------------------------------------------------------
/apps/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "dev": "next dev",
8 | "lint": "next lint --max-warnings 0",
9 | "lint:fix": "next lint --fix --max-warnings 0",
10 | "start": "next start",
11 | "typecheck": "tsc"
12 | },
13 | "dependencies": {
14 | "next": "15.3.1",
15 | "react": "^19.1.0",
16 | "react-dom": "^19.1.0",
17 | "react-infinite-scroll-hook": "*"
18 | },
19 | "devDependencies": {
20 | "@repo/eslint-config": "*",
21 | "@repo/lint-staged-config": "*",
22 | "@repo/typescript-config": "*",
23 | "@types/node": "^22.15.3",
24 | "@types/react": "^19.1.2",
25 | "@types/react-dom": "^19.1.3",
26 | "autoprefixer": "^10.4.21",
27 | "eslint": "^8.57.0",
28 | "eslint-config-next": "14.2.5",
29 | "postcss": "^8.5.3",
30 | "tailwindcss": "^3.4.10",
31 | "typescript": "^5.8.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/demo/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/demo/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/onderonur/react-infinite-scroll-hook/2ad3828e05345def748dfa2cb93d78aff423ebb6/apps/demo/src/app/favicon.ico
--------------------------------------------------------------------------------
/apps/demo/src/app/horizontal-element-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useInfiniteScroll from 'react-infinite-scroll-hook';
4 | import { List, ListItem, Loading } from '../../components/list';
5 | import { PageTitle } from '../../components/page-title';
6 | import { useLoadItems } from '../../lib/utils';
7 |
8 | export default function HorizontalElementScrollPage() {
9 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
10 |
11 | const [infiniteRef, { rootRef }] = useInfiniteScroll({
12 | loading,
13 | hasNextPage,
14 | onLoadMore: loadMore,
15 | disabled: Boolean(error),
16 | });
17 |
18 | return (
19 |
20 |
21 | Horizontal Element Scroll
22 |
23 |
27 |
28 | {items.map((item) => (
29 | {item.value}
30 | ))}
31 |
32 | {hasNextPage && }
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/demo/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next';
2 | import { Inter } from 'next/font/google';
3 | import { Header } from '../components/header';
4 | import { Layout } from '../components/layout';
5 | import '../styles/globals.css';
6 |
7 | const inter = Inter({ subsets: ['latin'] });
8 |
9 | export const metadata: Metadata = {
10 | title: 'react-infinite-scroll-hook',
11 | description: 'Demo app to showcase react-infinite-scroll-hook usage',
12 | };
13 |
14 | type RootLayoutProps = {
15 | children: React.ReactNode;
16 | };
17 |
18 | export default function RootLayout({ children }: RootLayoutProps) {
19 | return (
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/apps/demo/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { PageTitle } from '../components/page-title';
3 |
4 | export default function HomePage() {
5 | return (
6 |
7 | Home
8 |
9 | Select one of the options to see the live demo and their source code.
10 |
11 |
12 | This is a very simple demo website for `react-infinite-scroll-hook` only
13 | to have some live demos for users.
14 |
15 |
16 | You can check the full source code and docs{' '}
17 |
21 | here
22 |
23 | .
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/demo/src/app/reverse-horizontal-element-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
4 | import useInfiniteScroll from 'react-infinite-scroll-hook';
5 | import { List, ListItem, Loading } from '../../components/list';
6 | import { PageTitle } from '../../components/page-title';
7 | import { useLoadItems } from '../../lib/utils';
8 |
9 | export default function ReverseHorizontalElementScrollPage() {
10 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
11 |
12 | const [infiniteRef, { rootRef }] = useInfiniteScroll({
13 | loading,
14 | hasNextPage,
15 | onLoadMore: loadMore,
16 | disabled: Boolean(error),
17 | });
18 |
19 | const scrollableRootRef = useRef | null>(null);
20 | const lastScrollDistanceToRightRef = useRef(0);
21 |
22 | const reversedItems = useMemo(() => [...items].reverse(), [items]);
23 |
24 | // We keep the scroll position when new items are added etc.
25 | useLayoutEffect(() => {
26 | const scrollableRoot = scrollableRootRef.current;
27 | const lastScrollDistanceToRight = lastScrollDistanceToRightRef.current;
28 | if (scrollableRoot) {
29 | scrollableRoot.scrollLeft =
30 | scrollableRoot.scrollWidth - lastScrollDistanceToRight;
31 | }
32 | }, [reversedItems, rootRef]);
33 |
34 | const rootRefSetter = useCallback(
35 | (node: HTMLDivElement) => {
36 | rootRef(node);
37 | scrollableRootRef.current = node;
38 | },
39 | [rootRef],
40 | );
41 |
42 | const handleRootScroll = useCallback(() => {
43 | const rootNode = scrollableRootRef.current;
44 | if (rootNode) {
45 | const scrollDistanceToRight = rootNode.scrollWidth - rootNode.scrollLeft;
46 | lastScrollDistanceToRightRef.current = scrollDistanceToRight;
47 | }
48 | }, []);
49 |
50 | return (
51 |
52 |
53 | Reverse Horizontal Element Scroll
54 |
55 |
60 | {hasNextPage && }
61 |
62 | {reversedItems.map((item) => (
63 | {item.value}
64 | ))}
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/demo/src/app/reverse-vertical-element-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
4 | import useInfiniteScroll from 'react-infinite-scroll-hook';
5 | import { List, ListItem, Loading } from '../../components/list';
6 | import { PageTitle } from '../../components/page-title';
7 | import { useLoadItems } from '../../lib/utils';
8 |
9 | export default function ReverseVerticalElementScrollPage() {
10 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
11 |
12 | const [infiniteRef, { rootRef }] = useInfiniteScroll({
13 | loading,
14 | hasNextPage,
15 | onLoadMore: loadMore,
16 | disabled: Boolean(error),
17 | });
18 |
19 | const scrollableRootRef = useRef | null>(null);
20 | const lastScrollDistanceToBottomRef = useRef(0);
21 |
22 | const reversedItems = useMemo(() => [...items].reverse(), [items]);
23 |
24 | // We keep the scroll position when new items are added etc.
25 | useLayoutEffect(() => {
26 | const scrollableRoot = scrollableRootRef.current;
27 | const lastScrollDistanceToBottom = lastScrollDistanceToBottomRef.current;
28 | if (scrollableRoot) {
29 | scrollableRoot.scrollTop =
30 | scrollableRoot.scrollHeight - lastScrollDistanceToBottom;
31 | }
32 | }, [reversedItems, rootRef]);
33 |
34 | const rootRefSetter = useCallback(
35 | (node: HTMLDivElement) => {
36 | rootRef(node);
37 | scrollableRootRef.current = node;
38 | },
39 | [rootRef],
40 | );
41 |
42 | const handleRootScroll = useCallback(() => {
43 | const rootNode = scrollableRootRef.current;
44 | if (rootNode) {
45 | const scrollDistanceToBottom = rootNode.scrollHeight - rootNode.scrollTop;
46 | lastScrollDistanceToBottomRef.current = scrollDistanceToBottom;
47 | }
48 | }, []);
49 |
50 | return (
51 |
52 |
53 | Reverse Vertical Element Scroll
54 |
55 |
60 | {hasNextPage && }
61 |
62 | {reversedItems.map((item) => (
63 | {item.value}
64 | ))}
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/apps/demo/src/app/reverse-window-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
4 | import useInfiniteScroll from 'react-infinite-scroll-hook';
5 | import { List, ListItem, Loading } from '../../components/list';
6 | import { PageTitle } from '../../components/page-title';
7 | import { useLoadItems } from '../../lib/utils';
8 |
9 | export default function ReverseWindowScrollPage() {
10 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
11 |
12 | const [infiniteRef] = useInfiniteScroll({
13 | loading,
14 | hasNextPage,
15 | onLoadMore: loadMore,
16 | disabled: Boolean(error),
17 | });
18 |
19 | const lastScrollDistanceToBottomRef = useRef(0);
20 |
21 | const reversedItems = useMemo(() => [...items].reverse(), [items]);
22 |
23 | // We keep the scroll position when new items are added etc.
24 | useLayoutEffect(() => {
25 | const lastScrollDistanceToBottom = lastScrollDistanceToBottomRef.current;
26 |
27 | document.documentElement.scrollTop =
28 | document.body.scrollHeight - lastScrollDistanceToBottom;
29 | }, [reversedItems]);
30 |
31 | useEffect(() => {
32 | function handleScroll() {
33 | const rootNode = document.documentElement;
34 | const scrollDistanceToBottom = rootNode.scrollHeight - rootNode.scrollTop;
35 | lastScrollDistanceToBottomRef.current = scrollDistanceToBottom;
36 | }
37 |
38 | window.addEventListener('scroll', handleScroll);
39 |
40 | return () => {
41 | window.removeEventListener('scroll', handleScroll);
42 | };
43 | }, []);
44 |
45 | return (
46 |
47 | {hasNextPage && }
48 |
49 | {reversedItems.map((item) => (
50 | {item.value}
51 | ))}
52 |
53 |
54 | Reverse Window Scroll
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/apps/demo/src/app/vertical-element-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useInfiniteScroll from 'react-infinite-scroll-hook';
4 | import { List, ListItem, Loading } from '../../components/list';
5 | import { PageTitle } from '../../components/page-title';
6 | import { useLoadItems } from '../../lib/utils';
7 |
8 | export default function VerticalElementScrollPage() {
9 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
10 |
11 | const [infiniteRef, { rootRef }] = useInfiniteScroll({
12 | loading,
13 | hasNextPage,
14 | onLoadMore: loadMore,
15 | disabled: Boolean(error),
16 | });
17 |
18 | return (
19 |
20 |
21 | Vertical Element Scroll
22 |
23 |
27 |
28 | {items.map((item) => (
29 | {item.value}
30 | ))}
31 |
32 | {hasNextPage && }
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/demo/src/app/window-scroll/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import useInfiniteScroll from 'react-infinite-scroll-hook';
4 | import { List, ListItem, Loading } from '../../components/list';
5 | import { PageTitle } from '../../components/page-title';
6 | import { useLoadItems } from '../../lib/utils';
7 |
8 | export default function WindowScrollPage() {
9 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
10 |
11 | const [infiniteRef] = useInfiniteScroll({
12 | loading,
13 | hasNextPage,
14 | onLoadMore: loadMore,
15 | // When there is an error, we stop infinite loading.
16 | // It can be reactivated by setting "error" state as undefined.
17 | disabled: Boolean(error),
18 | // `rootMargin` is passed to `IntersectionObserver`.
19 | // We can use it to trigger 'onLoadMore' when the sentry comes near to become
20 | // visible, instead of becoming fully visible on the screen.
21 | rootMargin: '0px 0px 400px 0px',
22 | });
23 |
24 | return (
25 | <>
26 |
27 |
28 | Window Scroll
29 |
30 |
31 | {items.map((item) => (
32 | {item.value}
33 | ))}
34 |
35 | {hasNextPage && }
36 |
37 | {/* This is just for demonstration purposes.
38 | It may not be a good idea to put content below a full page infinite scroll list. */}
39 |
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/demo/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { usePathname, useRouter } from 'next/navigation';
5 |
6 | export function Header() {
7 | const router = useRouter();
8 | const pathname = usePathname();
9 |
10 | return (
11 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/apps/demo/src/components/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { usePathname } from 'next/navigation';
4 |
5 | type LayoutProps = {
6 | children: React.ReactNode;
7 | };
8 |
9 | export function Layout({ children }: LayoutProps) {
10 | const pathname = usePathname();
11 |
12 | return (
13 |
16 | {children}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/demo/src/components/list.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 |
3 | type ListProps = {
4 | direction?: 'vertical' | 'horizontal';
5 | children: React.ReactNode;
6 | };
7 |
8 | export function List({ direction, ...rest }: ListProps) {
9 | return (
10 |
14 | );
15 | }
16 |
17 | type ListItemProps = {
18 | children: React.ReactNode;
19 | };
20 |
21 | export function ListItem(props: ListItemProps) {
22 | return ;
23 | }
24 |
25 | export const Loading = forwardRef, unknown>(
26 | function Loading(props, ref) {
27 | return (
28 |
29 |
30 | Loading...
31 |
32 |
33 | );
34 | },
35 | );
36 |
--------------------------------------------------------------------------------
/apps/demo/src/components/page-title.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | type PageTitleProps = {
4 | filePath?: string;
5 | children: React.ReactNode;
6 | };
7 |
8 | export function PageTitle({ filePath, children }: PageTitleProps) {
9 | return (
10 |
11 |
{children}
12 | {filePath && (
13 |
19 | Source Code
20 |
21 | )}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/apps/demo/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | const ARRAY_SIZE = 20;
4 | const RESPONSE_TIME_IN_MS = 1000;
5 |
6 | export interface Item {
7 | key: number;
8 | value: string;
9 | }
10 |
11 | interface Response {
12 | hasNextPage: boolean;
13 | data: Item[];
14 | }
15 |
16 | function loadItems(startCursor = 0): Promise {
17 | return new Promise((resolve) => {
18 | let newArray: Item[] = [];
19 |
20 | setTimeout(() => {
21 | for (let i = startCursor; i < startCursor + ARRAY_SIZE; i++) {
22 | const newItem = {
23 | key: i,
24 | value: `This is item ${i.toString()}`,
25 | };
26 | newArray = [...newArray, newItem];
27 | }
28 |
29 | resolve({ hasNextPage: true, data: newArray });
30 | }, RESPONSE_TIME_IN_MS);
31 | });
32 | }
33 |
34 | export function useLoadItems() {
35 | const [loading, setLoading] = useState(false);
36 | const [items, setItems] = useState- ([]);
37 | const [hasNextPage, setHasNextPage] = useState
(true);
38 | const [error, setError] = useState();
39 |
40 | async function loadMore() {
41 | setLoading(true);
42 | try {
43 | const { data, hasNextPage: newHasNextPage } = await loadItems(
44 | items.length,
45 | );
46 | setItems((current) => [...current, ...data]);
47 | setHasNextPage(newHasNextPage);
48 | } catch (error_) {
49 | setError(
50 | error_ instanceof Error ? error_ : new Error('Something went wrong'),
51 | );
52 | } finally {
53 | setLoading(false);
54 | }
55 | }
56 |
57 | return { loading, items, hasNextPage, error, loadMore };
58 | }
59 |
--------------------------------------------------------------------------------
/apps/demo/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/apps/demo/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | export default {
4 | content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
5 | plugins: [],
6 | } satisfies Config;
7 |
--------------------------------------------------------------------------------
/apps/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ]
9 | },
10 | "include": [
11 | "next-env.d.ts",
12 | "**/*.ts",
13 | "**/*.tsx",
14 | "**/*.mjs",
15 | "**/*.js",
16 | ".next/types/**/*.ts"
17 | ],
18 | "exclude": ["node_modules"]
19 | }
20 |
--------------------------------------------------------------------------------
/lint-staged.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | '*': 'prettier --write --ignore-unknown',
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-infinite-scroll-hook",
3 | "private": true,
4 | "license": "MIT",
5 | "workspaces": [
6 | "apps/*",
7 | "packages/*"
8 | ],
9 | "scripts": {
10 | "build": "turbo build",
11 | "build:bundle": "turbo build --filter=react-infinite-scroll-hook",
12 | "codequality": "npm run format && npm run lint && npm run typecheck",
13 | "codequality:fix": "npm run format:fix && npm run lint:fix && npm run typecheck",
14 | "contributors:add": "all-contributors add",
15 | "contributors:generate": "all-contributors generate",
16 | "dev": "turbo dev",
17 | "format": "prettier --check --ignore-unknown .",
18 | "format:fix": "prettier --write --ignore-unknown .",
19 | "lint": "turbo lint",
20 | "lint:fix": "turbo lint:fix",
21 | "prepare": "husky && npm run build:bundle",
22 | "typecheck": "turbo typecheck",
23 | "updates": "npm-check-updates -i -ws --root"
24 | },
25 | "devDependencies": {
26 | "all-contributors-cli": "^6.26.1",
27 | "husky": "^9.1.7",
28 | "lint-staged": "^15.5.1",
29 | "npm-check-updates": "^18.0.1",
30 | "prettier": "^3.5.3",
31 | "prettier-plugin-organize-imports": "^4.1.0",
32 | "prettier-plugin-tailwindcss": "^0.6.11",
33 | "turbo": "^2.5.2"
34 | },
35 | "packageManager": "npm@11.3.0",
36 | "engines": {
37 | "node": ">=18"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | module.exports = {
5 | extends: [
6 | require.resolve('@vercel/style-guide/eslint/browser'),
7 | require.resolve('@vercel/style-guide/eslint/node'),
8 | require.resolve('@vercel/style-guide/eslint/react'),
9 | require.resolve('@vercel/style-guide/eslint/next'),
10 | require.resolve('./typescript'),
11 | require.resolve('./unicorn'),
12 | require.resolve('./prettier'),
13 | 'turbo',
14 | ],
15 | plugins: ['only-warn'],
16 | settings: {
17 | 'import/resolver': {
18 | typescript: {
19 | project: resolve(process.cwd(), 'tsconfig.json'),
20 | },
21 | },
22 | },
23 | ignorePatterns: [
24 | // Ignore dotfiles
25 | '.*.js',
26 | 'node_modules/',
27 | ],
28 | overrides: [{ files: ['*.js?(x)', '*.ts?(x)'] }],
29 | rules: {
30 | // `curly` rule is not working even if it is in `@vercel/style-guide/eslint/browser` and '@vercel/style-guide/eslint/node'.
31 | // The reason is, it conflicts with `eslint-config-prettier` and gets overriden
32 | // when it comes after this config file in the `extends` field of the root config file.
33 | // So, we add it here to make it work.
34 | curly: ['warn', 'multi-line'],
35 | 'import/no-default-export': 'off',
36 | 'react/jsx-no-leaked-render': 'off',
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "next.js",
7 | "prettier.js",
8 | "react-library.js",
9 | "typescript.js",
10 | "unicorn.js"
11 | ],
12 | "devDependencies": {
13 | "@typescript-eslint/eslint-plugin": "^7.17.0",
14 | "@typescript-eslint/parser": "^7.17.0",
15 | "@vercel/style-guide": "^6.0.0",
16 | "eslint-config-prettier": "^9.1.0",
17 | "eslint-config-turbo": "^2.0.14",
18 | "eslint-plugin-only-warn": "^1.1.0",
19 | "typescript": "^5.8.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/eslint-config/prettier.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | extends: ['prettier'],
4 | rules: {
5 | curly: ['warn', 'multi-line'],
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-library.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | module.exports = {
5 | extends: [
6 | require.resolve('@vercel/style-guide/eslint/browser'),
7 | require.resolve('@vercel/style-guide/eslint/react'),
8 | require.resolve('./typescript'),
9 | require.resolve('./unicorn'),
10 | require.resolve('./prettier'),
11 | 'turbo',
12 | ],
13 | plugins: ['only-warn'],
14 | settings: {
15 | 'import/resolver': {
16 | typescript: {
17 | project: resolve(process.cwd(), 'tsconfig.json'),
18 | },
19 | },
20 | },
21 | ignorePatterns: [
22 | // Ignore dotfiles
23 | '.*.js',
24 | 'node_modules/',
25 | 'dist/',
26 | ],
27 | overrides: [
28 | // Force ESLint to detect .tsx files
29 | { files: ['*.js?(x)', '*.ts?(x)'] },
30 | ],
31 | rules: {
32 | 'import/no-default-export': 'off',
33 | 'react/jsx-no-leaked-render': 'off',
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/packages/eslint-config/typescript.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | overrides: [
4 | {
5 | files: ['*.ts?(x)'],
6 | extends: [require.resolve('@vercel/style-guide/eslint/typescript')],
7 | rules: {
8 | '@typescript-eslint/consistent-type-definitions': 'off',
9 | '@typescript-eslint/explicit-function-return-type': 'off',
10 | '@typescript-eslint/naming-convention': 'off',
11 | },
12 | },
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/packages/eslint-config/unicorn.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | extends: ['plugin:unicorn/recommended'],
4 | // https://github.com/sindresorhus/eslint-plugin-unicorn/tree/main?tab=readme-ov-file#rules
5 | rules: {
6 | 'unicorn/prevent-abbreviations': 'off',
7 | 'unicorn/no-null': 'off',
8 | 'unicorn/prefer-module': 'off',
9 | 'unicorn/explicit-length-check': 'off',
10 | 'unicorn/prefer-ternary': 'off',
11 | 'unicorn/no-array-for-each': 'off',
12 | 'unicorn/prefer-export-from': 'off',
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/packages/lint-staged-config/generic.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | '*': 'prettier --write --ignore-unknown',
3 | '*.{js,jsx,ts,tsx}': 'eslint --max-warnings 0 --fix',
4 | };
5 |
6 | export default config;
7 |
--------------------------------------------------------------------------------
/packages/lint-staged-config/next.mjs:
--------------------------------------------------------------------------------
1 | // https://nextjs.org/docs/app/building-your-application/configuring/eslint#lint-staged
2 | import path from 'node:path';
3 |
4 | const buildEslintCommand = (filenames) =>
5 | `next lint --max-warnings 0 --fix --file ${filenames
6 | .map((f) =>
7 | path
8 | .relative(process.cwd(), f)
9 | // Removing `apps/` part to make `next lint` command work with `lint-staged`.
10 | .replace(/apps\/[a-z-]+\//, ''),
11 | )
12 | .join(' --file ')}`;
13 |
14 | const config = {
15 | '*': 'prettier --write --ignore-unknown',
16 | '*.{js,jsx,ts,tsx}': [buildEslintCommand],
17 | };
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/packages/lint-staged-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/lint-staged-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "generic.mjs",
7 | "next.mjs"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | module.exports = {
3 | root: true,
4 | extends: ['@repo/eslint-config/react-library.js'],
5 | parser: '@typescript-eslint/parser',
6 | parserOptions: {
7 | project: './tsconfig.json',
8 | tsconfigRootDir: __dirname,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 onderonur
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/README.md:
--------------------------------------------------------------------------------
1 | # react-infinite-scroll-hook
2 |
3 | 
4 | 
5 | 
6 |
7 |
8 |
9 | [](#contributors-)
10 |
11 |
12 |
13 | This is a hook to create infinite scroll components!
14 | **Live demo is [here](https://onderonur.github.io/react-infinite-scroll-hook/).**
15 |
16 | Basically, we need to set a `sentry` component to trigger infinite loading. When `sentry` becomes visible on the screen or it comes near to be visible (based on our config of course), it triggers infinite loading (by calling `onLoadMore` callback) all with the help of `IntersectionObserver`.
17 |
18 | `sentry` should be some component which will **not** be unmounted as long as we want to keep the infinite scrolling observer **active**. For example, we can use a "loading" indicator as our sentry. The trick is, because that we want to keep the infinite scrolling observer active as long as there is a **next page**, we need to keep this "loading" component mounted even if we don't have a `loading` flag as `true`. This will also keep our layout more consistent and prevent flickering etc.
19 |
20 | We don't need to use a "loading" component as the `sentry` and we can keep them separate too. It can be anything like some empty `div` or last item of your list etc. We just need to place it based on our scrolling direction. To bottom if we want to trigger loading when we scroll to bottom, to top if we want to trigger it when we scroll to top like a chat message box etc. Same approach can be used with horizontal scrolling too.
21 |
22 | Please check below to find examples with different layouts and libraries.
23 |
24 | **Note:** This package uses `IntersectionObserver` under the hood. You might want to check the browser compatibility from **[here](https://caniuse.com/intersectionobserver)** and if you want to support older browsers, you might need to use a polyfill.
25 |
26 | Before **v4**, `useInfiniteScroll` hook would basically check the DOM with an interval and look at the distance between the bottom of your "infinite" component and the bottom of the window. This was a simple solution. But it had its difficulties. It was not so easy to change the layout of your "infinite" component (like creating a chat message box with inverted scrolling etc). It was a requirement to modify the package based on each different use case.
27 |
28 | And also, checking the DOM with an interval by using `setInterval` wasn't a sophisticated solution. It was enough, but it had it's limits.
29 | With **v4**, we migrated to use `IntersectionObserver` and created a much more flexible API to support different design. Basically, now we have a little bit more [inversion of control](https://kentcdodds.com/blog/inversion-of-control).
30 |
31 | If you want to use the older version which is using `setInterval`, you can find it **[here](https://github.com/onderonur/react-infinite-scroll-hook/tree/v3)**.
32 |
33 | ## Installation
34 |
35 | ```sh
36 | npm install react-infinite-scroll-hook
37 | ```
38 |
39 | ## Simple Example
40 |
41 | ```tsx
42 | import useInfiniteScroll from 'react-infinite-scroll-hook';
43 |
44 | function WindowScroll() {
45 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
46 |
47 | const [infiniteRef] = useInfiniteScroll({
48 | loading,
49 | hasNextPage,
50 | onLoadMore: loadMore,
51 | // When there is an error, we stop infinite loading.
52 | // It can be reactivated by setting "error" state as undefined.
53 | disabled: Boolean(error),
54 | // `rootMargin` is passed to `IntersectionObserver`.
55 | // We can use it to trigger 'onLoadMore' when the sentry comes near to become
56 | // visible, instead of becoming fully visible on the screen.
57 | rootMargin: '0px 0px 400px 0px',
58 | });
59 |
60 | return (
61 |
62 |
63 | {items.map((item) => (
64 | {item.value}
65 | ))}
66 |
67 | {hasNextPage && }
68 |
69 | );
70 | }
71 | ```
72 |
73 | Or if we have a scrollable container and we want to use it as our "list container" instead of `document`, we just need to use `rootRef` like:
74 |
75 | ```tsx
76 | import useInfiniteScroll from 'react-infinite-scroll-hook';
77 |
78 | function VerticalElementScrollPage() {
79 | const { loading, items, hasNextPage, error, loadMore } = useLoadItems();
80 |
81 | const [infiniteRef, { rootRef }] = useInfiniteScroll({
82 | loading,
83 | hasNextPage,
84 | onLoadMore: loadMore,
85 | // When there is an error, we stop infinite loading.
86 | // It can be reactivated by setting "error" state as undefined.
87 | disabled: Boolean(error),
88 | // `rootMargin` is passed to `IntersectionObserver`.
89 | // We can use it to trigger 'onLoadMore' when the sentry comes near to become
90 | // visible, instead of becoming fully visible on the screen.
91 | rootMargin: '0px 0px 400px 0px',
92 | });
93 |
94 | return (
95 |
96 |
97 | {items.map((item) => (
98 | {item.value}
99 | ))}
100 |
101 | {hasNextPage && }
102 |
103 | );
104 | }
105 | ```
106 |
107 | ## Other Examples
108 |
109 | You can find different layout examples **[here](./apps/demo/src/app/)**. **[Live demo](https://onderonur.github.io/react-infinite-scroll-hook/)** contains all of these cases.
110 |
111 | Also, we have more realistic examples with [SWR](https://github.com/onderonur/next-moviez) and [TanStack Query](https://github.com/onderonur/next-rickql) too.
112 |
113 | ## Arguments
114 |
115 | | Name | Description | Type | Optional | Default Value |
116 | | ----------- | ------------------------------------------------------------------------------------------------ | ------------ | -------- | ------------- |
117 | | loading | Some sort of "is fetching" info of the request. | boolean | ❌ | |
118 | | hasNextPage | If the list has more items to load. | boolean | ❌ | |
119 | | onLoadMore | The callback function to execute when the 'onLoadMore' is triggered. | VoidFunction | ❌ | |
120 | | rootMargin | We pass this to 'IntersectionObserver'. We can use it to configure when to trigger 'onLoadMore'. | string | ✅ | |
121 | | disabled | Flag to stop infinite scrolling. Can be used in case of an error etc too. | boolean | ✅ | |
122 | | delayInMs | How long it should wait before triggering 'onLoadMore' (in milliseconds). | number | ✅ | 100 |
123 |
124 | ## Contributors ✨
125 |
126 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
127 |
128 |
129 |
130 |
131 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
145 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/lint-staged.config.mjs:
--------------------------------------------------------------------------------
1 | export { default } from '@repo/lint-staged-config/generic.mjs';
2 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-infinite-scroll-hook",
3 | "version": "6.0.0",
4 | "description": "A simple hook to create infinite scroll components",
5 | "keywords": [
6 | "react",
7 | "react-hooks",
8 | "infinite-scroll",
9 | "react-component"
10 | ],
11 | "homepage": "https://onderonur.github.io/react-infinite-scroll-hook",
12 | "bugs": {
13 | "url": "https://github.com/onderonur/react-infinite-scroll-hook/issues"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/onderonur/react-infinite-scroll-hook.git"
18 | },
19 | "license": "MIT",
20 | "author": "onderonur",
21 | "exports": {
22 | ".": {
23 | "import": {
24 | "types": "./dist/index.d.mts",
25 | "default": "./dist/index.mjs"
26 | },
27 | "require": {
28 | "types": "./dist/index.d.ts",
29 | "default": "./dist/index.js"
30 | }
31 | }
32 | },
33 | "main": "./dist/index.js",
34 | "module": "./dist/index.mjs",
35 | "types": "./dist/index.d.ts",
36 | "files": [
37 | "dist"
38 | ],
39 | "scripts": {
40 | "attw": "attw --pack .",
41 | "build": "tsup",
42 | "dev": "tsup --watch",
43 | "lint": "eslint . --max-warnings 0",
44 | "lint:fix": "eslint . --fix --max-warnings 0",
45 | "prepublishOnly": "npm run build",
46 | "publint": "publint",
47 | "typecheck": "tsc"
48 | },
49 | "dependencies": {
50 | "react-intersection-observer-hook": "^4.0.0"
51 | },
52 | "devDependencies": {
53 | "@arethetypeswrong/cli": "^0.17.4",
54 | "@repo/eslint-config": "*",
55 | "@repo/lint-staged-config": "*",
56 | "@repo/typescript-config": "*",
57 | "@types/react": "^19.1.2",
58 | "eslint": "^8.57.0",
59 | "publint": "^0.3.12",
60 | "react": "^19.1.0",
61 | "tsup": "^8.4.0",
62 | "typescript": "^5.8.3"
63 | },
64 | "peerDependencies": {
65 | "react": ">=19",
66 | "react-dom": ">=19"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | default,
3 | type UseInfiniteScrollHookArgs,
4 | type UseInfiniteScrollHookRefCallback,
5 | type UseInfiniteScrollHookResult,
6 | type UseInfiniteScrollHookRootRefCallback,
7 | } from './use-infinite-scroll';
8 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/src/use-infinite-scroll.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import {
3 | useTrackVisibility,
4 | type IntersectionObserverHookArgs,
5 | type IntersectionObserverHookRefCallback as UseInfiniteScrollHookRefCallback,
6 | type IntersectionObserverHookRootRefCallback as UseInfiniteScrollHookRootRefCallback,
7 | } from 'react-intersection-observer-hook';
8 |
9 | const DEFAULT_DELAY_IN_MS = 100;
10 |
11 | export {
12 | type UseInfiniteScrollHookRefCallback,
13 | type UseInfiniteScrollHookRootRefCallback,
14 | };
15 |
16 | export type UseInfiniteScrollHookResult = [
17 | UseInfiniteScrollHookRefCallback,
18 | { rootRef: UseInfiniteScrollHookRootRefCallback },
19 | ];
20 |
21 | export type UseInfiniteScrollHookArgs = Pick<
22 | IntersectionObserverHookArgs,
23 | // We pass this to 'IntersectionObserver'. We can use it to configure when to trigger 'onLoadMore'.
24 | 'rootMargin'
25 | > & {
26 | // Some sort of "is fetching" info of the request.
27 | loading: boolean;
28 | // If the list has more items to load.
29 | hasNextPage: boolean;
30 | // The callback function to execute when the 'onLoadMore' is triggered.
31 | onLoadMore: () => unknown;
32 | // Flag to stop infinite scrolling. Can be used in case of an error etc too.
33 | disabled?: boolean;
34 | // How long it should wait before triggering 'onLoadMore'.
35 | delayInMs?: number;
36 | };
37 |
38 | function useInfiniteScroll({
39 | loading,
40 | hasNextPage,
41 | onLoadMore,
42 | rootMargin,
43 | disabled,
44 | delayInMs = DEFAULT_DELAY_IN_MS,
45 | }: UseInfiniteScrollHookArgs): UseInfiniteScrollHookResult {
46 | const savedCallbackRef = useRef(onLoadMore);
47 | const [ref, { rootRef, isVisible }] = useTrackVisibility({
48 | rootMargin,
49 | });
50 |
51 | useEffect(() => {
52 | savedCallbackRef.current = onLoadMore;
53 | }, [onLoadMore]);
54 |
55 | const shouldLoadMore = !disabled && !loading && isVisible && hasNextPage;
56 |
57 | useEffect(() => {
58 | if (shouldLoadMore) {
59 | // When we trigger 'onLoadMore' and new items are added to the list,
60 | // right before they become rendered on the screen, 'loading' becomes false
61 | // and 'isVisible' can be true for a brief time, based on the scroll position.
62 | // So, it triggers 'onLoadMore' just after the first one is finished.
63 | // We use a small delay here to prevent this kind of situations.
64 | // It can be configured by hook args.
65 | const timer = setTimeout(() => {
66 | savedCallbackRef.current();
67 | }, delayInMs);
68 | return () => {
69 | clearTimeout(timer);
70 | };
71 | }
72 | }, [shouldLoadMore, delayInMs]);
73 |
74 | return [ref, { rootRef }];
75 | }
76 |
77 | export default useInfiniteScroll;
78 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "include": ["**/*.ts", "**/*.tsx", "**/*.mjs"],
4 | "exclude": ["node_modules", "dist"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/react-infinite-scroll-hook/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup';
2 |
3 | export default defineConfig(() => ({
4 | entry: ['src/index.ts'],
5 | outDir: 'dist',
6 | format: ['cjs', 'esm'],
7 | // Clean output directory before each build
8 | clean: true,
9 | // Generate dts files
10 | dts: true,
11 | }));
12 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "declaration": true,
6 | "declarationMap": true,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "lib": ["es2022", "DOM", "DOM.Iterable"],
11 | "module": "NodeNext",
12 | "moduleDetection": "force",
13 | "moduleResolution": "NodeNext",
14 | "noUncheckedIndexedAccess": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2022",
19 | // We use TypeScript only for linting/typechecking purposes.
20 | // So, we don't need it to emit anything.
21 | "noEmit": true
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "plugins": [{ "name": "next" }],
7 | "module": "ESNext",
8 | "moduleResolution": "Bundler",
9 | "allowJs": true,
10 | "jsx": "preserve"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "publishConfig": {
6 | "access": "public"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | import styleguide from '@vercel/style-guide/prettier';
2 |
3 | /**
4 | * @see https://prettier.io/docs/en/configuration.html
5 | * @type {import("prettier").Config}
6 | */
7 | const config = {
8 | ...styleguide,
9 | // To skip destructive code actions of `prettier-plugin-organize-imports`,
10 | // removing unused imports:
11 | // https://www.npmjs.com/package/prettier-plugin-organize-imports#skip-destructive-code-actions
12 | organizeImportsSkipDestructiveCodeActions: true,
13 | plugins: [
14 | ...styleguide.plugins,
15 | 'prettier-plugin-organize-imports',
16 | // Should be the last one.
17 | // https://github.com/tailwindlabs/prettier-plugin-tailwindcss?tab=readme-ov-file#compatibility-with-other-prettier-plugins
18 | 'prettier-plugin-tailwindcss',
19 | ],
20 | };
21 |
22 | export default config;
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
7 | "outputs": [".next/**", "!.next/cache/**", "dist/**"]
8 | },
9 | "dev": {
10 | "cache": false,
11 | "persistent": true
12 | },
13 | "lint": {
14 | "dependsOn": ["^build", "^lint"]
15 | },
16 | "lint:fix": {
17 | "dependsOn": ["^build", "^lint:fix"]
18 | },
19 | "typecheck": {
20 | "dependsOn": ["^build"]
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------