Hone logo
Hone
Problems

Vue Persistent Cache with Local Storage

Develop a Vue 3 component that implements a persistent cache mechanism for API data, leveraging the browser's Local Storage. This will allow cached data to persist across page reloads and even browser sessions, improving user experience by reducing redundant API calls and speeding up data retrieval.

Problem Description

Your task is to create a Vue 3 composable function and an associated component that enables caching of data fetched from an asynchronous source (simulated by a setTimeout for this challenge). The cache should be stored in the browser's localStorage to ensure persistence.

Key Requirements:

  1. Composable Function (usePersistentCache):

    • Accepts a unique cacheKey (string) to identify the cached data.
    • Accepts an asyncFetcher function (e.g., () => Promise<T>) that retrieves the data.
    • Accepts an optional ttl (time-to-live) in milliseconds for the cache entries. If ttl is provided and has expired, the cache should be considered stale.
    • Should attempt to retrieve data from localStorage first.
    • If data is not found in localStorage, is stale (expired ttl), or an error occurs during retrieval, it should call the asyncFetcher.
    • Upon successful fetch, it should store the data in localStorage with an expiration timestamp (if ttl is provided).
    • Should return a reactive object containing the fetched data, a loading state, and an error state.
    • Provide a mechanism to manually clear the cache for a given cacheKey.
  2. Vue Component (CachedDataDisplay):

    • A simple component that demonstrates the usage of the usePersistentCache composable.
    • It should take a cacheKey and an asyncFetcher as props.
    • It should display the fetched data, a loading indicator when data is being fetched, and an error message if an error occurs.
    • Include a button to manually clear the cache for the data displayed by this component.

Expected Behavior:

  • Initial Load: When the component mounts, it should check localStorage. If valid cached data exists, it should be displayed immediately. Otherwise, a fetch will be initiated.
  • Data Fetching: During an API call, a loading indicator should be visible.
  • Data Display: Once data is fetched, it should be displayed.
  • Persistence: After a fetch, refreshing the page should show the cached data (if not expired).
  • TTL Expiration: If ttl is set and the cache has expired, a new fetch should be triggered even if data exists in localStorage.
  • Error Handling: If the asyncFetcher rejects, an error message should be displayed.
  • Cache Clearing: Clicking the "Clear Cache" button should remove the data from localStorage and trigger a refetch.

Edge Cases:

  • localStorage is unavailable or disabled.
  • The fetched data is null or undefined.
  • The asyncFetcher takes a significant amount of time.
  • Concurrent calls to usePersistentCache with the same cacheKey (though for this challenge, we can assume sequential or handle simple race conditions).

Examples

Example 1: Basic Data Fetching and Caching

// In a Vue component or setup function:

import { usePersistentCache } from './usePersistentCache'; // Assuming your composable is here

const fetchData = async () => {
  console.log('Fetching data from API...');
  await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API delay
  return { id: 1, name: 'Example Item' };
};

const { data, isLoading, error, clearCache } = usePersistentCache('myAppData', fetchData, { ttl: 5 * 60 * 1000 }); // 5 minutes TTL
  • Initial Load: localStorage is checked for myAppData. If not found or expired, fetchData is called. "Fetching data from API..." is logged. Loading indicator is shown.
  • After Fetch: data becomes reactive { id: 1, name: 'Example Item' }. Loading indicator hides. The data is stored in localStorage with an expiration timestamp.
  • Page Reload (within TTL): fetchData is not called. The cached data { id: 1, name: 'Example Item' } is immediately retrieved from localStorage and displayed.
  • Clear Cache: Calling clearCache() removes myAppData from localStorage, and the next access will trigger a refetch.

Example 2: Cache Expiration

// Assuming previous fetch occurred and was cached.
// Now, the TTL (e.g., 5 minutes) has passed.

// Accessing usePersistentCache again with the same key and fetcher:
const { data, isLoading, error } = usePersistentCache('myAppData', fetchData, { ttl: 5 * 60 * 1000 });
  • Initial Load: localStorage is checked for myAppData. The stored data is found, but its expiration timestamp is in the past. The cache is considered stale. fetchData is called. "Fetching data from API..." is logged. Loading indicator is shown.
  • After Fetch: data is updated with the new fetched data. The data is re-stored in localStorage with a new expiration timestamp.

Example 3: Handling Errors

const failingFetcher = async () => {
  console.log('Attempting to fetch, will fail...');
  await new Promise((_, reject) => setTimeout(() => reject(new Error('API request failed')), 500));
};

const { data, isLoading, error } = usePersistentCache('errorProneData', failingFetcher, { ttl: 60000 }); // 1 minute TTL
  • Initial Load: localStorage is checked. If no valid cache, failingFetcher is called. "Attempting to fetch, will fail..." is logged. Loading indicator is shown.
  • After Fetch: The failingFetcher rejects. error becomes reactive Error('API request failed'). data remains null or its previous value. isLoading becomes false.

Constraints

  • The cacheKey will be a non-empty string.
  • The asyncFetcher will always return a Promise.
  • The ttl (if provided) will be a positive integer representing milliseconds.
  • The solution should be written in TypeScript and use Vue 3's Composition API.
  • For simplicity, assume localStorage is available and functional. No need to implement fallback mechanisms for disabled localStorage.
  • The solution should be efficient and avoid unnecessary localStorage reads/writes.

Notes

  • Consider how to store complex data types (objects, arrays) in localStorage. JSON.stringify and JSON.parse will be your friends.
  • When storing expiration, consider storing both the data and the expiration timestamp together in localStorage as a single item.
  • The usePersistentCache composable should be designed for reusability across different components.
  • The CachedDataDisplay component is primarily for demonstration and testing of the composable.
  • Think about how to handle race conditions if multiple components use the same cacheKey concurrently (though for this challenge, a basic implementation is sufficient).
  • The ttl should be checked against the current time when the cache is accessed.
Loading editor...
typescript