# Server-Side Pagination Implementation Guide
## Overview
This implementation provides server-side pagination with persistent state management for large datasets (5000+ records). It combines PrimeVue's lazy loading capabilities with Pinia stores for state persistence.
## Architecture
### Stores
1. **`usePaginationStore`** - Manages pagination state (page, pageSize, totalRecords, sorting)
2. **`useFiltersStore`** - Manages filter state (existing, enhanced for pagination)
3. **`useLoadingStore`** - Manages loading states (existing, works with pagination)
### Components
1. **`DataTable`** - Enhanced with lazy loading support
2. **`Api`** - Updated with pagination and filtering parameters
## Key Features
✅ **Server-side pagination** - Only loads current page data
✅ **Persistent state** - Page and filter state survive navigation
✅ **Real-time filtering** - Filters reset to page 1 and re-query server
✅ **Sorting support** - Server-side sorting with state persistence
✅ **Loading states** - Integrated with existing loading system
✅ **Performance** - Handles 5000+ records efficiently
## Usage
### Basic Paginated DataTable
```vue
```
## API Implementation
### Required API Method Structure
```javascript
// In your API class
static async getPaginatedData(paginationParams = {}, filters = {}) {
const {
page = 0,
pageSize = 10,
sortField = null,
sortOrder = null
} = paginationParams;
// Build database query with pagination
const offset = page * pageSize;
const limit = pageSize;
// Apply filters to query
const whereClause = buildWhereClause(filters);
// Apply sorting
const orderBy = sortField ? `${sortField} ${sortOrder === -1 ? 'DESC' : 'ASC'}` : '';
// Execute queries
const [data, totalCount] = await Promise.all([
db.query(`SELECT * FROM table ${whereClause} ${orderBy} LIMIT ${limit} OFFSET ${offset}`),
db.query(`SELECT COUNT(*) FROM table ${whereClause}`)
]);
return {
data: data,
totalRecords: totalCount[0].count
};
}
```
### Frappe Framework Implementation
```javascript
static async getPaginatedClientDetails(paginationParams = {}, filters = {}) {
const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams;
// Build Frappe filters
let frappeFilters = {};
Object.keys(filters).forEach(key => {
if (filters[key] && filters[key].value) {
switch (key) {
case 'fullName':
frappeFilters.address_line1 = ['like', `%${filters[key].value}%`];
break;
// Add other filter mappings
}
}
});
// Get total count and paginated data
const [totalCount, records] = await Promise.all([
this.getDocCount("DocType", frappeFilters),
this.getDocsList("DocType", ["*"], frappeFilters, page, pageSize)
]);
// Process and return data
const processedData = records.map(record => ({
id: record.name,
// ... other fields
}));
return {
data: processedData,
totalRecords: totalCount
};
}
```
## DataTable Props
### New Props for Pagination
```javascript
const props = defineProps({
// Existing props...
// Server-side pagination
lazy: {
type: Boolean,
default: false, // Set to true for server-side pagination
},
totalRecords: {
type: Number,
default: 0, // Total records from server
},
onLazyLoad: {
type: Function,
default: null, // Lazy load handler function
},
});
```
### Events
- **`@lazy-load`** - Emitted when pagination/filtering/sorting changes
- **`@page-change`** - Emitted when page changes
- **`@sort-change`** - Emitted when sorting changes
- **`@filter-change`** - Emitted when filters change
## Pagination Store Methods
### Basic Usage
```javascript
const paginationStore = usePaginationStore();
// Initialize pagination for a table
paginationStore.initializeTablePagination("clients", {
rows: 10,
totalRecords: 0,
});
// Update pagination after API response
paginationStore.setTotalRecords("clients", 1250);
// Navigate pages
paginationStore.setPage("clients", 2);
paginationStore.nextPage("clients");
paginationStore.previousPage("clients");
// Get pagination parameters for API calls
const params = paginationStore.getPaginationParams("clients");
// Returns: { page: 2, pageSize: 10, offset: 20, limit: 10, sortField: null, sortOrder: null }
// Get page information for display
const info = paginationStore.getPageInfo("clients");
// Returns: { start: 21, end: 30, total: 1250 }
```
### Advanced Methods
```javascript
// Handle PrimeVue lazy load events
const params = paginationStore.handleLazyLoad("clients", primeVueEvent);
// Set sorting
paginationStore.setSorting("clients", "name", 1); // 1 for ASC, -1 for DESC
// Change rows per page
paginationStore.setRowsPerPage("clients", 25);
// Reset to first page (useful when filters change)
paginationStore.resetToFirstPage("clients");
// Get computed properties
const totalPages = paginationStore.getTotalPages("clients");
const hasNext = paginationStore.hasNextPage("clients");
const hasPrevious = paginationStore.hasPreviousPage("clients");
```
## Filter Integration
Filters work seamlessly with pagination:
```javascript
// When a filter changes, pagination automatically resets to page 1
const handleFilterChange = (fieldName, value) => {
// Update filter
filtersStore.updateTableFilter("clients", fieldName, value);
// Pagination automatically resets to page 1 in DataTable component
// New API call is triggered with updated filters
};
```
## State Persistence
Both pagination and filter states persist across:
- Component re-mounts
- Page navigation
- Browser refresh (if using localStorage)
### Persistence Configuration
```javascript
// In your store, you can add persistence
import { defineStore } from "pinia";
export const usePaginationStore = defineStore("pagination", {
// ... store definition
persist: {
enabled: true,
strategies: [
{
key: "pagination-state",
storage: localStorage, // or sessionStorage
paths: ["tablePagination"],
},
],
},
});
```
## Performance Considerations
### Database Optimization
1. **Indexes** - Ensure filtered and sorted columns are indexed
2. **Query Optimization** - Use efficient WHERE clauses
3. **Connection Pooling** - Handle concurrent requests efficiently
### Frontend Optimization
1. **Debounced Filtering** - Avoid excessive API calls
2. **Loading States** - Provide user feedback during requests
3. **Error Handling** - Gracefully handle API failures
4. **Memory Management** - Clear data when not needed
### Recommended Page Sizes
- **Small screens**: 5-10 records
- **Desktop**: 10-25 records
- **Large datasets**: 25-50 records
- **Avoid**: 100+ records per page
## Error Handling
```javascript
const handleLazyLoad = async (event) => {
try {
isLoading.value = true;
const result = await Api.getPaginatedData(params, filters);
// Success handling
tableData.value = result.data;
totalRecords.value = result.totalRecords;
} catch (error) {
console.error("Pagination error:", error);
// Reset to safe state
tableData.value = [];
totalRecords.value = 0;
// Show user-friendly error
showErrorToast("Failed to load data. Please try again.");
// Optionally retry with fallback parameters
if (event.page > 0) {
paginationStore.setPage(tableName, 0);
// Retry with page 0
}
} finally {
isLoading.value = false;
}
};
```
## Migration from Client-Side
### Before (Client-side)
```javascript
// Old approach - loads all data
onMounted(async () => {
const data = await Api.getAllClients(); // 5000+ records
tableData.value = data;
});
```
### After (Server-side)
```javascript
// New approach - loads only current page
onMounted(async () => {
paginationStore.initializeTablePagination("clients");
await handleLazyLoad({
page: 0,
rows: 10,
// ... other params
});
});
```
## Testing
### Unit Tests
```javascript
import { usePaginationStore } from "@/stores/pagination";
describe("Pagination Store", () => {
it("should initialize pagination correctly", () => {
const store = usePaginationStore();
store.initializeTablePagination("test", { rows: 20 });
const pagination = store.getTablePagination("test");
expect(pagination.rows).toBe(20);
expect(pagination.page).toBe(0);
});
it("should handle page navigation", () => {
const store = usePaginationStore();
store.setTotalRecords("test", 100);
store.setPage("test", 2);
expect(store.getTablePagination("test").page).toBe(2);
expect(store.hasNextPage("test")).toBe(true);
});
});
```
### Integration Tests
```javascript
// Test lazy loading with mock API
const mockLazyLoad = vi.fn().mockResolvedValue({
data: [{ id: 1, name: "Test" }],
totalRecords: 50,
});
// Test component with mocked API
const wrapper = mount(DataTableComponent, {
props: {
lazy: true,
onLazyLoad: mockLazyLoad,
},
});
// Verify API calls
expect(mockLazyLoad).toHaveBeenCalledWith({
page: 0,
rows: 10,
// ... expected parameters
});
```
## Troubleshooting
### Common Issues
1. **Infinite Loading**
- Check API endpoint returns correct totalRecords
- Verify pagination parameters are calculated correctly
2. **Filters Not Working**
- Ensure filter parameters are passed to API correctly
- Check database query includes WHERE clauses
3. **Page State Not Persisting**
- Verify store persistence is configured
- Check localStorage/sessionStorage permissions
4. **Performance Issues**
- Add database indexes for filtered/sorted columns
- Optimize API query efficiency
- Consider reducing page size
### Debug Information
```javascript
// Add debug logging to lazy load handler
const handleLazyLoad = async (event) => {
console.log("Lazy Load Event:", {
page: event.page,
rows: event.rows,
sortField: event.sortField,
sortOrder: event.sortOrder,
filters: event.filters,
timestamp: new Date().toISOString(),
});
// ... rest of implementation
};
```
This implementation provides a robust, performant solution for handling large datasets with persistent pagination and filtering state.