Last active
February 7, 2026 22:03
-
-
Save modellking/ec9d841dfe07cf18ad1ea069e945d640 to your computer and use it in GitHub Desktop.
Pinia Pagination Lazy Client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { defineStore } from 'pinia'; | |
| import { ref, computed } from 'vue'; | |
| export const useDataStore = defineStore('paginatedData', () => { | |
| const items = ref<any[]>([]); // The "Sparse Array" | |
| const totalCount = ref(0); | |
| const pageSize = 20; | |
| // Track which indices are currently being fetched | |
| const loadingIndices = ref(new Set<number>()); | |
| /** | |
| * Action: Ensures a specific range is loaded. | |
| * Components call this with absolute indices. | |
| */ | |
| async function ensureRange(start: number, end: number) { | |
| // 1. Identify which indices in this range are missing and NOT already loading | |
| const missingIndices = []; | |
| for (let i = start; i <= end; i++) { | |
| if (items.value[i] === undefined && !loadingIndices.value.has(i)) { | |
| missingIndices.push(i); | |
| } | |
| } | |
| if (missingIndices.length === 0) return; | |
| // 2. Map missing indices to required page numbers | |
| const requiredPages = [...new Set( | |
| missingIndices.map(idx => Math.floor(idx / pageSize) + 1) | |
| )]; | |
| // 3. Mark as loading to prevent duplicate requests | |
| requiredPages.forEach(p => { | |
| for (let i = (p - 1) * pageSize; i < p * pageSize; i++) { | |
| loadingIndices.value.add(i); | |
| } | |
| }); | |
| // 4. Fetch pages in parallel | |
| await Promise.all(requiredPages.map(async (page) => { | |
| try { | |
| const response = await fetch(`/api/data?page=${page}&limit=${pageSize}`); | |
| const { data, total } = await response.json(); | |
| totalCount.value = total; | |
| // Insert data into absolute indices | |
| data.forEach((item: any, index: number) => { | |
| const absoluteIndex = (page - 1) * pageSize + index; | |
| items.value[absoluteIndex] = item; | |
| loadingIndices.value.delete(absoluteIndex); | |
| }); | |
| } catch (error) { | |
| console.error(`Failed to load page ${page}`, error); | |
| } | |
| })); | |
| } | |
| /** | |
| * The "Magic" Function: | |
| * Combines data slicing and lazy loading into one. | |
| */ | |
| function connectRange(startRef: Ref<number>, sizeRef: Ref<number>) { | |
| // 1. Automatically fetch whenever the requested window changes | |
| watch( | |
| [startRef, sizeRef], | |
| ([start, size]) => { | |
| ensureRange(start, start + size - 1); | |
| }, | |
| { immediate: true } | |
| ); | |
| // 2. Return a computed slice that the component can use directly | |
| return computed(() => { | |
| const start = startRef.value; | |
| const end = start + sizeRef.value; | |
| return items.value.slice(start, end); | |
| }); | |
| } | |
| return { items, connectRange }; | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <script setup> | |
| import { ref } from 'vue'; | |
| import { useDataStore } from './stores/dataStore'; | |
| const store = useDataStore(); | |
| // Reactive local state | |
| const startIndex = ref(0); | |
| const viewSize = ref(10); | |
| // Single line: The store handles the watch and the loading internally | |
| const visibleItems = store.connectRange(startIndex, viewSize); | |
| </script> | |
| <template> | |
| <div v-for="(item, idx) in visibleItems" :key="idx"> | |
| <span v-if="item">{{ item.name }}</span> | |
| <span v-else>Loading...</span> | |
| </div> | |
| <button @click="startIndex += 10">Next Page</button> | |
| </template> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment