@zuzjs/flare 0.2.22 → 0.2.23

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 CHANGED
@@ -238,6 +238,189 @@ stop();
238
238
  messageStream.close();
239
239
  ```
240
240
 
241
+ ### Pagination Patterns (Stream vs Manual)
242
+
243
+ Use one of these based on UX needs:
244
+
245
+ - Stream page window: realtime for the current page; rebuild stream when page changes.
246
+ - Manual cursor paging: deterministic `load more` and stable history.
247
+ - Hybrid: stream first page (latest data) and fetch older pages manually.
248
+
249
+ #### 1) Stream Page Window (offset-based)
250
+
251
+ ```tsx
252
+ import { useEffect, useMemo, useState } from 'react';
253
+ import { collection, Collections, useLiveQuery } from '@zuzjs/flare';
254
+
255
+ const PAGE_SIZE = 25;
256
+
257
+ export function ContactsPagedStream() {
258
+ const [rows, setRows] = useState<any[]>([]);
259
+ const [loading, setLoading] = useState(true);
260
+ const [page, setPage] = useState(0);
261
+
262
+ const contacts = useLiveQuery({
263
+ onData: (data, meta) => {
264
+ setLoading(!meta.ready);
265
+ setRows(data as any[]);
266
+ },
267
+ });
268
+
269
+ const query = useMemo(() => {
270
+ return collection(Collections.Contacts)
271
+ .where({ sheet: '!= null' })
272
+ .orderBy('_seq', 'desc')
273
+ .limit(PAGE_SIZE)
274
+ .offset(page * PAGE_SIZE);
275
+ }, [page]);
276
+
277
+ useEffect(() => {
278
+ contacts.buildStream(query);
279
+ return () => contacts.closeStream();
280
+ }, [contacts, query]);
281
+
282
+ return (
283
+ <div>
284
+ <div>{loading ? 'Loading...' : `Rows: ${rows.length}`}</div>
285
+ <button onClick={() => setPage((p) => Math.max(0, p - 1))}>Prev</button>
286
+ <button onClick={() => setPage((p) => p + 1)}>Next</button>
287
+ </div>
288
+ );
289
+ }
290
+ ```
291
+
292
+ Notes:
293
+
294
+ - This keeps only one live page at a time.
295
+ - Realtime inserts can shift offset pages; this is expected for live data.
296
+
297
+ #### 2) Manual Cursor Pagination (recommended for stable "load more")
298
+
299
+ ```tsx
300
+ import { useEffect, useState } from 'react';
301
+ import { collection, Collections } from '@zuzjs/flare';
302
+
303
+ const PAGE_SIZE = 25;
304
+
305
+ export function ContactsManualPagination() {
306
+ const [rows, setRows] = useState<any[]>([]);
307
+ const [loading, setLoading] = useState(false);
308
+ const [cursor, setCursor] = useState<number | null>(null);
309
+ const [hasMore, setHasMore] = useState(true);
310
+
311
+ const loadInitial = async () => {
312
+ setLoading(true);
313
+ const page = await collection(Collections.Contacts)
314
+ .where({ sheet: '!= null' })
315
+ .orderBy('_seq', 'desc')
316
+ .limit(PAGE_SIZE)
317
+ .get();
318
+
319
+ setRows(page as any[]);
320
+ const last = (page as any[])[(page as any[]).length - 1];
321
+ setCursor(last?._seq ?? null);
322
+ setHasMore((page as any[]).length === PAGE_SIZE);
323
+ setLoading(false);
324
+ };
325
+
326
+ const loadMore = async () => {
327
+ if (!hasMore || cursor == null) return;
328
+ setLoading(true);
329
+
330
+ const page = await collection(Collections.Contacts)
331
+ .where({ sheet: '!= null' })
332
+ .orderBy('_seq', 'desc')
333
+ .startAfter(cursor)
334
+ .limit(PAGE_SIZE)
335
+ .get();
336
+
337
+ const next = page as any[];
338
+ setRows((prev) => [...prev, ...next]);
339
+ const last = next[next.length - 1];
340
+ setCursor(last?._seq ?? null);
341
+ setHasMore(next.length === PAGE_SIZE);
342
+ setLoading(false);
343
+ };
344
+
345
+ useEffect(() => {
346
+ void loadInitial();
347
+ }, []);
348
+
349
+ return (
350
+ <div>
351
+ <div>{loading ? 'Loading...' : `Rows: ${rows.length}`}</div>
352
+ <button onClick={loadMore} disabled={!hasMore || loading}>
353
+ {hasMore ? 'Load more' : 'No more'}
354
+ </button>
355
+ </div>
356
+ );
357
+ }
358
+ ```
359
+
360
+ #### 3) Hybrid (stream latest page + manual history)
361
+
362
+ ```tsx
363
+ import { useEffect, useMemo, useState } from 'react';
364
+ import { collection, Collections, useLiveQuery } from '@zuzjs/flare';
365
+
366
+ const PAGE_SIZE = 25;
367
+
368
+ export function ContactsHybridPagination() {
369
+ const [liveRows, setLiveRows] = useState<any[]>([]);
370
+ const [historyRows, setHistoryRows] = useState<any[]>([]);
371
+ const [historyCursor, setHistoryCursor] = useState<number | null>(null);
372
+ const [ready, setReady] = useState(false);
373
+
374
+ const live = useLiveQuery({
375
+ onData: (data, meta) => {
376
+ setReady(meta.ready);
377
+ setLiveRows(data as any[]);
378
+ },
379
+ });
380
+
381
+ const liveQuery = useMemo(() => {
382
+ return collection(Collections.Contacts)
383
+ .where({ sheet: '!= null' })
384
+ .orderBy('_seq', 'desc')
385
+ .limit(PAGE_SIZE);
386
+ }, []);
387
+
388
+ useEffect(() => {
389
+ live.buildStream(liveQuery);
390
+ return () => live.closeStream();
391
+ }, [live, liveQuery]);
392
+
393
+ const loadOlder = async () => {
394
+ const anchor = historyCursor ?? liveRows[liveRows.length - 1]?._seq;
395
+ if (anchor == null) return;
396
+
397
+ const page = await collection(Collections.Contacts)
398
+ .where({ sheet: '!= null' })
399
+ .orderBy('_seq', 'desc')
400
+ .startAfter(anchor)
401
+ .limit(PAGE_SIZE)
402
+ .get();
403
+
404
+ const next = page as any[];
405
+ setHistoryRows((prev) => [...prev, ...next]);
406
+ setHistoryCursor(next[next.length - 1]?._seq ?? anchor);
407
+ };
408
+
409
+ const allRows = [...liveRows, ...historyRows];
410
+
411
+ return (
412
+ <div>
413
+ <div>{ready ? `Rows: ${allRows.length}` : 'Loading live page...'}</div>
414
+ <button onClick={loadOlder}>Load older</button>
415
+ </div>
416
+ );
417
+ }
418
+ ```
419
+
420
+ Tip:
421
+
422
+ - For feeds/chat, hybrid mode usually gives the best UX: live top of list + stable older history.
423
+
241
424
  #### React.js Example
242
425
 
243
426
  ```tsx