XYLEX Group
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 limit and offset for 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