Skip to content

Instantly share code, notes, and snippets.

@modellking
Last active February 7, 2026 22:03
Show Gist options
  • Select an option

  • Save modellking/ec9d841dfe07cf18ad1ea069e945d640 to your computer and use it in GitHub Desktop.

Select an option

Save modellking/ec9d841dfe07cf18ad1ea069e945d640 to your computer and use it in GitHub Desktop.
Pinia Pagination Lazy Client
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 };
});
<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