lazy-render-virtual-scroll 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +303 -0
- package/dist/cjs/adapters/react/LazyList.d.ts +12 -0
- package/dist/cjs/adapters/react/LazyList.d.ts.map +1 -0
- package/dist/cjs/adapters/react/useLazyList.d.ts +13 -0
- package/dist/cjs/adapters/react/useLazyList.d.ts.map +1 -0
- package/dist/cjs/core/Engine.d.ts +48 -0
- package/dist/cjs/core/Engine.d.ts.map +1 -0
- package/dist/cjs/core/PrefetchManager.d.ts +13 -0
- package/dist/cjs/core/PrefetchManager.d.ts.map +1 -0
- package/dist/cjs/core/RequestQueue.d.ts +23 -0
- package/dist/cjs/core/RequestQueue.d.ts.map +1 -0
- package/dist/cjs/core/WindowManager.d.ts +20 -0
- package/dist/cjs/core/WindowManager.d.ts.map +1 -0
- package/dist/cjs/core/types.d.ts +20 -0
- package/dist/cjs/core/types.d.ts.map +1 -0
- package/dist/cjs/index.d.ts +11 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +435 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/platform/browser/ScrollObserver.d.ts +25 -0
- package/dist/cjs/platform/browser/ScrollObserver.d.ts.map +1 -0
- package/dist/cjs/utils/debounce.d.ts +5 -0
- package/dist/cjs/utils/debounce.d.ts.map +1 -0
- package/dist/cjs/utils/throttle.d.ts +5 -0
- package/dist/cjs/utils/throttle.d.ts.map +1 -0
- package/dist/esm/adapters/react/LazyList.d.ts +12 -0
- package/dist/esm/adapters/react/LazyList.d.ts.map +1 -0
- package/dist/esm/adapters/react/useLazyList.d.ts +13 -0
- package/dist/esm/adapters/react/useLazyList.d.ts.map +1 -0
- package/dist/esm/core/Engine.d.ts +48 -0
- package/dist/esm/core/Engine.d.ts.map +1 -0
- package/dist/esm/core/PrefetchManager.d.ts +13 -0
- package/dist/esm/core/PrefetchManager.d.ts.map +1 -0
- package/dist/esm/core/RequestQueue.d.ts +23 -0
- package/dist/esm/core/RequestQueue.d.ts.map +1 -0
- package/dist/esm/core/WindowManager.d.ts +20 -0
- package/dist/esm/core/WindowManager.d.ts.map +1 -0
- package/dist/esm/core/types.d.ts +20 -0
- package/dist/esm/core/types.d.ts.map +1 -0
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +425 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/platform/browser/ScrollObserver.d.ts +25 -0
- package/dist/esm/platform/browser/ScrollObserver.d.ts.map +1 -0
- package/dist/esm/utils/debounce.d.ts +5 -0
- package/dist/esm/utils/debounce.d.ts.map +1 -0
- package/dist/esm/utils/throttle.d.ts +5 -0
- package/dist/esm/utils/throttle.d.ts.map +1 -0
- package/dist/index.d.ts +181 -0
- package/examples/chat-ui/Chat.jsx +158 -0
- package/examples/infinite-feed/Feed.jsx +97 -0
- package/examples/react-basic/App.jsx +64 -0
- package/package.json +55 -0
- package/rollup.config.js +39 -0
- package/src/adapters/react/LazyList.tsx +92 -0
- package/src/adapters/react/useLazyList.ts +87 -0
- package/src/core/Engine.ts +134 -0
- package/src/core/PrefetchManager.ts +22 -0
- package/src/core/RequestQueue.ts +69 -0
- package/src/core/WindowManager.ts +49 -0
- package/src/core/types.ts +24 -0
- package/src/index.ts +17 -0
- package/src/platform/browser/ScrollObserver.ts +86 -0
- package/src/utils/debounce.ts +19 -0
- package/src/utils/throttle.ts +19 -0
- package/test/engine.test.ts +136 -0
- package/test/prefetchManager.test.ts +99 -0
- package/test/reactAdapter.test.ts +26 -0
- package/test/requestQueue.test.ts +88 -0
- package/test/testRunner.ts +8 -0
- package/test/windowManager.test.ts +98 -0
- package/tsconfig.json +33 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface EngineConfig {
|
|
4
|
+
itemHeight: number;
|
|
5
|
+
viewportHeight: number;
|
|
6
|
+
bufferSize?: number;
|
|
7
|
+
totalItems?: number;
|
|
8
|
+
}
|
|
9
|
+
interface VisibleRange {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
interface FetchMoreCallback {
|
|
14
|
+
(): Promise<any>;
|
|
15
|
+
}
|
|
16
|
+
interface EngineState {
|
|
17
|
+
scrollTop: number;
|
|
18
|
+
visibleRange: VisibleRange;
|
|
19
|
+
loadedItems: number;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class Engine {
|
|
24
|
+
private config;
|
|
25
|
+
private windowManager;
|
|
26
|
+
private prefetchManager;
|
|
27
|
+
private requestQueue;
|
|
28
|
+
private state;
|
|
29
|
+
private fetchMoreCallback;
|
|
30
|
+
private totalItems;
|
|
31
|
+
constructor(config: EngineConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Update scroll position and recalculate visible range
|
|
34
|
+
*/
|
|
35
|
+
updateScrollPosition(scrollTop: number): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get the current visible range
|
|
38
|
+
*/
|
|
39
|
+
getVisibleRange(): VisibleRange;
|
|
40
|
+
/**
|
|
41
|
+
* Check if more items should be fetched
|
|
42
|
+
*/
|
|
43
|
+
shouldFetchMore(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Fetch more items
|
|
46
|
+
*/
|
|
47
|
+
fetchMore(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Set the fetchMore callback function
|
|
50
|
+
*/
|
|
51
|
+
setFetchMoreCallback(callback: FetchMoreCallback): void;
|
|
52
|
+
/**
|
|
53
|
+
* Update total items count
|
|
54
|
+
*/
|
|
55
|
+
updateTotalItems(count: number): void;
|
|
56
|
+
/**
|
|
57
|
+
* Get current engine state
|
|
58
|
+
*/
|
|
59
|
+
getState(): EngineState;
|
|
60
|
+
/**
|
|
61
|
+
* Update viewport dimensions
|
|
62
|
+
*/
|
|
63
|
+
updateDimensions(viewportHeight: number, itemHeight: number): void;
|
|
64
|
+
/**
|
|
65
|
+
* Cleanup resources
|
|
66
|
+
*/
|
|
67
|
+
cleanup(): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare class WindowManager {
|
|
71
|
+
private itemHeight;
|
|
72
|
+
private viewportHeight;
|
|
73
|
+
private bufferSize;
|
|
74
|
+
constructor(itemHeight: number, viewportHeight: number, bufferSize?: number);
|
|
75
|
+
/**
|
|
76
|
+
* Calculate the visible range based on scroll position
|
|
77
|
+
*/
|
|
78
|
+
calculateVisibleRange(scrollTop: number): VisibleRange;
|
|
79
|
+
/**
|
|
80
|
+
* Update viewport height if it changes
|
|
81
|
+
*/
|
|
82
|
+
updateViewportHeight(height: number): void;
|
|
83
|
+
/**
|
|
84
|
+
* Update item height if it changes
|
|
85
|
+
*/
|
|
86
|
+
updateItemHeight(height: number): void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
declare class PrefetchManager {
|
|
90
|
+
private bufferSize;
|
|
91
|
+
constructor(bufferSize?: number);
|
|
92
|
+
/**
|
|
93
|
+
* Determine if more items should be fetched based on visible range and loaded items
|
|
94
|
+
*/
|
|
95
|
+
shouldPrefetch(visibleEnd: number, totalLoaded: number): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Update buffer size if it changes
|
|
98
|
+
*/
|
|
99
|
+
updateBufferSize(size: number): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
declare class RequestQueue {
|
|
103
|
+
private queue;
|
|
104
|
+
private processing;
|
|
105
|
+
private maxConcurrent;
|
|
106
|
+
constructor(maxConcurrent?: number);
|
|
107
|
+
/**
|
|
108
|
+
* Add a request to the queue
|
|
109
|
+
*/
|
|
110
|
+
add(requestFn: () => Promise<any>): Promise<any>;
|
|
111
|
+
/**
|
|
112
|
+
* Process the queue
|
|
113
|
+
*/
|
|
114
|
+
private processQueue;
|
|
115
|
+
/**
|
|
116
|
+
* Clear the queue
|
|
117
|
+
*/
|
|
118
|
+
clear(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Get the current queue length
|
|
121
|
+
*/
|
|
122
|
+
getLength(): number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare class ScrollObserver {
|
|
126
|
+
private container;
|
|
127
|
+
private callback;
|
|
128
|
+
private options;
|
|
129
|
+
private observer;
|
|
130
|
+
private sentinelElement;
|
|
131
|
+
constructor(container: HTMLElement, callback: (scrollTop: number) => void, options?: Partial<IntersectionObserverInit>);
|
|
132
|
+
/**
|
|
133
|
+
* Start observing scroll events
|
|
134
|
+
*/
|
|
135
|
+
observe(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Handle scroll events
|
|
138
|
+
*/
|
|
139
|
+
private onScroll;
|
|
140
|
+
/**
|
|
141
|
+
* Debounce function for scroll events
|
|
142
|
+
*/
|
|
143
|
+
private debounce;
|
|
144
|
+
/**
|
|
145
|
+
* Disconnect observer and clean up
|
|
146
|
+
*/
|
|
147
|
+
disconnect(): void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface LazyListConfig extends EngineConfig {
|
|
151
|
+
fetchMore: FetchMoreCallback;
|
|
152
|
+
}
|
|
153
|
+
declare const useLazyList: (config: LazyListConfig) => {
|
|
154
|
+
visibleRange: VisibleRange;
|
|
155
|
+
loadedItems: any[];
|
|
156
|
+
isLoading: boolean;
|
|
157
|
+
setContainerRef: (element: HTMLElement | null) => (() => void) | undefined;
|
|
158
|
+
refresh: () => void;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
interface LazyListProps extends EngineConfig {
|
|
162
|
+
fetchMore: FetchMoreCallback;
|
|
163
|
+
renderItem: (item: any, index: number) => React.ReactNode;
|
|
164
|
+
items: any[];
|
|
165
|
+
className?: string;
|
|
166
|
+
style?: React.CSSProperties;
|
|
167
|
+
}
|
|
168
|
+
declare const LazyList: React.ForwardRefExoticComponent<LazyListProps & React.RefAttributes<HTMLDivElement>>;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Debounce function to limit the rate at which a function is called
|
|
172
|
+
*/
|
|
173
|
+
declare function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Throttle function to limit the rate at which a function is called
|
|
177
|
+
*/
|
|
178
|
+
declare function throttle<T extends (...args: any[]) => any>(func: T, limit: number): (...args: Parameters<T>) => void;
|
|
179
|
+
|
|
180
|
+
export { Engine, LazyList, PrefetchManager, RequestQueue, ScrollObserver, WindowManager, debounce, throttle, useLazyList };
|
|
181
|
+
export type { EngineConfig, EngineState, FetchMoreCallback, VisibleRange };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useState, useRef } from 'react';
|
|
2
|
+
import { LazyList, useLazyList } from 'lazy-render';
|
|
3
|
+
|
|
4
|
+
const ChatUI = () => {
|
|
5
|
+
const [messages, setMessages] = useState([]);
|
|
6
|
+
const [inputText, setInputText] = useState('');
|
|
7
|
+
const [userId] = useState('user_' + Math.random().toString(36).substr(2, 9));
|
|
8
|
+
const messagesEndRef = useRef(null);
|
|
9
|
+
|
|
10
|
+
// Initialize with some messages
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
const initialMessages = Array.from({ length: 100 }, (_, i) => ({
|
|
13
|
+
id: i,
|
|
14
|
+
text: `Initial message ${i} - Lorem ipsum dolor sit amet, consectetur adipiscing elit.`,
|
|
15
|
+
sender: i % 3 === 0 ? 'other' : 'self',
|
|
16
|
+
timestamp: new Date(Date.now() - (100 - i) * 60000).toLocaleTimeString(),
|
|
17
|
+
avatar: `https://i.pravatar.cc/32?img=${i % 5 + 1}`
|
|
18
|
+
}));
|
|
19
|
+
setMessages(initialMessages);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const sendMessage = () => {
|
|
23
|
+
if (inputText.trim() === '') return;
|
|
24
|
+
|
|
25
|
+
const newMessage = {
|
|
26
|
+
id: messages.length,
|
|
27
|
+
text: inputText,
|
|
28
|
+
sender: 'self',
|
|
29
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
30
|
+
avatar: `https://i.pravatar.cc/32?img=6`
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
setMessages(prev => [...prev, newMessage]);
|
|
34
|
+
setInputText('');
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleKeyPress = (e) => {
|
|
38
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
sendMessage();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Custom hook usage for advanced control
|
|
45
|
+
const {
|
|
46
|
+
visibleRange,
|
|
47
|
+
setContainerRef,
|
|
48
|
+
scrollToIndex
|
|
49
|
+
} = useLazyList({
|
|
50
|
+
itemHeight: 80, // Average message height
|
|
51
|
+
viewportHeight: 500,
|
|
52
|
+
bufferSize: 5,
|
|
53
|
+
overscan: 3,
|
|
54
|
+
fetchMore: async () => {
|
|
55
|
+
// In a real app, this would fetch older messages
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const renderMessage = (msg, index) => (
|
|
61
|
+
<div
|
|
62
|
+
key={msg.id}
|
|
63
|
+
style={{
|
|
64
|
+
minHeight: '60px',
|
|
65
|
+
borderBottom: '1px solid #eee',
|
|
66
|
+
padding: '12px 16px',
|
|
67
|
+
display: 'flex',
|
|
68
|
+
alignItems: 'flex-start',
|
|
69
|
+
backgroundColor: msg.sender === 'self' ? '#e3f2fd' : '#fff'
|
|
70
|
+
}}
|
|
71
|
+
>
|
|
72
|
+
<img
|
|
73
|
+
src={msg.avatar}
|
|
74
|
+
alt="Avatar"
|
|
75
|
+
style={{
|
|
76
|
+
width: '32px',
|
|
77
|
+
height: '32px',
|
|
78
|
+
borderRadius: '50%',
|
|
79
|
+
marginRight: '12px',
|
|
80
|
+
flexShrink: 0
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
<div style={{ flex: 1 }}>
|
|
84
|
+
<div style={{ fontWeight: 'bold', fontSize: '12px', marginBottom: '4px' }}>
|
|
85
|
+
{msg.sender === 'self' ? 'You' : 'Other User'} • {msg.timestamp}
|
|
86
|
+
</div>
|
|
87
|
+
<div style={{ fontSize: '14px' }}>
|
|
88
|
+
{msg.text}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
96
|
+
<h1 style={{ textAlign: 'center', margin: '10px 0' }}>Chat UI Example</h1>
|
|
97
|
+
|
|
98
|
+
{/* Messages container */}
|
|
99
|
+
<div
|
|
100
|
+
ref={setContainerRef}
|
|
101
|
+
style={{
|
|
102
|
+
flex: 1,
|
|
103
|
+
overflowY: 'auto',
|
|
104
|
+
border: '1px solid #ddd',
|
|
105
|
+
borderRadius: '8px',
|
|
106
|
+
marginBottom: '10px'
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
<div style={{ height: `${visibleRange.start * 80}px` }} />
|
|
110
|
+
|
|
111
|
+
{messages
|
|
112
|
+
.slice(visibleRange.start, visibleRange.end)
|
|
113
|
+
.map((msg, idx) => renderMessage(msg, visibleRange.start + idx))}
|
|
114
|
+
|
|
115
|
+
<div style={{ height: `${Math.max(0, (messages.length - visibleRange.end) * 80)}px` }} />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* Input area */}
|
|
119
|
+
<div style={{ display: 'flex', gap: '8px', padding: '8px' }}>
|
|
120
|
+
<textarea
|
|
121
|
+
value={inputText}
|
|
122
|
+
onChange={(e) => setInputText(e.target.value)}
|
|
123
|
+
onKeyPress={handleKeyPress}
|
|
124
|
+
placeholder="Type a message..."
|
|
125
|
+
style={{
|
|
126
|
+
flex: 1,
|
|
127
|
+
padding: '10px',
|
|
128
|
+
border: '1px solid #ddd',
|
|
129
|
+
borderRadius: '4px',
|
|
130
|
+
resize: 'none',
|
|
131
|
+
minHeight: '40px',
|
|
132
|
+
maxHeight: '100px'
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
<button
|
|
136
|
+
onClick={sendMessage}
|
|
137
|
+
disabled={!inputText.trim()}
|
|
138
|
+
style={{
|
|
139
|
+
padding: '10px 16px',
|
|
140
|
+
backgroundColor: inputText.trim() ? '#2196f3' : '#ccc',
|
|
141
|
+
color: 'white',
|
|
142
|
+
border: 'none',
|
|
143
|
+
borderRadius: '4px',
|
|
144
|
+
cursor: inputText.trim() ? 'pointer' : 'not-allowed'
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
Send
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div style={{ fontSize: '12px', textAlign: 'center', color: '#666' }}>
|
|
152
|
+
Showing {visibleRange.start}-{visibleRange.end} of {messages.length} messages
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default ChatUI;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { LazyList } from 'lazy-render';
|
|
3
|
+
|
|
4
|
+
const InfiniteFeed = () => {
|
|
5
|
+
const [posts, setPosts] = useState([]);
|
|
6
|
+
const [page, setPage] = useState(1);
|
|
7
|
+
const [hasMore, setHasMore] = useState(true);
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
|
|
10
|
+
const fetchMorePosts = async () => {
|
|
11
|
+
if (loading || !hasMore) return;
|
|
12
|
+
|
|
13
|
+
setLoading(true);
|
|
14
|
+
|
|
15
|
+
// Simulate API call
|
|
16
|
+
try {
|
|
17
|
+
await new Promise(resolve => setTimeout(resolve, 800)); // Simulate network delay
|
|
18
|
+
|
|
19
|
+
// Generate new posts
|
|
20
|
+
const newPosts = Array.from({ length: 10 }, (_, i) => ({
|
|
21
|
+
id: (page * 10) + i,
|
|
22
|
+
title: `Post Title ${page * 10 + i}`,
|
|
23
|
+
content: `This is the content for post number ${page * 10 + i}. It contains some interesting information.`,
|
|
24
|
+
author: `Author ${(page * 10) + i % 5}`,
|
|
25
|
+
timestamp: new Date(Date.now() - Math.random() * 10000000000).toISOString(),
|
|
26
|
+
likes: Math.floor(Math.random() * 100),
|
|
27
|
+
comments: Math.floor(Math.random() * 50)
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
setPosts(prev => [...prev, ...newPosts]);
|
|
31
|
+
setPage(prev => prev + 1);
|
|
32
|
+
|
|
33
|
+
// Stop after 100 posts for demo purposes
|
|
34
|
+
if (page >= 10) {
|
|
35
|
+
setHasMore(false);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching posts:', error);
|
|
39
|
+
} finally {
|
|
40
|
+
setLoading(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return newPosts;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const renderPost = (post, index) => (
|
|
47
|
+
<div
|
|
48
|
+
key={post.id}
|
|
49
|
+
style={{
|
|
50
|
+
height: '120px',
|
|
51
|
+
borderBottom: '1px solid #eee',
|
|
52
|
+
padding: '16px',
|
|
53
|
+
backgroundColor: '#fafafa'
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<h3>{post.title}</h3>
|
|
57
|
+
<p style={{ fontSize: '14px', color: '#666', margin: '4px 0' }}>
|
|
58
|
+
by {post.author} • {post.timestamp.substring(0, 10)}
|
|
59
|
+
</p>
|
|
60
|
+
<p style={{ margin: '8px 0' }}>{post.content}</p>
|
|
61
|
+
<div style={{ fontSize: '12px', color: '#888' }}>
|
|
62
|
+
{post.likes} likes • {post.comments} comments
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}>
|
|
69
|
+
<h1>Infinite Feed Example</h1>
|
|
70
|
+
<p>{posts.length} posts loaded</p>
|
|
71
|
+
|
|
72
|
+
<LazyList
|
|
73
|
+
items={posts}
|
|
74
|
+
itemHeight={120}
|
|
75
|
+
viewportHeight={500}
|
|
76
|
+
fetchMore={fetchMorePosts}
|
|
77
|
+
renderItem={renderPost}
|
|
78
|
+
bufferSize={3}
|
|
79
|
+
overscan={2}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
{loading && (
|
|
83
|
+
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
|
84
|
+
Loading more posts...
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{!hasMore && (
|
|
89
|
+
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
|
90
|
+
You've reached the end!
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default InfiniteFeed;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { LazyList } from 'lazy-render';
|
|
3
|
+
|
|
4
|
+
const App = () => {
|
|
5
|
+
// Generate 10000 sample items
|
|
6
|
+
const [items] = useState(() =>
|
|
7
|
+
Array.from({ length: 10000 }, (_, i) => ({
|
|
8
|
+
id: i,
|
|
9
|
+
text: `Item ${i}`,
|
|
10
|
+
timestamp: new Date(Date.now() - Math.random() * 10000000000).toISOString()
|
|
11
|
+
}))
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const [loadedItems, setLoadedItems] = useState(items.slice(0, 100)); // Initially load 100 items
|
|
15
|
+
const [hasMore, setHasMore] = useState(true);
|
|
16
|
+
|
|
17
|
+
const fetchMore = async () => {
|
|
18
|
+
if (loadedItems.length >= items.length) {
|
|
19
|
+
setHasMore(false);
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Simulate API delay
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
25
|
+
|
|
26
|
+
const nextBatch = items.slice(loadedItems.length, loadedItems.length + 50);
|
|
27
|
+
setLoadedItems(prev => [...prev, ...nextBatch]);
|
|
28
|
+
return nextBatch;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderItem = (item, index) => (
|
|
32
|
+
<div
|
|
33
|
+
key={item.id}
|
|
34
|
+
style={{
|
|
35
|
+
height: '60px',
|
|
36
|
+
borderBottom: '1px solid #eee',
|
|
37
|
+
display: 'flex',
|
|
38
|
+
alignItems: 'center',
|
|
39
|
+
paddingLeft: '16px'
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{item.text} - {item.timestamp.substring(0, 10)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ width: '100%', height: '100vh' }}>
|
|
48
|
+
<h1>Basic Lazy Render Example</h1>
|
|
49
|
+
<p>Loading {loadedItems.length} of {items.length} items</p>
|
|
50
|
+
|
|
51
|
+
<LazyList
|
|
52
|
+
items={loadedItems}
|
|
53
|
+
itemHeight={60}
|
|
54
|
+
viewportHeight={400}
|
|
55
|
+
fetchMore={fetchMore}
|
|
56
|
+
renderItem={renderItem}
|
|
57
|
+
bufferSize={5}
|
|
58
|
+
overscan={2}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default App;
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lazy-render-virtual-scroll",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A framework-agnostic virtual scrolling and lazy rendering solution",
|
|
5
|
+
"main": "dist/cjs/index.js",
|
|
6
|
+
"module": "dist/esm/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rollup -c",
|
|
11
|
+
"dev": "rollup -c -w",
|
|
12
|
+
"test": "npx tsx test/testRunner.ts",
|
|
13
|
+
"test:verbose": "npx tsx test/testRunner.ts",
|
|
14
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"virtual-scrolling",
|
|
19
|
+
"lazy-rendering",
|
|
20
|
+
"performance",
|
|
21
|
+
"react",
|
|
22
|
+
"vue",
|
|
23
|
+
"angular",
|
|
24
|
+
"scrolling",
|
|
25
|
+
"infinite-scroll",
|
|
26
|
+
"pagination"
|
|
27
|
+
],
|
|
28
|
+
"author": "Sannu",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/sannuk79/lezzyrender.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/sannuk79/lezzyrender/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/sannuk79/lezzyrender#readme",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"react": "^18.3.1",
|
|
40
|
+
"react-dom": "^18.3.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
44
|
+
"@rollup/plugin-typescript": "^11.0.0",
|
|
45
|
+
"@types/react": "^18.3.28",
|
|
46
|
+
"@types/react-dom": "^18.3.7",
|
|
47
|
+
"rollup": "^3.0.0",
|
|
48
|
+
"rollup-plugin-dts": "^6.0.0",
|
|
49
|
+
"tslib": "^2.4.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"tsx": "^4.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import resolve from '@rollup/plugin-node-resolve';
|
|
2
|
+
import typescript from '@rollup/plugin-typescript';
|
|
3
|
+
import dts from 'rollup-plugin-dts';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
|
7
|
+
|
|
8
|
+
export default [
|
|
9
|
+
{
|
|
10
|
+
input: 'src/index.ts',
|
|
11
|
+
output: [
|
|
12
|
+
{
|
|
13
|
+
file: packageJson.main,
|
|
14
|
+
format: 'cjs',
|
|
15
|
+
sourcemap: true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
file: packageJson.module,
|
|
19
|
+
format: 'esm',
|
|
20
|
+
sourcemap: true
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
plugins: [
|
|
24
|
+
resolve({
|
|
25
|
+
browser: true,
|
|
26
|
+
}),
|
|
27
|
+
typescript({
|
|
28
|
+
tsconfig: './tsconfig.json',
|
|
29
|
+
jsx: 'react' // Use classic JSX transform
|
|
30
|
+
})
|
|
31
|
+
],
|
|
32
|
+
external: ['react', 'react-dom']
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
input: 'src/index.ts',
|
|
36
|
+
output: [{ file: 'dist/index.d.ts', format: 'es' }],
|
|
37
|
+
plugins: [dts()],
|
|
38
|
+
}
|
|
39
|
+
];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { useLazyList } from './useLazyList';
|
|
3
|
+
import { EngineConfig, FetchMoreCallback } from '../../core/types';
|
|
4
|
+
|
|
5
|
+
interface LazyListProps extends EngineConfig {
|
|
6
|
+
fetchMore: FetchMoreCallback;
|
|
7
|
+
renderItem: (item: any, index: number) => React.ReactNode;
|
|
8
|
+
items: any[];
|
|
9
|
+
className?: string;
|
|
10
|
+
style?: React.CSSProperties;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const LazyList = forwardRef<HTMLDivElement, LazyListProps>((props, ref) => {
|
|
14
|
+
const {
|
|
15
|
+
fetchMore,
|
|
16
|
+
renderItem,
|
|
17
|
+
items,
|
|
18
|
+
itemHeight,
|
|
19
|
+
viewportHeight,
|
|
20
|
+
bufferSize,
|
|
21
|
+
className = '',
|
|
22
|
+
style = {},
|
|
23
|
+
...rest
|
|
24
|
+
} = props;
|
|
25
|
+
|
|
26
|
+
const { visibleRange, setContainerRef, isLoading } = useLazyList({
|
|
27
|
+
fetchMore,
|
|
28
|
+
itemHeight,
|
|
29
|
+
viewportHeight,
|
|
30
|
+
bufferSize,
|
|
31
|
+
...rest
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Calculate container height to simulate infinite scroll
|
|
35
|
+
const containerHeight = items.length * itemHeight;
|
|
36
|
+
const visibleItems = items.slice(visibleRange.start, visibleRange.end);
|
|
37
|
+
|
|
38
|
+
// Calculate top padding to maintain scroll position
|
|
39
|
+
const paddingTop = visibleRange.start * itemHeight;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
ref={(el) => {
|
|
44
|
+
setContainerRef(el);
|
|
45
|
+
if (ref) {
|
|
46
|
+
if (typeof ref === 'function') {
|
|
47
|
+
ref(el);
|
|
48
|
+
} else {
|
|
49
|
+
ref.current = el;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}}
|
|
53
|
+
className={`lazy-list ${className}`}
|
|
54
|
+
style={{
|
|
55
|
+
height: `${viewportHeight}px`,
|
|
56
|
+
overflowY: 'auto',
|
|
57
|
+
...style
|
|
58
|
+
}}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{/* Top padding to maintain scroll position */}
|
|
62
|
+
<div style={{ height: `${paddingTop}px` }} />
|
|
63
|
+
|
|
64
|
+
{/* Visible items */}
|
|
65
|
+
{visibleItems.map((item, index) => (
|
|
66
|
+
<div
|
|
67
|
+
key={visibleRange.start + index}
|
|
68
|
+
style={{ height: `${itemHeight}px` }}
|
|
69
|
+
className="lazy-item"
|
|
70
|
+
>
|
|
71
|
+
{renderItem(item, visibleRange.start + index)}
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
|
|
75
|
+
{/* Bottom padding */}
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
height: `${Math.max(0, containerHeight - (visibleRange.end * itemHeight))}px`
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
{/* Loading indicator */}
|
|
83
|
+
{isLoading && (
|
|
84
|
+
<div className="lazy-loading">
|
|
85
|
+
Loading more items...
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
LazyList.displayName = 'LazyList';
|