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

Leader 1 2 21
calendar_today agoschedule2 min read
— Originally published at dev.to

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>
  );
}
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>
  );
}

Resources


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


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

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

Local-First: The Browser as the Vault

Pocket Portfolio - Apr 20

Go Fullscreen Safely in React: Introducing the useFullscreen Hook

saurav_tb_pandey - Aug 26

Go Fullscreen Safely in React: Introducing the useFullscreen Hook

saurav_tb_pandey - Aug 28

React Native Quote Audit - USA

kajolshah - Mar 2
chevron_left
1.2k Points24 Badges
19Posts
1Comments
4Connections
An independent and self-motivated engineering enthusiast with an innovative mindset.

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!