Caching List Data in React with react-query and localforage
Efficient data caching is crucial in React applications, especially when handling large datasets, slow APIs, or repeated list rendering. Combining react-query with localforage gives you both fast in-memory caching and long-term persistence using IndexedDB, WebSQL, or LocalStorage.
This approach displays cached list data instantly and silently fetches fresh data in the background, drastically improving perceived performance.
Why Cache with react-query + localforage
React hooks like useMemo and useCallback optimize rendering, but they do not persist data between sessions. Meanwhile:
- react-query handles server synchronization, caching, stale-time logic, and retries.
- localforage stores cached data persistently, even after page reloads.
Using both gives you a “local-first” model: show cached data immediately, then refresh in the background.
Step 1: Configure localforage
import localforage from 'localforage';
export const localStore = localforage.createInstance({
name: 'app-cache',
});This instance will store cached lists by key.
Step 2: Build a useLocalforage Hook
import { useCallback, useEffect, useState } from 'react';
export const useLocalforage = key => {
const [localData, setLocalData] = useState();
const [localLoading, setLocalLoading] = useState(true);
const get = useCallback(async () => {
try {
return await localStore.getItem(key);
} catch {
return null;
}
}, [key]);
const set = useCallback(async value => {
try {
await localStore.setItem(key, value);
} catch {}
}, [key]);
useEffect(() => {
(async () => {
const stored = await get();
if (stored) setLocalData(stored);
setLocalLoading(false);
})();
}, [get]);
return { localData, localLoading, set };
};This hook retrieves cached data on mount and exposes methods to persist new data.
Step 3: Create the useLocalQuery Hook
import { useQuery, useQueryClient } from 'react-query';
import { useLocalforage } from './useLocalforage';
export const useLocalQuery = ({ queryKey, queryFn, onSuccess }) => {
const { localData, localLoading, set } = useLocalforage(queryKey);
const { data: serverData, isLoading } = useQuery({
queryKey,
queryFn,
onSuccess: newData => {
set(newData);
if (onSuccess) onSuccess(newData);
},
});
const data = localData || serverData;
return {
data,
isLoading: localLoading || isLoading,
};
};Why this works
- Cached data loads instantly.
- react-query fetches fresh data silently.
- New data is persisted to localforage automatically.
Example Component
import axios from 'axios';
import { useLocalQuery } from './useLocalQuery';
const fetchList = async () => {
const res = await axios.get('/api/list');
return res.data;
};
export const ListComponent = () => {
const { data, isLoading } = useLocalQuery({
queryKey: 'items',
queryFn: fetchList,
});
if (isLoading) return <p>Loading...</p>;
return (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
};Key Advantages
- Instant load from local storage
- Updated automatically via react-query
- Works offline
- Reduces backend load
- Smooth experience for large datasets
Conclusion
Using react-query together with localforage creates a powerful caching layer that persists list data across reloads and sessions. The UI loads instantly using local storage while staying fresh through background API requests. This hybrid model provides both performance and reliability, making it ideal for dashboards, admin panels, and mobile-friendly apps.