DEV Community

Saurav Pandey
Saurav Pandey

Posted on

Build Snappy Offline-First Apps: Introducing SWR and IndexedDB-powered useResource in react-hook-lab

Data fetching in modern React apps often forces a trade-off: either you build complex caching infrastructure, or you accept awkward layout shifts, loading spinners, and sluggish transitions. SWR (Stale-While-Revalidate) changed the game for in-memory fetching, but what if your app needs to work seamlessly offline, cache gigabytes of data securely, and coordinate state across browser tabs?

Today, I'm thrilled to announce the release of useResource, a highly optimized React hook designed to tackle SWR-based data fetching, cache persistence, and state management inside the react-hook-lab library. Along with this, we've updated useIndexedDB with conditional control to lay down a robust architecture for offline-first React systems.

Let's dive into how it works and how you can use it to build incredibly snappy user interfaces.


Why useResource?

Unlike traditional query libraries that require extensive configuration, useResource brings atomic SWR caching and browser-native persistence under one simple roof.

Key features include:

  • Flexible Caching Layers: Store cached data in-memory, synchronize it globally across instances using shared memory, or save it permanently via indexeddb.
  • Optimistic Mutations: Update your local UI instantly while the background syncing handles server reconciliations.
  • Automatic Retries: Smart exponential backoff retry logic built right into the hook.
  • Tab Synchronization: Changes inside one tab automatically replicate in others, minimizing redundant API requests.

Feature Deep Dive & Code Examples

To power these advanced capabilities, we also upgraded useIndexedDB by adding a custom enabled option. This allows hooks to conditionally bypass storage transactions when they are idle or during complex setup states, avoiding unnecessary database operations.

Here are two concrete examples showing how you can integrate these updates into your codebase today.

Example 1: Basic SWR Data Fetching

If you need simple background revalidation with robust loading states and easy refresh actions, a lightweight memory cache is all you need.

import React from 'react';
import { useResource } from 'react-hook-lab';

const fetchUserProfile = async (signal) => {
  const response = await fetch('/api/user/profile', { signal });
  if (!response.ok) throw new Error('Failed to load profile');
  return response.json();
};

export function UserProfile() {
  const { data, loading, error, refresh } = useResource({
    key: 'user-profile',
    fetcher: fetchUserProfile,
    staleTime: 10000, // consider fresh for 10 seconds
  });

  if (loading && !data) return <p>Loading your profile...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h3>Welcome, {data?.name}!</h3>
      <p>Email: {data?.email}</p>
      <button onClick={refresh}>Force Revalidate</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example 2: Offline-First Caching with IndexedDB & Optimistic UI

By leveraging the newly integrated IndexedDB storage backend, you can store heavy datasets on the user's hard drive and update the interface immediately while server calls complete in the background.

import React from 'react';
import { useResource } from 'react-hook-lab';

const fetchTodoList = async (signal) => {
  const response = await fetch('/api/todos', { signal });
  return response.json();
};

export function TodoManager() {
  const { data: todos, mutate, loading } = useResource({
    key: 'todo-items',
    fetcher: fetchTodoList,
    cache: 'indexeddb', // Persist data locally via IndexedDB
    persist: {
      store: 'app-cache-store', 
    },
    initialData: [],
  });

  const handleToggleTodo = (todoId) => {
    // Optimistically update the checklist locally
    mutate((currentList) => {
      return (currentList || []).map((todo) =>
        todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
      );
    });
  };

  return (
    <div>
      <h2>Your Tasks {loading && ' (Syncing with server...)'}</h2>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
            <label>
              <input
                type="checkbox"
                checked={!!todo.completed}
                onChange={() => handleToggleTodo(todo.id)}
              />
              {todo.text}
            </label>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Resources


Originally published on my blog. You can read the alternative breakdown here.

Top comments (0)