PrimeThink Documentation Help

Pagination

Overview

Live Pages supports server-side pagination to efficiently handle large datasets. Instead of loading all records at once, pagination loads data in smaller chunks, reducing data transfer and improving performance.

Pagination Methods

There are two main approaches to pagination:

  1. Page-based pagination: Uses page and pageSize parameters (recommended for UIs)

  2. Offset-based pagination: Uses limit and offset parameters (more flexible)

Page-Based Pagination

Page-based pagination is the most intuitive approach for user interfaces with "Previous" and "Next" buttons.

Basic Implementation

let currentPage = 1; let pageSize = 20; let hasMorePages = false; async function loadPage(page) { const result = await pt.list({ entityNames: ['task'], filters: { status: 'active' }, page: page, pageSize: pageSize, returnMetadata: true }); currentPage = page; hasMorePages = result.pagination.has_more; return result.entities.filter(e => e.entity_name === 'task'); } // Navigation functions async function goToNextPage() { if (hasMorePages) { const tasks = await loadPage(currentPage + 1); displayTasks(tasks); } } async function goToPreviousPage() { if (currentPage > 1) { const tasks = await loadPage(currentPage - 1); displayTasks(tasks); } } async function goToFirstPage() { const tasks = await loadPage(1); displayTasks(tasks); }

Response Structure with Metadata

When returnMetadata: true is set, the response includes pagination information:

{ entities: [...], // Array of entity objects count: 20, // Number of items in this response pagination: { page: 3, // Current page number page_size: 20, // Items per page has_more: true, // true if more pages exist limit: 20, // Same as page_size offset: 40 // Starting position (calculated) } }

Complete Pagination UI Example

<div class="container mx-auto p-6"> <!-- Content Area --> <div id="tasksList" class="space-y-4 mb-6"> <!-- Tasks will be displayed here --> </div> <!-- Pagination Controls --> <div class="flex justify-center items-center gap-4 bg-white rounded-lg shadow-md p-4"> <button id="firstPageBtn" onclick="goToFirstPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50" > First </button> <button id="prevPageBtn" onclick="goToPreviousPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50" > Previous </button> <span id="pageInfo" class="text-gray-700 font-medium"> Page 1 </span> <button id="nextPageBtn" onclick="goToNextPage()" class="px-3 py-2 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50" > Next </button> </div> </div> <script> let currentPage = 1; let pageSize = 20; let hasMorePages = false; let currentFilters = {}; async function loadPage(page, resetFilters = false) { if (resetFilters) { currentPage = 1; page = 1; } try { const result = await pt.list({ entityNames: ['task'], filters: currentFilters, page: page, pageSize: pageSize, returnMetadata: true }); currentPage = page; hasMorePages = result.pagination.has_more; const tasks = result.entities.filter(e => e.entity_name === 'task'); displayTasks(tasks); updatePaginationControls(); return tasks; } catch (error) { console.error('Error loading page:', error); return []; } } function updatePaginationControls() { document.getElementById('pageInfo').textContent = `Page ${currentPage}`; document.getElementById('firstPageBtn').disabled = currentPage === 1; document.getElementById('prevPageBtn').disabled = currentPage === 1; document.getElementById('nextPageBtn').disabled = !hasMorePages; } async function goToFirstPage() { await loadPage(1); } async function goToPreviousPage() { if (currentPage > 1) { await loadPage(currentPage - 1); } } async function goToNextPage() { if (hasMorePages) { await loadPage(currentPage + 1); } } function displayTasks(tasks) { const html = tasks.map(task => ` <div class="bg-white p-4 rounded shadow"> <p>${task.data.text}</p> </div> `).join(''); document.getElementById('tasksList').innerHTML = html; } // Initialize document.addEventListener('DOMContentLoaded', () => { loadPage(1); }); </script>

Offset-Based Pagination

Offset-based pagination uses limit and offset for more flexible control.

Basic Implementation

let currentOffset = 0; const pageSize = 20; async function loadPageByOffset(offset) { const result = await pt.list({ entityNames: ['task'], filters: { status: 'active' }, limit: pageSize, offset: offset, returnMetadata: true }); currentOffset = offset; return result.entities.filter(e => e.entity_name === 'task'); } // Navigate by offset async function nextPage() { const tasks = await loadPageByOffset(currentOffset + pageSize); displayTasks(tasks); } async function previousPage() { if (currentOffset >= pageSize) { const tasks = await loadPageByOffset(currentOffset - pageSize); displayTasks(tasks); } } async function jumpToPage(pageNumber) { const offset = (pageNumber - 1) * pageSize; const tasks = await loadPageByOffset(offset); displayTasks(tasks); }

Pagination with Filtering

Maintain pagination state when filters change:

class PaginatedList { constructor(pageSize = 20) { this.pageSize = pageSize; this.currentPage = 1; this.currentFilters = {}; this.hasMore = false; } async search(filters, resetPage = true) { if (resetPage) { this.currentPage = 1; } this.currentFilters = filters; return await this.loadCurrentPage(); } async loadCurrentPage() { const result = await pt.list({ entityNames: ['task'], filters: this.currentFilters, page: this.currentPage, pageSize: this.pageSize, returnMetadata: true }); this.hasMore = result.pagination.has_more; return result.entities.filter(e => e.entity_name === 'task'); } async nextPage() { if (this.hasMore) { this.currentPage++; return await this.loadCurrentPage(); } return []; } async previousPage() { if (this.currentPage > 1) { this.currentPage--; return await this.loadCurrentPage(); } return []; } async goToPage(page) { this.currentPage = page; return await this.loadCurrentPage(); } } // Usage const taskList = new PaginatedList(20); // Load first page await taskList.search({ status: 'active' }); // Change filters (resets to page 1) await taskList.search({ status: 'active', priority: 'high' }); // Navigate pages (maintains filters) await taskList.nextPage(); await taskList.previousPage();

Infinite Scroll

Implement infinite scrolling for a seamless user experience:

class InfiniteScrollManager { constructor(pageSize = 20) { this.pageSize = pageSize; this.currentPage = 0; this.loading = false; this.hasMore = true; this.allData = []; } async loadMore(filters = {}) { if (this.loading || !this.hasMore) return; this.loading = true; this.currentPage++; try { const result = await pt.list({ entityNames: ['task'], filters: filters, page: this.currentPage, pageSize: this.pageSize, returnMetadata: true }); const newData = result.entities.filter(e => e.entity_name === 'task'); this.hasMore = result.pagination.has_more; this.allData = [...this.allData, ...newData]; this.appendToUI(newData); return newData; } catch (error) { console.error('Error loading more data:', error); return []; } finally { this.loading = false; } } appendToUI(newData) { const container = document.getElementById('tasksList'); newData.forEach(task => { const taskElement = document.createElement('div'); taskElement.className = 'bg-white p-4 rounded shadow mb-2'; taskElement.textContent = task.data.text; container.appendChild(taskElement); }); } reset() { this.currentPage = 0; this.hasMore = true; this.allData = []; document.getElementById('tasksList').innerHTML = ''; } } // Setup infinite scroll const infiniteScroll = new InfiniteScrollManager(20); // Load initial data await infiniteScroll.loadMore({ status: 'active' }); // Setup scroll listener window.addEventListener('scroll', () => { const scrollPosition = window.innerHeight + window.scrollY; const threshold = document.body.offsetHeight - 1000; if (scrollPosition >= threshold) { infiniteScroll.loadMore({ status: 'active' }); } });

Pagination with Caching

Improve performance by caching previously loaded pages:

class CachedPaginationManager { constructor(pageSize = 20) { this.pageSize = pageSize; this.currentPage = 1; this.currentFilters = {}; this.cache = new Map(); } async loadPage(page, filters = {}) { const cacheKey = this.getCacheKey(page, filters); // Check cache first if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } // Load from server const result = await pt.list({ entityNames: ['task'], filters: filters, page: page, pageSize: this.pageSize, returnMetadata: true }); const data = { entities: result.entities.filter(e => e.entity_name === 'task'), hasMore: result.pagination.has_more }; // Cache the result this.cache.set(cacheKey, data); return data; } getCacheKey(page, filters) { return `${page}-${JSON.stringify(filters)}`; } clearCache() { this.cache.clear(); } async search(filters, resetPage = true) { if (resetPage) { this.currentPage = 1; } this.currentFilters = filters; return await this.loadPage(this.currentPage, filters); } async nextPage() { this.currentPage++; return await this.loadPage(this.currentPage, this.currentFilters); } async previousPage() { if (this.currentPage > 1) { this.currentPage--; return await this.loadPage(this.currentPage, this.currentFilters); } return await this.loadPage(this.currentPage, this.currentFilters); } }

Cursor-Based Pagination

For very large datasets, cursor-based pagination can be more efficient:

class CursorPaginationManager { constructor(pageSize = 20) { this.pageSize = pageSize; this.cursors = []; // Store cursors for each page this.currentPage = 0; } async loadFirstPage(filters = {}) { this.currentPage = 1; this.cursors = []; const result = await pt.list({ entityNames: ['task'], filters: filters, limit: this.pageSize, offset: 0, returnMetadata: true }); const data = result.entities.filter(e => e.entity_name === 'task'); if (data.length > 0) { // Store the last item's ID as cursor for next page this.cursors[1] = data[data.length - 1].id; } return { data, hasMore: result.pagination.has_more }; } async loadNextPage(filters = {}) { if (this.cursors[this.currentPage]) { this.currentPage++; // Use the cursor to get items after the last loaded item const enhancedFilters = { ...filters, id: { $gt: this.cursors[this.currentPage - 1] } }; const result = await pt.list({ entityNames: ['task'], filters: enhancedFilters, limit: this.pageSize, offset: 0, returnMetadata: true }); const data = result.entities.filter(e => e.entity_name === 'task'); if (data.length > 0) { this.cursors[this.currentPage] = data[data.length - 1].id; } return { data, hasMore: result.pagination.has_more }; } return { data: [], hasMore: false }; } } // Usage const cursor = new CursorPaginationManager(20); const firstPage = await cursor.loadFirstPage({ status: 'active' }); const secondPage = await cursor.loadNextPage({ status: 'active' });

Pagination Best Practices

1. Use Reasonable Page Sizes

// ✅ GOOD: Reasonable page size const OPTIMAL_PAGE_SIZE = 20; // ❌ AVOID: Too large const TOO_LARGE = 1000; // ❌ AVOID: Too small (too many requests) const TOO_SMALL = 5;

2. Reset to Page 1 When Filters Change

// ✅ GOOD: Reset page when filters change async function applyFilters(newFilters) { currentPage = 1; currentFilters = newFilters; await loadPage(currentPage); }

3. Show Loading States

async function loadPage(page) { // Show loading indicator document.getElementById('loading').style.display = 'block'; try { const result = await pt.list({ entityNames: ['task'], page: page, pageSize: 20, returnMetadata: true }); displayTasks(result.entities); } finally { // Hide loading indicator document.getElementById('loading').style.display = 'none'; } }

4. Disable Navigation During Loading

let isLoading = false; async function loadPage(page) { if (isLoading) return; isLoading = true; updateButtonStates(); try { const result = await pt.list({ entityNames: ['task'], page: page, pageSize: 20, returnMetadata: true }); displayTasks(result.entities); } finally { isLoading = false; updateButtonStates(); } } function updateButtonStates() { const buttons = document.querySelectorAll('.pagination-btn'); buttons.forEach(btn => { btn.disabled = isLoading; }); }

5. Handle Empty Results

function displayTasks(tasks) { const container = document.getElementById('tasksList'); if (tasks.length === 0) { container.innerHTML = ` <div class="text-center py-8 text-gray-500"> No tasks found </div> `; return; } container.innerHTML = tasks.map(task => ` <div class="bg-white p-4 rounded shadow mb-2"> ${task.data.text} </div> `).join(''); }

Performance Optimization

1. Avoid Offset for Very Large Datasets

For datasets with millions of records, offset-based pagination can become slow:

// ❌ SLOW: Large offset const result = await pt.list({ entityNames: ['task'], limit: 20, offset: 1000000 // Very slow for large offsets }); // ✅ BETTER: Use cursor-based pagination const result = await pt.list({ entityNames: ['task'], filters: { id: { $gt: lastSeenId } }, limit: 20 });

2. Cache Page Results

const pageCache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes async function loadPageWithCache(page, filters) { const cacheKey = `${page}-${JSON.stringify(filters)}`; const cached = pageCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } const result = await pt.list({ entityNames: ['task'], filters: filters, page: page, pageSize: 20, returnMetadata: true }); pageCache.set(cacheKey, { data: result, timestamp: Date.now() }); return result; }

3. Prefetch Next Page

async function loadPageWithPrefetch(page, filters) { // Load current page const currentPromise = pt.list({ entityNames: ['task'], filters: filters, page: page, pageSize: 20, returnMetadata: true }); // Prefetch next page in parallel const nextPromise = pt.list({ entityNames: ['task'], filters: filters, page: page + 1, pageSize: 20, returnMetadata: true }); // Wait for current page const current = await currentPromise; // Cache next page result nextPromise.then(result => { cachePageResult(page + 1, filters, result); }); return current; }

Next Steps

22 October 2025