An open API service indexing awesome lists of open source software.

https://github.com/suhaotian/broad-infinite-list

A high-performance, bidirectional infinite list for React/Vue/Expo. Use case: smoothly chat history, news feed updates, or stream logs in both directions without layout shifts. Best for large scroll list.
https://github.com/suhaotian/broad-infinite-list

bidirectional-scrollview expo infinite-list infinite-loading react react-native vue3 vue3-typescript

Last synced: 4 months ago
JSON representation

A high-performance, bidirectional infinite list for React/Vue/Expo. Use case: smoothly chat history, news feed updates, or stream logs in both directions without layout shifts. Best for large scroll list.

Awesome Lists containing this project

README

          

# [Broad Infinite List](https://suhaotian.github.io/broad-infinite-list/) Β· [![Size](https://deno.bundlejs.com/badge?q=broad-infinite-list/react&treeshake=[{default}]&config={%22esbuild%22:{%22external%22:[%22react%22,%22react-dom%22,%22react/jsx-runtime%22]}})](https://bundlejs.com/?q=broad-infinite-list/react&treeshake=%5B%7Bdefault%7D%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22react%22,%22react-dom%22,%22react/jsx-runtime%22%5D%7D%7D) [![npm version](https://img.shields.io/npm/v/broad-infinite-list.svg?style=flat)](https://www.npmjs.com/package/broad-infinite-list) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/suhaotian/broad-infinite-list/pulls) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/suhaotian/broad-infinite-list/blob/main/LICENSE) [![jsDocs.io](https://img.shields.io/badge/jsDocs.io-reference-blue)](https://www.jsdocs.io/package/broad-infinite-list) ![typescript](https://badgen.net/badge/icon/typescript?icon=typescript&label&color=blue)

**broad-infinite-list** is a tiny component that renders large lists efficiently by showing only a limited range of items. No need to configure each row’s height or use fixed row heights. It is suitable for chat message lists, news feed lists, and stream logs.

## Features

- πŸ”„ Bidirectional infinite scrolling
- ⚑ High performance - only renders fixed items
- πŸ“ Dynamic heights - no configuration needed
- πŸͺŸ Window or container scrolling

## Demos

- Chat Messages List [Demo](https://suhaotian.github.io/broad-infinite-list?demo=chat)
- News feed [Demo](https://suhaotian.github.io/broad-infinite-list?demo=news)
- Logs [Demo](https://suhaotian.github.io/broad-infinite-list?demo=logs)
- Vue Chat [Demo](https://suhaotian.github.io/broad-infinite-list/vue?demo=chat)
- ChatGPT [Demo](https://suhaotian.github.io/broad-infinite-list?demo=chatgpt)
- Claude theme [Demo](https://suhaotian.github.io/broad-infinite-list?demo=claude)

### Expo Demo Preview(ReactNative)

> [!NOTE]
> Scan it, then open it in Expo Go.

![react native demo](https://qr.expo.dev/eas-update?projectId=6ff17c83-f729-41d7-832c-93b12ba3435e&groupId=715a0933-a3de-4942-8079-b7940eeedba2)

**How It Works**

Define a fixed window of visible items (e.g., 30 entries from a 100,000-record dataset). Load items entering the viewport as the user scrolls, and remove items leaving the viewport. This keeps rendered items constant and maintains smooth performance with large datasets.

![how-it-works-chart](./flow.svg)

## Installation

```bash
npm install broad-infinite-list
```

## Quick Start

> [!CAUTION]
> For vue3 or React Native usage, check the example in [`vue-example/src/App.vue`](./vue-example/src/App.vue) or [`rn-expo-example/app/(tabs)/index.tsx`](<./rn-expo-example/app/(tabs)/index.tsx>)

> [!NOTE]
> For React web, copy and paste the below demo code, then run it.

```tsx
"use client";
import { useState, useRef } from "react";
import BidirectionalList, {
type BidirectionalListProps,
type BidirectionalListRef,
} from "broad-infinite-list/react";

interface NewsItem {
id: number;
title: string;
category: string;
timestamp: number;
}

const CATEGORIES = ["Tech", "Science", "Politics", "Business"];
const TITLES = [
"Senate Passes Landmark Infrastructure Bill",
"New AI Model Achieves Human-Level Performance",
"Global Temperatures Record Highest Monthly Average",
"Startup Raises $200M Series C for Autonomous Systems",
];

const TOTAL = 1000000;
const ALL_NEWS: NewsItem[] = Array.from({ length: TOTAL }, (_, i) => ({
id: i + 1,
title: `${i + 1}. ${TITLES[i % TITLES.length] || ""}`,
category: CATEGORIES[i % CATEGORIES.length] || "",
timestamp: Date.now() - (TOTAL - i) * 3600000,
}));

const NEWS_BY_RECENCY = [...ALL_NEWS].reverse();

const VIEW_COUNT = 50;
const PAGE_SIZE = 20;

function MyList() {
const [items, setItems] = useState(
NEWS_BY_RECENCY.slice(0, VIEW_COUNT)
);

const newestLoaded = items[0]?.id ?? 0;
const oldestLoaded = items[items.length - 1]?.id ?? ALL_NEWS.length + 1;
const hasPrevious = newestLoaded < ALL_NEWS.length;
const hasNext = oldestLoaded > 1;

const handleLoadMore: BidirectionalListProps["onLoadMore"] = async (
direction,
refItem
) => {
await new Promise((r) => setTimeout(r, 200));
const idx = NEWS_BY_RECENCY.findIndex((n) => n.id === refItem.id);
if (direction === "down") {
return NEWS_BY_RECENCY.slice(idx + 1, idx + PAGE_SIZE + 1);
} else {
return NEWS_BY_RECENCY.slice(idx - PAGE_SIZE, idx);
}
};

const listRef = useRef(null);
const showScrollTopButton = items?.[0]?.id !== NEWS_BY_RECENCY[0]?.id;
const onScrollToFirst = () => {
setItems(NEWS_BY_RECENCY.slice(0, VIEW_COUNT));
listRef.current?.scrollToTop();
};

return (
<>

ref={listRef}
items={items}
itemKey={(item) => item.id.toString()}
spinnerRow={




}
renderItem={(item) => (

{item.title}

{item.category}


)}
onLoadMore={handleLoadMore}
onItemsChange={setItems}
hasPrevious={hasPrevious}
hasNext={hasNext}
viewCount={VIEW_COUNT}
useWindow={true}
/>

↑


Total: {TOTAL}(1million)


Display: {items.length}


useWindow: {"true"}



>
);
}

const Spinner = () => (


);

export default MyList;
```

### BidirectionalList Props

| Property | Type | Required | Default | Description |
| ------------------ | --------------------------------------------------------- | -------- | ----------- | -------------------------------------------------------------------------------- |
| `items` | `T[]` | Yes | - | Current array of items to display |
| `itemKey` | `(item: T) => string` | Yes | - | Function to extract a unique key from each item |
| `renderItem` | `(item: T) => React.ReactNode` | Yes | - | Function to render each item |
| `onLoadMore` | `(direction: "up" \| "down", refItem: T) => Promise` | Yes | - | Called when more items should be loaded; returns the new items to prepend/append |
| `hasPrevious` | `boolean` | Yes | - | Whether there are more items available above the current view |
| `hasNext` | `boolean` | Yes | - | Whether there are more items available below the current view |
| `onItemsChange` | `(items: T[]) => void` | No | `undefined` | Called when the items array changes due to loading or trimming |
| `className` | `string` | No | `undefined` | The container tag's className |
| `itemClassName` | `string \| (items: T, index: number) => string` | No | `undefined` | The item tag's className |
| `itemStyle` | `itemStyle?: CSSProperties \| ((item: T, index: number) => CSSProperties \| undefined);` | No | `undefined` | The item element's style |
| `listClassName` | `string` | No | `undefined` | The list wrapper div's className |
| `containerAs` | `string` | No | `div` | The container tag, default is div, example: conatinerAs='table' |
| `as` | `string` | No | `div` | The list wrapper tag, default is div, example: as='ul' |
| `itemAs` | `string` | No | `div` | The item tag, default is div, example: itemAs='li' |
| `spinnerRow` | `React.ReactNode` | No | `undefined` | Custom loading indicator shown during fetch |
| `emptyState` | `React.ReactNode` | No | `undefined` | Content to display when items array is empty |
| `viewCount` | `number` | No | `50` | Maximum number of items to keep in DOM; older items are trimmed |
| `threshold` | `number` | No | `10` | Pixel distance from edge to trigger loading |
| `useWindow` | `boolean` | No | `false` | If true, use window scroll instead of container scroll |
| `disable` | `boolean` | No | `false` | If true, disable loading in both directions |
| `onScrollStart` | `() => void` | No | `undefined` | Called when a programmatic scroll adjustment begins |
| `onScrollEnd` | `() => void` | No | `undefined` | Called when a programmatic scroll adjustment ends |
| `headerSlot` | `({children}: {children: ReactNode}) => children` | No | `undefined` | for table element |
| `footerSlot` | `({children}: {children: ReactNode}) => children` | No | `undefined` | for table element |

### BidirectionalListRef

| Property | Type | Required | Default | Description |
| ------------------- | -------------------------------------------------- | -------- | ------- | --------------------------------------------- |
| `scrollViewRef` | `RefObject` | Yes | - | Reference to the scrollable container element |
| `scrollToTop` | `(behavior?: ScrollBehavior) => void` | Yes | - | Scroll to the top of the list |
| `scrollToBottom` | `(behavior?: ScrollBehavior) => void` | Yes | - | Scroll to the bottom of the list |
| `scrollTo` | `(top: number, behavior?: ScrollBehavior) => void` | Yes | - | Scroll to a specific pixel offset from top |
| `scrollToKey` | `(key: string, behavior?: ScrollBehavior) => void` | Yes | - | Scroll to an item by its key |
| `getTopDistance` | `() => number` | Yes | - | Get Current distnace to top |
| `getBottomDistance` | `() => number` | Yes | - | Get Current distnace to bottom |

## Development

This project use bun, but in `rn-expo-example/` use pnpm.

```sh
bun install && bun run build
```

## Reporting Issues

Found an issue? Please feel free to [create issue](https://github.com/suhaotian/broad-infinite-list/issues/new)

## Support

If you find this project helpful, consider [buying me a coffee](https://github.com/suhaotian/broad-infinite-list/stargazers).

## Projects You May Also Be Interested In

- [xior](https://github.com/suhaotian/xior) - Tiny fetch library with plugins support and axios-like API
- [tsdk](https://github.com/tsdk-monorepo/tsdk) - Type-safe API development CLI tool for TypeScript projects
- [useNextTick](https://github.com/suhaotian/use-next-tick) - `nextTick` but for react: Running callbacks after the DOM or native views have updated.