Infinite Scroll Pagination
All major list views — calls, plans, tasks, notifications, and messaging — use server-side offset pagination with client-side infinite scroll. This replaces the previous .limit(500) pattern, reducing initial load times and memory usage.
Server-side pagination
Section titled “Server-side pagination”Every paginated API endpoint accepts two optional query parameters:
| Parameter | Type | Default | Range |
|---|---|---|---|
page | u64 | 0 | 0-based |
page_size | u64 | 50 | 1–100 |
Each endpoint returns a response struct with the data plus pagination metadata:
// Example: CallListResponse (same shape for all modules)pub struct CallListResponse { pub calls: Vec<Call>, pub page: u64, pub page_size: u64, pub total_pages: u64, pub total_count: u64,}Paginated endpoints
Section titled “Paginated endpoints”| Endpoint | Response type | Data field |
|---|---|---|
GET /api/calls | CallListResponse | calls |
GET /api/plans | PlanListResponse | plans |
GET /api/tasks | TaskListResponse | tasks |
GET /api/notifications | NotificationListResponse | notifications |
GET /api/messaging/contacts/:id/messages | ContactMessagesResponse | messages |
GET /api/messaging/unanswered-contacts | UnansweredContactsResponse | contacts |
Server implementation pattern
Section titled “Server implementation pattern”Each API handler runs two queries against the database:
- Count query — lightweight
SELECT COUNT(*)with the same filters (no joins or ordering). - Data query — applies
.offset(page * page_size).limit(page_size)with ordering and joins.
// Countlet total_count: i64 = count_query .select_only() .column_as(Column::Id.count(), "count") .into_tuple() .one(&db) .await? .unwrap_or(0);
let total_pages = (total_count as u64 + page_size - 1) / page_size;
// Datalet items = query .order_by(Column::CreatedAt, Order::Desc) .offset(page * page_size) .limit(page_size) .all(&db) .await?;Client-side infinite scroll
Section titled “Client-side infinite scroll”Every list view follows the same four-signal pattern:
let mut accumulated_items = use_signal(Vec::<Item>::new);let mut current_page = use_signal(|| 0u64);let mut total_pages = use_signal(|| 1u64);let mut is_loading_more = use_signal(|| false);Lifecycle
Section titled “Lifecycle”- Seed — A
use_resourcefetches page 0. Ause_effectseedsaccumulated_itemsfrom the response. - Reset — When filters change (time range, tab, status), a reactive
use_effectclears accumulated items and resetscurrent_pageto 0. - Load more — A
use_futureattaches a JS scroll listener to the scroll container. When the user scrolls within 100px of the bottom, it sends a message viadioxus.send(). - Append — The Rust side receives the message, fetches the next page, and extends
accumulated_items.
Scroll listener setup
Section titled “Scroll listener setup”use_future(move || async move { let mut handle = document::eval(r#" (() => { const el = document.getElementById('my-scroll-container'); if (!el) return; el._paginationHandler = () => { const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 100; if (nearBottom && !el._paginationCooldown) { el._paginationCooldown = true; dioxus.send('load-more'); setTimeout(() => { el._paginationCooldown = false; }, 500); } }; el.addEventListener('scroll', el._paginationHandler); })(); "#); while let Ok(_) = handle.recv::<String>().await { // fetch next page, append to accumulated_items }});The 500ms cooldown prevents duplicate fetches during fast scrolling.
Messaging: upward scroll
Section titled “Messaging: upward scroll”The communication feed loads older messages when you scroll up. The use_paginated_messages hook in src/mods/messaging/hooks/use_paginated_messages.rs handles this with scroll position preservation:
- Capture
scrollHeightandscrollTopbefore prepending. - Prepend older messages to the front of the accumulated list.
- Restore scroll position:
scrollTop = prevScrollTop + (newScrollHeight - prevScrollHeight).
This prevents the viewport from jumping when older messages are inserted above.
Adding pagination to a new module
Section titled “Adding pagination to a new module”- Create a response type with
items,page,page_size,total_pages,total_countfields. - Add
pageandpage_sizequery params to the API endpoint. - Run a count query before the data query with matching filters.
- In the view, add the four signals, seed/reset effects, and scroll listener.
- Show a
WaveIndicatorwhenis_loading_moreis true (replaces the oldSpinner).