Skip to content

Last updated:

Caching Strategies

CPR uses several caching mechanisms to reduce redundant API calls and improve responsiveness. Since CPR is an SPA, all caching is client-side.

sessionStorage for Auth State

Authentication state (user object and token) is persisted in sessionStorage. This keeps the user logged in across page refreshes within the same browser tab, while ensuring the session is cleared when the tab is closed.

ts
// From auth.store.ts -- hydrate restores state from sessionStorage
const hydrate = () => {
  const storedAuth = sessionStorage.getItem('auth');

  if (storedAuth) {
    try {
      const storedStated: AuthState = JSON.parse(storedAuth);
      state.user = storedStated.user;
      state.token = storedStated.token;
      state.resetCodeVerified = storedStated.resetCodeVerified;
      state.branchServices = storedStated.user?.branch_services || [];
      authBranchStore.selectedBranch =
        state.user?.default_branch || null;
    } catch (e) {
      state.user = null;
      state.token = null;
      state.resetCodeVerified = false;
      console.warn('Failed to parse auth from sessionStorage:', e);
    }
  }
};

Why sessionStorage Instead of localStorage

  • Tab isolation -- Each tab has its own session. A doctor logged into one branch in one tab does not affect another tab.
  • Automatic cleanup -- Closing the tab clears the session, reducing the risk of stale tokens.
  • Security -- Tokens are not persisted across browser sessions. If a shared workstation is used (common in clinics), closing the browser clears all sessions.

In-Memory Store Caching with Pinia

Pinia stores act as an in-memory cache for the current session. Once data is fetched from the API, it is stored in the Pinia store and available immediately for any component that needs it.

ts
// From insurance.store.ts
export const useInsuranceStore = defineStore('insurance', () => {
  const list = ref<InsuranceResource[]>([]);
  const pagination = ref<APIPagination>({
    per_page: 15,
    total: 0,
    from: 0,
    to: 0,
    current_page: 1,
  });

  return { list, pagination };
});

When the insurance list page loads, useInsurance().getInsurances() fetches the data and populates the store. Any other component on the page can access useInsuranceStore().list without making another API call.

Avoiding Redundant API Calls with useListFilters

The useListFilters composable includes a built-in deduplication mechanism that compares the current filter parameters with the last successful fetch:

ts
// From useListFilters.ts
let lastFetchParams = '';

const filter = async () => {
  const currentParams = JSON.stringify(buildParams());
  // Skip fetch if params haven't changed
  if (currentParams === lastFetchParams) return;
  lastFetchParams = currentParams;

  await options.onFilter(buildParams());
};

This prevents redundant API calls in common scenarios:

  • User clicks the same status tab twice
  • Browser back/forward navigation returns to the same filter state
  • Component re-renders without actual filter changes

To force a refresh (after creating or deleting a record), use refetchCurrentPage():

ts
const { refetchCurrentPage } = useListFilters({ ... });

// After deleting a record, force a fresh fetch
await deleteInsurance(id);
await refetchCurrentPage();

System Categories: Load Once on Mount

The useSystemCategoryStore holds lookup data (bill item categories, bill item sources, patient categories) that rarely changes. These are loaded once when the relevant page mounts and then reused:

ts
// From systemCategories.store.ts
export const useSystemCategoryStore = defineStore('systemCategory', () => {
  const billItemSources = ref<BillItemSource[]>([]);
  const billItemCategories = ref<BillItemCategory[]>([]);
  const patientCategories = ref<PatientCategory[]>([]);

  return {
    billItemCategories,
    billItemSources,
    patientCategories,
  };
});

Pattern for loading categories once:

vue
<script setup lang="ts">
const categoryStore = useSystemCategoryStore();

onMounted(async () => {
  // Only fetch if not already loaded
  if (categoryStore.patientCategories.length === 0) {
    await fetchPatientCategories();
  }
});
</script>

Caching Best Practices for CPR

Do Cache

  • Auth state -- Token and user info in sessionStorage.
  • Lookup data -- Patient categories, bill item sources, payment methods. These change rarely and are used on many pages.
  • Current page data -- The Pinia store holds the current list/detail so components sharing a page do not re-fetch.

Do Not Cache

  • Sensitive patient data -- Never persist patient records in localStorage or sessionStorage. Keep them only in memory (Pinia stores) so they are cleared when the tab closes.
  • Stale tokens -- The auth middleware checks JWT expiry before every navigation. Do not skip this check to use a cached token.
  • Queue data -- Queue status changes frequently. Always fetch fresh data when entering the queue page.

Cache Invalidation

Data TypeInvalidation Strategy
Auth tokenJWT exp claim check in auth middleware
List data (insurance, medicines, etc.)refetchCurrentPage() after CUD operations
System categoriesLoad once, refresh on full page reload
Branch selectionUpdated via updateBranch() in auth store

URL as Cache Key

useListFilters synchronizes filter state with URL query parameters. This means the URL itself acts as a cache key:

/transactions?search=PhilHealth&status=Completed&page=2

When a user bookmarks or shares this URL, the page restores the exact filter state from the query string via syncFromRoute():

ts
const syncFromRoute = () => {
  Object.keys(options.initialFilters).forEach((key) => {
    filters.value[key as keyof T] = route.query[key]
      ? String(route.query[key])
      : options.initialFilters[key as keyof T];
  });

  currentPage.value = Number(route.query[pageKey] ?? 1);
};

CPR - Clinical Patient Records