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:
-
Composable Function (
usePersistentCache):- Accepts a unique
cacheKey(string) to identify the cached data. - Accepts an
asyncFetcherfunction (e.g.,() => Promise<T>) that retrieves the data. - Accepts an optional
ttl(time-to-live) in milliseconds for the cache entries. Ifttlis provided and has expired, the cache should be considered stale. - Should attempt to retrieve data from
localStoragefirst. - If data is not found in
localStorage, is stale (expiredttl), or an error occurs during retrieval, it should call theasyncFetcher. - Upon successful fetch, it should store the data in
localStoragewith an expiration timestamp (ifttlis 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.
- Accepts a unique
-
Vue Component (
CachedDataDisplay):- A simple component that demonstrates the usage of the
usePersistentCachecomposable. - It should take a
cacheKeyand anasyncFetcheras 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.
- A simple component that demonstrates the usage of the
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
ttlis set and the cache has expired, a new fetch should be triggered even if data exists inlocalStorage. - Error Handling: If the
asyncFetcherrejects, an error message should be displayed. - Cache Clearing: Clicking the "Clear Cache" button should remove the data from
localStorageand trigger a refetch.
Edge Cases:
localStorageis unavailable or disabled.- The fetched data is
nullorundefined. - The
asyncFetchertakes a significant amount of time. - Concurrent calls to
usePersistentCachewith the samecacheKey(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:
localStorageis checked formyAppData. If not found or expired,fetchDatais called. "Fetching data from API..." is logged. Loading indicator is shown. - After Fetch:
databecomes reactive{ id: 1, name: 'Example Item' }. Loading indicator hides. The data is stored inlocalStoragewith an expiration timestamp. - Page Reload (within TTL):
fetchDatais not called. The cached data{ id: 1, name: 'Example Item' }is immediately retrieved fromlocalStorageand displayed. - Clear Cache: Calling
clearCache()removesmyAppDatafromlocalStorage, 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:
localStorageis checked formyAppData. The stored data is found, but its expiration timestamp is in the past. The cache is considered stale.fetchDatais called. "Fetching data from API..." is logged. Loading indicator is shown. - After Fetch:
datais updated with the new fetched data. The data is re-stored inlocalStoragewith 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:
localStorageis checked. If no valid cache,failingFetcheris called. "Attempting to fetch, will fail..." is logged. Loading indicator is shown. - After Fetch: The
failingFetcherrejects.errorbecomes reactiveError('API request failed').dataremainsnullor its previous value.isLoadingbecomesfalse.
Constraints
- The
cacheKeywill be a non-empty string. - The
asyncFetcherwill always return aPromise. - 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
localStorageis available and functional. No need to implement fallback mechanisms for disabledlocalStorage. - The solution should be efficient and avoid unnecessary
localStoragereads/writes.
Notes
- Consider how to store complex data types (objects, arrays) in
localStorage.JSON.stringifyandJSON.parsewill be your friends. - When storing expiration, consider storing both the data and the expiration timestamp together in
localStorageas a single item. - The
usePersistentCachecomposable should be designed for reusability across different components. - The
CachedDataDisplaycomponent is primarily for demonstration and testing of the composable. - Think about how to handle race conditions if multiple components use the same
cacheKeyconcurrently (though for this challenge, a basic implementation is sufficient). - The
ttlshould be checked against the current time when the cache is accessed.