DevelopmentResource Framework
ResourceStore
ResourceStore
The useResourceStore is a Zustand store for managing paginated resource data with IndexedDB caching support. It's designed to work with /v2/[resource] routes and provides efficient data management for large datasets.
Overview
The ResourceStore provides:
- Pagination Management: Handles page size and page number state per resource
- IndexedDB Caching: Automatic caching of paginated data with 5-minute expiry
- Separate Storage: Each resource gets its own IndexedDB database
- Offset Calculation: Automatic calculation of
limitandoffsetfor API calls
Key Concepts
Pagination Formula
limit = pageSize
offset = pageSize * pageNumber
```typescript
**Example**: If page size is 100 and we're on page 3:
- `limit`: 100
- `offset`: 300 (100 × 3)
This fetches items 301-400 from the database.
### IndexedDB Structure
Each resource gets its own database:
- **Database Name**: `resource_cache_${resourceName}` (e.g., `resource_cache_invoices`)
- **Store Name**: `pages`
- **Key**: `pageNumber` (0-indexed)
- **Value**: `{ pageNumber, data, pageSize, fetchedAt }`
## Installation
The store is exported from the main stores module:
```typescript
import { useResourceStore } from "@/lib/stores";
```typescript
## Basic Usage
### Setting Up Pagination
```typescript
import { useResourceStore, getPaginationParams } from "@/lib/stores";
function InvoicesList() {
const { setResourceData, getResourceData } = useResourceStore();
const resourceName = "invoices";
const pageSize = 100;
const pageNumber = 0; // First page
// Calculate API parameters
const { limit, offset } = getPaginationParams(pageSize, pageNumber);
console.log(limit); // 100
console.log(offset); // 0
// Fetch data
const fetchData = async () => {
const response = await fetch("/api/fetch/data", {
method: "POST",
body: JSON.stringify({
table_name: "invoices",
limit, // 100
offset, // 0
})
});
const result = await response.json();
// Store in Zustand + IndexedDB
setResourceData(resourceName, result.data, pageSize, pageNumber);
};
// Get cached data
const cached = getResourceData(resourceName);
return <InvoiceTable data={cached?.data || []} />;
}
```typescript
### Pagination Controls
```typescript
import { useResourceStore, getPaginationParams } from "@/lib/stores";
import { useState } from "react";
function PaginatedTable() {
const { setResourceData, getResourceData, setPageNumber } = useResourceStore();
const [currentPage, setCurrentPage] = useState(0);
const pageSize = 100;
const resourceName = "customers";
const fetchPage = async (pageNum: number) => {
const { limit, offset } = getPaginationParams(pageSize, pageNum);
const response = await fetch("/api/fetch/data", {
method: "POST",
body: JSON.stringify({
table_name: "customers",
limit,
offset,
})
});
const result = await response.json();
setResourceData(resourceName, result.data, pageSize, pageNum);
setPageNumber(resourceName, pageNum);
setCurrentPage(pageNum);
};
const handleNextPage = () => {
fetchPage(currentPage + 1);
};
const handlePrevPage = () => {
if (currentPage > 0) {
fetchPage(currentPage - 1);
}
};
const cached = getResourceData(resourceName);
return (
<div>
<Table data={cached?.data || []} />
<Pagination>
<Button onClick={handlePrevPage} disabled={currentPage === 0}>
Previous
</Button>
<span>Page {currentPage + 1}</span>
<Button onClick={handleNextPage}>
Next
</Button>
</Pagination>
</div>
);
}
```typescript
## API Reference
### Store Methods
#### setResourceData
Store data for a resource with pagination info:
```typescript
setResourceData(
resourceName: string,
data: any[],
pageSize: number,
pageNumber: number,
totalCount?: number
): void
```typescript
**Parameters**:
- `resourceName`: Unique identifier for the resource (e.g., "invoices")
- `data`: Array of items for this page
- `pageSize`: Number of items per page
- `pageNumber`: Current page (0-indexed)
- `totalCount`: Optional total count of items across all pages
**Example**:
```typescript
setResourceData("invoices", invoiceArray, 100, 2, 500);
// Stores page 3 (pageNumber 2) of invoices
// Page has 100 items, total is 500 items
```typescript
#### getResourceData
Get cached data for a resource:
```typescript
getResourceData(resourceName: string): ResourcePageData | undefined
```typescript
**Returns**:
```typescript
{
data: any[];
pageSize: number;
pageNumber: number;
totalCount?: number;
fetchedAt: number;
}
```typescript
**Example**:
```typescript
const cached = getResourceData("invoices");
if (cached) {
console.log(`Page ${cached.pageNumber + 1}`);
console.log(`${cached.data.length} items`);
console.log(`Fetched ${Date.now() - cached.fetchedAt}ms ago`);
}
```typescript
#### setPageSize
Update page size for a resource:
```typescript
setPageSize(resourceName: string, size: number): void
```typescript
**Example**:
```typescript
setPageSize("invoices", 50); // Change from 100 to 50 items per page
```typescript
#### setPageNumber
Update current page for a resource:
```typescript
setPageNumber(resourceName: string, page: number): void
```typescript
**Example**:
```typescript
setPageNumber("invoices", 5); // Go to page 6 (0-indexed)
```typescript
#### clearResourceData
Clear cached data for a specific resource:
```typescript
clearResourceData(resourceName: string): void
```typescript
**Example**:
```typescript
clearResourceData("invoices"); // Clears Zustand + IndexedDB
```typescript
#### clearAllResourceData
Clear cached data for all resources:
```typescript
clearAllResourceData(): void
```typescript
**Example**:
```typescript
clearAllResourceData(); // Clears all cached resources
```typescript
### Helper Functions
#### getPaginationParams
Calculate `limit` and `offset` for API calls:
```typescript
getPaginationParams(
pageSize: number,
pageNumber: number
): { limit: number; offset: number }
```typescript
**Example**:
```typescript
// Page 1, 100 items per page
getPaginationParams(100, 0);
// { limit: 100, offset: 0 }
// Page 5, 50 items per page
getPaginationParams(50, 4);
// { limit: 50, offset: 200 }
// Page 10, 25 items per page
getPaginationParams(25, 9);
// { limit: 25, offset: 225 }
```typescript
## IndexedDB Functions
These functions are called automatically by the store but can be used directly if needed:
### saveToIndexedDB
```typescript
saveToIndexedDB(
resourceName: string,
pageNumber: number,
data: any[],
pageSize: number
): Promise<void>
```typescript
### loadFromIndexedDB
```typescript
loadFromIndexedDB(
resourceName: string,
pageNumber: number
): Promise<ResourcePageData | null>
```typescript
### clearIndexedDB
```typescript
clearIndexedDB(resourceName: string): Promise<void>
```typescript
## Advanced Usage
### Cache-First Strategy
Check IndexedDB before fetching from API:
```typescript
import { useResourceStore, loadFromIndexedDB, getPaginationParams } from "@/lib/stores";
async function fetchWithCache(resourceName: string, pageSize: number, pageNumber: number) {
const { setResourceData } = useResourceStore.getState();
// Try to load from cache first
const cached = await loadFromIndexedDB(resourceName, pageNumber);
if (cached) {
// Cache hit - use cached data
setResourceData(resourceName, cached.data, cached.pageSize, pageNumber);
return cached.data;
}
// Cache miss - fetch from API
const { limit, offset } = getPaginationParams(pageSize, pageNumber);
const response = await fetch("/api/fetch/data", {
method: "POST",
body: JSON.stringify({
table_name: resourceName,
limit,
offset,
})
});
const result = await response.json();
// Store in cache
setResourceData(resourceName, result.data, pageSize, pageNumber);
return result.data;
}
```typescript
### Dynamic Page Sizes
Let users change page size:
```typescript
function TableWithPageSize() {
const { setPageSize, getResourceData } = useResourceStore();
const [size, setSize] = useState(100);
const resourceName = "products";
const handlePageSizeChange = (newSize: number) => {
setSize(newSize);
setPageSize(resourceName, newSize);
// Refetch with new page size
fetchPage(0, newSize);
};
return (
<div>
<select value={size} onChange={(e) => handlePageSizeChange(Number(e.target.value))}>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
<Table data={getResourceData(resourceName)?.data || []} />
</div>
);
}
```typescript
### Total Pages Calculation
```typescript
function PaginationWithTotal() {
const cached = getResourceData("invoices");
if (!cached?.totalCount) {
return <Pagination />;
}
const totalPages = Math.ceil(cached.totalCount / cached.pageSize);
const currentPage = cached.pageNumber + 1;
return (
<div>
<span>Page {currentPage} of {totalPages}</span>
<span>({cached.totalCount} total items)</span>
</div>
);
}
```typescript
### Prefetching Next Page
Improve UX by prefetching the next page:
```typescript
function SmartPagination() {
const { setResourceData } = useResourceStore();
const [currentPage, setCurrentPage] = useState(0);
const pageSize = 100;
const resourceName = "orders";
const prefetchNextPage = async (pageNum: number) => {
const { limit, offset } = getPaginationParams(pageSize, pageNum);
const response = await fetch("/api/fetch/data", {
method: "POST",
body: JSON.stringify({
table_name: resourceName,
limit,
offset,
})
});
const result = await response.json();
setResourceData(resourceName, result.data, pageSize, pageNum);
};
useEffect(() => {
// Prefetch next page in background
prefetchNextPage(currentPage + 1);
}, [currentPage]);
return <Table />;
}
```typescript
## Type Definitions
### ResourcePageData
```typescript
interface ResourcePageData<T = any> {
data: T[];
pageSize: number;
pageNumber: number;
totalCount?: number;
fetchedAt: number;
}
```typescript
### ResourceStore
```typescript
interface ResourceStore {
resourceData: Record<string, ResourcePageData>;
setPageSize: (resourceName: string, size: number) => void;
setPageNumber: (resourceName: string, page: number) => void;
setResourceData: (
resourceName: string,
data: any[],
pageSize: number,
pageNumber: number,
totalCount?: number
) => void;
getResourceData: (resourceName: string) => ResourcePageData | undefined;
clearResourceData: (resourceName: string) => void;
clearAllResourceData: () => void;
}
```typescript
## Best Practices
### 1. Use Consistent Resource Names
Match your resource names with your route names:
```typescript
// ✅ Good - matches /v2/invoices
const resourceName = "invoices";
// ❌ Bad - inconsistent naming
const resourceName = "invoice_data";
```typescript
### 2. Handle Loading States
Show loading indicators while fetching:
```typescript
const [isLoading, setIsLoading] = useState(false);
const fetchPage = async (pageNum: number) => {
setIsLoading(true);
try {
// ... fetch logic
} finally {
setIsLoading(false);
}
};
```typescript
### 3. Clear Cache on Logout
```typescript
function LogoutButton() {
const { clearAllResourceData } = useResourceStore();
const handleLogout = () => {
clearAllResourceData(); // Clear all cached data
// ... logout logic
};
return <Button onClick={handleLogout}>Logout</Button>;
}
```typescript
### 4. Validate Page Numbers
```typescript
const handlePageChange = (pageNum: number) => {
if (pageNum < 0) return; // Don't go below 0
const cached = getResourceData(resourceName);
if (cached?.totalCount) {
const maxPage = Math.ceil(cached.totalCount / cached.pageSize) - 1;
if (pageNum > maxPage) return; // Don't exceed max
}
fetchPage(pageNum);
};
```typescript
### 5. Combine with ResourceProvider
Use both together for complete data management:
```typescript
import { useResourceContext } from "@/packages/resource-framework";
import { useResourceStore } from "@/lib/stores";
function DataManagementComponent() {
// User-level resources (preferences, scopes, notifications, flags)
const { hasScope, flags } = useResourceContext();
// Paginated table data
const { getResourceData } = useResourceStore();
const tableData = getResourceData("invoices");
if (!hasScope("view_invoices")) {
return <AccessDenied />;
}
return <InvoiceTable data={tableData?.data || []} />;
}
```typescript
## Cache Expiry
The default cache expiry is **5 minutes** (300,000ms). Data older than this is considered stale and will be refetched.
```typescript
const CACHE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
```typescript
To check if cached data is fresh:
```typescript
const cached = getResourceData("invoices");
if (cached) {
const age = Date.now() - cached.fetchedAt;
const isFresh = age < 5 * 60 * 1000;
if (!isFresh) {
// Refetch
fetchPage(cached.pageNumber);
}
}
```typescript
## Performance Considerations
### IndexedDB Operations
- **Open**: ~10-50ms (once per resource)
- **Read**: ~1-5ms per page
- **Write**: ~5-10ms per page
- **Delete**: ~5-10ms per database
### Memory Usage
- Zustand store: Lightweight (only current data in memory)
- IndexedDB: Storage quota varies by browser (typically 50MB-1GB+)
### Network Savings
With a 5-minute cache:
- **Without cache**: Every page load = API call
- **With cache**: 1 API call per page per 5 minutes
- **Savings**: ~90% reduction in API calls for frequently accessed pages
## Troubleshooting
### Data Not Persisting
**Problem**: Data disappears on page reload
**Solutions**:
- Check browser IndexedDB support
- Verify IndexedDB not disabled in browser settings
- Check for private/incognito mode (may have restrictions)
- Look for errors in browser console
### Stale Data
**Problem**: Seeing old data
**Solutions**:
- Clear cache: `clearResourceData(resourceName)`
- Check `fetchedAt` timestamp
- Reduce cache expiry time if needed
- Force refetch after mutations
### Wrong Page Data
**Problem**: Getting incorrect page data
**Solutions**:
- Verify `pageNumber` is 0-indexed
- Check `offset` calculation
- Ensure `pageSize` is consistent
- Verify API response matches expected structure
## Examples
### Complete Paginated Table
```typescript
import { useResourceStore, getPaginationParams } from "@/lib/stores";
import { useState, useEffect } from "react";
function PaginatedInvoiceTable() {
const { setResourceData, getResourceData, clearResourceData } = useResourceStore();
const [currentPage, setCurrentPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const pageSize = 100;
const resourceName = "invoices";
const cached = getResourceData(resourceName);
const fetchPage = async (pageNum: number) => {
setIsLoading(true);
try {
const { limit, offset } = getPaginationParams(pageSize, pageNum);
const response = await fetch("/api/fetch/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Company-Id": user.company_id,
"X-User-Id": user.user_id,
},
body: JSON.stringify({
table_name: "invoices",
limit,
offset,
})
});
const result = await response.json();
setResourceData(
resourceName,
result.data,
pageSize,
pageNum,
result.total_count
);
setCurrentPage(pageNum);
} catch (error) {
console.error("Failed to fetch page:", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (!cached) {
fetchPage(0);
}
}, []);
const totalPages = cached?.totalCount
? Math.ceil(cached.totalCount / pageSize)
: 1;
return (
<div>
<h1>Invoices</h1>
{isLoading ? (
<LoadingSpinner />
) : (
<InvoiceTable data={cached?.data || []} />
)}
<div className="pagination">
<Button
onClick={() => fetchPage(currentPage - 1)}
disabled={currentPage === 0 || isLoading}
>
Previous
</Button>
<span>
Page {currentPage + 1} of {totalPages}
{cached?.totalCount && ` (${cached.totalCount} total)`}
</span>
<Button
onClick={() => fetchPage(currentPage + 1)}
disabled={currentPage >= totalPages - 1 || isLoading}
>
Next
</Button>
<Button
onClick={() => {
clearResourceData(resourceName);
fetchPage(0);
}}
>
Refresh
</Button>
</div>
</div>
);
}
```typescript
## Related Documentation
- [ResourceProvider](./resource-provider.md) - User-level resource management
- [Resource Routes](./resource-routes.md) - Configure resource table routes
- [Components](./components.md) - UI components for resources
- [Hooks](./hooks.md) - Available hooks in the resource framework