Caching List Data in React with react-query and localforage

November, 1st 2024 3 min read

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

js
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

js
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

js
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

js
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.