- name
- webf-infinite-scrolling
- description
- Create high-performance infinite scrolling lists with pull-to-refresh and load-more capabilities using WebFListView. Use when building feed-style UIs, product catalogs, chat messages, or any scrollable list that needs optimal performance with large datasets.
# WebF Infinite Scrolling
> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **performance optimization for scrolling lists** - a WebF-specific pattern that provides native-level performance automatically.
Build high-performance infinite scrolling lists with Flutter-optimized rendering. WebF's `WebFListView` component automatically handles performance optimizations at the Flutter level, providing smooth 60fps scrolling even with thousands of items.
## Why Use WebFListView?
In browsers, long scrolling lists can cause performance issues:
- DOM nodes accumulate (memory consumption)
- Re-renders affect all items (slow updates)
- Intersection observers needed for virtualization
- Complex state management for infinite loading
**WebF's solution**: `WebFListView` delegates rendering to Flutter's optimized ListView widget, which:
- ✅ Automatically virtualizes (recycles) views
- ✅ Maintains 60fps scrolling with thousands of items
- ✅ Provides native pull-to-refresh and load-more
- ✅ Zero configuration - optimization happens automatically
## Critical Structure Requirement
**⚠️ IMPORTANT**: For Flutter optimization to work, each list item must be a **direct child** of `WebFListView`:
### ✅ CORRECT: Direct Children
```jsx
<WebFListView>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
{/* Each item is a direct child */}
</WebFListView>
```
### ❌ WRONG: Wrapped in Container
```jsx
<WebFListView>
<div>
{/* DON'T wrap items in a container div */}
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>
</WebFListView>
```
**Why this matters**: Flutter's ListView requires direct children to perform view recycling. If items are wrapped in a container, Flutter sees only one child (the container) and cannot optimize individual items.
## React Setup
### Installation
```bash
npm install @openwebf/react-core-ui
```
### Basic Scrolling List
```tsx
import { WebFListView } from '@openwebf/react-core-ui';
function ProductList() {
const products = [
{ id: 1, name: 'Product 1', price: 19.99 },
{ id: 2, name: 'Product 2', price: 29.99 },
{ id: 3, name: 'Product 3', price: 39.99 },
// ... hundreds or thousands of items
];
return (
<WebFListView scrollDirection="vertical" shrinkWrap={true}>
{products.map(product => (
// ✅ Each item is a direct child
<div key={product.id} className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</WebFListView>
);
}
```
### Infinite Scrolling with Load More
```tsx
import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui';
import { useRef, useState } from 'react';
function InfiniteList() {
const listRef = useRef<WebFListViewElement>(null);
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const [page, setPage] = useState(1);
const handleLoadMore = async () => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Fetch next page
const newItems = Array.from(
{ length: 5 },
(_, i) => items.length + i + 1
);
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
// Check if there's more data
const hasMore = page < 10; // Example: 10 pages max
// Notify WebFListView that loading finished
listRef.current?.finishLoad(hasMore ? 'success' : 'noMore');
} catch (error) {
// Notify failure
listRef.current?.finishLoad('fail');
}
};
return (
<WebFListView
ref={listRef}
onLoadMore={handleLoadMore}
scrollDirection="vertical"
shrinkWrap={true}
>
{items.map(item => (
<div key={item} className="item">
Item {item}
</div>
))}
</WebFListView>
);
}
```
### Pull-to-Refresh
```tsx
import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui';
import { useRef, useState } from 'react';
function RefreshableList() {
const listRef = useRef<WebFListViewElement>(null);
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const handleRefresh = async () => {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Fetch fresh data
const freshItems = [1, 2, 3, 4, 5];
setItems(freshItems);
// Notify WebFListView that refresh finished
listRef.current?.finishRefresh('success');
} catch (error) {
// Notify failure
listRef.current?.finishRefresh('fail');
}
};
return (
<WebFListView
ref={listRef}
onRefresh={handleRefresh}
scrollDirection="vertical"
shrinkWrap={true}
>
{items.map(item => (
<div key={item} className="item">
Item {item}
</div>
))}
</WebFListView>
);
}
```
### Combined: Pull-to-Refresh + Infinite Scrolling
```tsx
import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui';
import { useRef, useState } from 'react';
function FeedList() {
const listRef = useRef<WebFListViewElement>(null);
const [posts, setPosts] = useState([
{ id: 1, title: 'Post 1', content: 'Content 1' },
{ id: 2, title: 'Post 2', content: 'Content 2' },
{ id: 3, title: 'Post 3', content: 'Content 3' },
]);
const [page, setPage] = useState(1);
const handleRefresh = async () => {
try {
// Fetch latest posts
const response = await fetch('/api/posts?page=1');
const freshPosts = await response.json();
setPosts(freshPosts);
setPage(1);
listRef.current?.finishRefresh('success');
} catch (error) {
listRef.current?.finishRefresh('fail');
}
};
const handleLoadMore = async () => {
try {
const nextPage = page + 1;
// Fetch next page
const response = await fetch(`/api/posts?page=${nextPage}`);
const newPosts = await response.json();
setPosts(prev => [...prev, ...newPosts]);
setPage(nextPage);
// Check if more data exists
const hasMore = newPosts.length > 0;
listRef.current?.finishLoad(hasMore ? 'success' : 'noMore');
} catch (error) {
listRef.current?.finishLoad('fail');
}
};
return (
<WebFListView
ref={listRef}
onRefresh={handleRefresh}
onLoadMore={handleLoadMore}
scrollDirection="vertical"
shrinkWrap={true}
style={{ height: '100vh' }}
>
{posts.map(post => (
<article key={post.id} className="post-card">
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</WebFListView>
);
}
```
## Vue Setup
### Installation
```bash
npm install @openwebf/vue-core-ui
```
### Setup Global Types
In your `src/env.d.ts` or `src/main.ts`:
```typescript
import '@openwebf/vue-core-ui';
```
### Basic Scrolling List
```vue
<template>
<webf-list-view scroll-direction="vertical" :shrink-wrap="true">
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p>${{ product.price }}</p>
</div>
</webf-list-view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const products = ref([
{ id: 1, name: 'Product 1', price: 19.99 },
{ id: 2, name: 'Product 2', price: 29.99 },
{ id: 3, name: 'Product 3', price: 39.99 },
]);
</script>
```
### Infinite Scrolling with Load More
```vue
<template>
<webf-list-view
ref="listRef"
@loadmore="handleLoadMore"
scroll-direction="vertical"
:shrink-wrap="true"
>
<div v-for="item in items" :key="item" class="item">
Item {{ item }}
</div>
</webf-list-view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const listRef = ref<HTMLElement>();
const items = ref([1, 2, 3, 4, 5]);
const page = ref(1);
async function handleLoadMore() {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Fetch next page
const newItems = Array.from(
{ length: 5 },
(_, i) => items.value.length + i + 1
);
items.value.push(...newItems);
page.value++;
// Check if there's more data
const hasMore = page.value < 10;
// Notify WebFListView
(listRef.value as any)?.finishLoad(hasMore ? 'success' : 'noMore');
} catch (error) {
(listRef.value as any)?.finishLoad('fail');
}
}
</script>
```
### Pull-to-Refresh
```vue
<template>
<webf-list-view
ref="listRef"
@refresh="handleRefresh"
scroll-direction="vertical"
:shrink-wrap="true"
>
<div v-for="item in items" :key="item" class="item">
Item {{ item }}
</div>
</webf-list-view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const listRef = ref<HTMLElement>();
const items = ref([1, 2, 3, 4, 5]);
async function handleRefresh() {
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// Fetch fresh data
items.value = [1, 2, 3, 4, 5];
// Notify WebFListView
(listRef.value as any)?.finishRefresh('success');
} catch (error) {
(listRef.value as any)?.finishRefresh('fail');
}
}
</script>
```
## Props and Configuration
### WebFListView Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `scrollDirection` | `'vertical' \| 'horizontal'` | `'vertical'` | Scroll direction for the list |
| `shrinkWrap` | `boolean` | `true` | Whether list should shrink-wrap its contents |
| `onRefresh` / `@refresh` | `() => void \| Promise<void>` | - | Pull-to-refresh callback |
| `onLoadMore` / `@loadmore` | `() => void \| Promise<void>` | - | Infinite scroll callback (triggered near end) |
| `className` / `class` | `string` | - | CSS class names |
| `style` | `object` | - | Inline styles |
### Ref Methods (React) / Element Methods (Vue)
| Method | Signature | Description |
عرض على GitHub