--- 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
Item 1
Item 2
Item 3
{/* Each item is a direct child */}
``` ### ❌ WRONG: Wrapped in Container ```jsx
{/* DON'T wrap items in a container div */}
Item 1
Item 2
Item 3
``` **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 ( {products.map(product => ( // ✅ Each item is a direct child

{product.name}

${product.price}

))}
); } ``` ### Infinite Scrolling with Load More ```tsx import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; import { useRef, useState } from 'react'; function InfiniteList() { const listRef = useRef(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 ( {items.map(item => (
Item {item}
))}
); } ``` ### Pull-to-Refresh ```tsx import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; import { useRef, useState } from 'react'; function RefreshableList() { const listRef = useRef(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 ( {items.map(item => (
Item {item}
))}
); } ``` ### 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(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 ( {posts.map(post => (

{post.title}

{post.content}

))}
); } ``` ## 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 ``` ### Infinite Scrolling with Load More ```vue ``` ### Pull-to-Refresh ```vue ``` ## 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` | - | Pull-to-refresh callback | | `onLoadMore` / `@loadmore` | `() => void \| Promise` | - | 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 | |--------|-----------|-------------| | `finishRefresh` | `(result?: 'success' \| 'fail' \| 'noMore') => void` | Call after refresh completes | | `finishLoad` | `(result?: 'success' \| 'fail' \| 'noMore') => void` | Call after load-more completes | | `resetHeader` | `() => void` | Reset refresh header to initial state | | `resetFooter` | `() => void` | Reset load-more footer to initial state | ### Result Values - `'success'` - Operation succeeded, more data available - `'fail'` - Operation failed (shows error state) - `'noMore'` - No more data to load (hides footer/shows "no more" message) ## Common Patterns ### Pattern 1: Search with Results List ```tsx import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; import { useRef, useState } from 'react'; function SearchResults() { const listRef = useRef(null); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const handleSearch = async (searchQuery: string) => { setQuery(searchQuery); // Fetch search results const response = await fetch(`/api/search?q=${searchQuery}`); const data = await response.json(); setResults(data.results); }; const handleLoadMore = async () => { try { const response = await fetch( `/api/search?q=${query}&offset=${results.length}` ); const data = await response.json(); setResults(prev => [...prev, ...data.results]); listRef.current?.finishLoad( data.results.length > 0 ? 'success' : 'noMore' ); } catch (error) { listRef.current?.finishLoad('fail'); } }; return (
handleSearch(e.target.value)} /> {results.map(result => (
{result.title}
))}
); } ``` ### Pattern 2: Chat Messages (Reverse List) For chat-style UIs where new messages appear at the bottom: ```tsx import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; import { useRef, useState, useEffect } from 'react'; function ChatMessages() { const listRef = useRef(null); const [messages, setMessages] = useState([ { id: 1, text: 'Hello', timestamp: Date.now() }, { id: 2, text: 'Hi there!', timestamp: Date.now() }, ]); // Load older messages when scrolling to top const handleLoadMore = async () => { try { // In real app, fetch older messages before first message const oldestId = messages[0]?.id; const response = await fetch(`/api/messages?before=${oldestId}`); const olderMessages = await response.json(); // Prepend older messages setMessages(prev => [...olderMessages, ...prev]); listRef.current?.finishLoad( olderMessages.length > 0 ? 'success' : 'noMore' ); } catch (error) { listRef.current?.finishLoad('fail'); } }; return ( {messages.map(message => (
{message.text}
))}
); } ``` ### Pattern 3: Horizontal Scrolling Gallery ```tsx import { WebFListView } from '@openwebf/react-core-ui'; function ImageGallery({ images }) { return ( {images.map(image => ( {image.title} ))} ); } ``` ## Common Mistakes ### Mistake 1: Wrapping Items in Container ```jsx // ❌ WRONG - Items wrapped in container div
{items.map(item =>
{item}
)}
// ✅ CORRECT - Items are direct children {items.map(item =>
{item}
)}
``` **Why**: Flutter's ListView needs direct children for view recycling to work. ### Mistake 2: Forgetting to Call finishLoad/finishRefresh ```tsx // ❌ WRONG - Never calls finishLoad const handleLoadMore = async () => { const data = await fetchData(); setItems(prev => [...prev, ...data]); // finishLoad never called - loading indicator stuck! }; // ✅ CORRECT - Always call finishLoad const handleLoadMore = async () => { try { const data = await fetchData(); setItems(prev => [...prev, ...data]); listRef.current?.finishLoad('success'); } catch (error) { listRef.current?.finishLoad('fail'); } }; ``` ### Mistake 3: Not Handling "No More Data" State ```tsx // ❌ WRONG - Always calls 'success', even when no data const handleLoadMore = async () => { const data = await fetchData(); setItems(prev => [...prev, ...data]); listRef.current?.finishLoad('success'); // Wrong if data is empty! }; // ✅ CORRECT - Check if more data exists const handleLoadMore = async () => { const data = await fetchData(); setItems(prev => [...prev, ...data]); // Tell WebFListView there's no more data listRef.current?.finishLoad(data.length > 0 ? 'success' : 'noMore'); }; ``` ### Mistake 4: Timeout Issues (Taking Too Long) WebFListView has a 4-second timeout for refresh/load operations. If your operation takes longer, it will auto-fail. ```tsx // ❌ WRONG - Operation takes 10 seconds (will timeout) const handleRefresh = async () => { await new Promise(resolve => setTimeout(resolve, 10000)); // 10s listRef.current?.finishRefresh('success'); // Too late! }; // ✅ CORRECT - Complete within 4 seconds const handleRefresh = async () => { try { // Use Promise.race to enforce timeout await Promise.race([ fetchData(), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3500) ) ]); listRef.current?.finishRefresh('success'); } catch (error) { listRef.current?.finishRefresh('fail'); } }; ``` ## Performance Tips ### 1. Use Keys Correctly Always provide unique, stable keys for list items: ```tsx // ✅ GOOD - Stable ID from data {items.map(item =>
{item.name}
)} // ❌ BAD - Index as key (can cause bugs with dynamic lists) {items.map((item, index) =>
{item.name}
)} ``` ### 2. Avoid Heavy Computations in Render ```tsx // ❌ BAD - Heavy computation on every render {items.map(item => (
{expensiveCalculation(item)} {/* Calculated on every render! */}
))}
// ✅ GOOD - Memoize or pre-calculate const processedItems = useMemo( () => items.map(item => ({ ...item, computed: expensiveCalculation(item) })), [items] ); {processedItems.map(item => (
{item.computed}
))}
``` ### 3. Optimize Item Components ```tsx // ✅ GOOD - Memoized item component const ListItem = memo(({ item }) => (

{item.title}

{item.description}

)); function MyList({ items }) { return ( {items.map(item => ( ))} ); } ``` ### 4. Set Explicit Height for Scrolling For full-screen lists, set explicit height: ```tsx {/* items */} ``` ## Debugging ### Check if finishLoad/finishRefresh is Called Add logging to verify callbacks execute: ```tsx const handleLoadMore = async () => { console.log('🔄 Load more started'); try { const data = await fetchData(); setItems(prev => [...prev, ...data]); console.log('✅ Load more finished:', data.length, 'items'); listRef.current?.finishLoad(data.length > 0 ? 'success' : 'noMore'); } catch (error) { console.error('❌ Load more failed:', error); listRef.current?.finishLoad('fail'); } }; ``` ### Verify Direct Children Structure Use React DevTools or Vue DevTools to inspect the rendered structure. Ensure items are direct children of ``: ```html
Item 1
Item 2
Item 3
Item 1
Item 2
``` ## Resources - **React Core UI Package**: `/Users/andycall/workspace/webf/packages/react-core-ui/README.md` - **Vue Core UI Package**: `/Users/andycall/workspace/webf/packages/vue-core-ui/README.md` - **Complete Examples**: See `examples.md` in this skill - **npm Packages**: - https://www.npmjs.com/package/@openwebf/react-core-ui - https://www.npmjs.com/package/@openwebf/vue-core-ui ## Key Takeaways ✅ **DO**: - Use `WebFListView` for long scrolling lists - Make each item a direct child (not wrapped in container) - Always call `finishLoad` / `finishRefresh` after operations - Use `'noMore'` result when no more data exists - Provide unique, stable keys for list items - Set explicit height for full-screen lists ❌ **DON'T**: - Wrap items in a container div - Forget to call finish methods (loading indicator gets stuck) - Use index as key for dynamic lists - Let operations exceed 4-second timeout - Use heavy computations in render without memoization - Expect browser-style virtualization libraries (not needed!) ## Quick Reference ```bash # Install packages npm install @openwebf/react-core-ui # React npm install @openwebf/vue-core-ui # Vue ``` ```tsx // React - Basic pattern import { WebFListView, WebFListViewElement } from '@openwebf/react-core-ui'; const listRef = useRef(null); { await refreshData(); listRef.current?.finishRefresh('success'); }} onLoadMore={async () => { const hasMore = await loadMore(); listRef.current?.finishLoad(hasMore ? 'success' : 'noMore'); }} > {items.map(item =>
{item.name}
)}
``` ```vue
{{ item.name }}
```