npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classic-homes/offline-react

v0.1.3

Published

React bindings for offline persistence and caching

Readme

@classic-homes/offline-react

React hooks for offline persistence and caching. Provides reactive file storage, form draft persistence, and data caching for React applications.

Features

  • useFileStorage: Reactive file storage with validation and previews
  • usePersistedForm: Automatic form draft persistence
  • useOfflineCache: Data caching with TTL and stale-while-revalidate
  • TypeScript: Full type safety with generics
  • React 18/19: Compatible with latest React versions

Installation

npm install @classic-homes/offline-react @classic-homes/offline-core

Quick Start

import { useEffect } from 'react';
import { initOfflineStorage } from '@classic-homes/offline-core';
import { useFileStorage, usePersistedForm, useOfflineCache } from '@classic-homes/offline-react';

// Initialize storage at app startup
function App() {
  useEffect(() => {
    initOfflineStorage();
  }, []);

  return <MyForm />;
}

Hooks

useFileStorage

Manage file attachments with validation, preview generation, and persistence.

import { useFileStorage } from '@classic-homes/offline-react';

function FileUploadField() {
  const documents = useFileStorage({
    formKey: 'contact-form',
    fieldName: 'documents',
    maxFiles: 5,
    maxSizeBytes: 10 * 1024 * 1024, // 10MB
    acceptedTypes: ['application/pdf', 'image/*'],
    onValidationError: (errors) => {
      errors.forEach((e) => toast.error(e.message));
    },
  });

  const handleDrop = async (files: File[]) => {
    await documents.addFiles(files);
  };

  return (
    <div>
      {documents.isLoading ? (
        <Spinner />
      ) : (
        <FileList files={documents.files} onRemove={documents.removeFile} />
      )}
      <DropZone onDrop={handleDrop} />
    </div>
  );
}

Options

| Option | Type | Description | | ------------------- | ---------- | ------------------------------------------------ | | formKey | string | Form identifier | | fieldName | string | Field name within the form | | maxFiles | number | Maximum number of files | | maxSizeBytes | number | Maximum file size in bytes | | acceptedTypes | string[] | Accepted MIME types (supports wildcards) | | expirationMs | number | File expiration time | | generatePreviews | boolean | Generate preview URLs for images (default: true) | | onValidationError | function | Callback for validation errors |

Return Value

| Property | Type | Description | | ------------------- | ----------------- | -------------------------------- | | files | PersistedFile[] | Current files | | fileIds | string[] | File IDs for form submission | | isLoading | boolean | Loading state | | error | Error \| null | Current error | | addFiles | function | Add files (validates and stores) | | removeFile | function | Remove a file by ID | | clearFiles | function | Remove all files | | reload | function | Reload from storage | | getFilesForSubmit | function | Get File objects for FormData |

usePersistedForm

Automatically persist form state across page reloads.

import { usePersistedForm } from '@classic-homes/offline-react';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string(),
});

function ContactForm() {
  const form = usePersistedForm({
    formKey: 'contact-form',
    defaultValues: { name: '', email: '', message: '' },
    schema, // Optional Zod schema
    debounceMs: 500,
    excludeFields: ['password'], // Don't persist sensitive fields
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!form.isValid) return;

    await submitForm(form.values);
    await form.clearDraft();
  };

  return (
    <form onSubmit={handleSubmit}>
      {form.hasDraft && (
        <button type="button" onClick={form.restoreDraft}>
          Restore draft from {form.draftInfo?.timestamp}
        </button>
      )}

      <input
        name="name"
        value={form.values.name}
        onChange={(e) => form.setValue('name', e.target.value)}
      />
      {form.errors.name && <span>{form.errors.name}</span>}

      <input
        name="email"
        value={form.values.email}
        onChange={(e) => form.setValue('email', e.target.value)}
      />

      <textarea
        name="message"
        value={form.values.message}
        onChange={(e) => form.setValue('message', e.target.value)}
      />

      <button type="submit" disabled={!form.isValid || form.isSubmitting}>
        Submit
      </button>
    </form>
  );
}

Options

| Option | Type | Description | | --------------- | ----------- | ------------------------------------------ | | formKey | string | Unique form identifier | | defaultValues | T | Default form values | | schema | ZodSchema | Optional Zod validation schema | | debounceMs | number | Debounce time for auto-save (default: 500) | | excludeFields | string[] | Fields to exclude from persistence | | expirationMs | number | Draft expiration time | | autoRestore | boolean | Auto-restore on mount (default: false) |

Return Value

| Property | Type | Description | | -------------- | ---------------- | -------------------------------- | | values | T | Current form values | | errors | FieldErrors<T> | Validation errors by field | | isValid | boolean | Whether form passes validation | | isDirty | boolean | Whether form has unsaved changes | | hasDraft | boolean | Whether a draft exists | | draftInfo | object | Draft metadata | | setValue | function | Set a field value | | setValues | function | Set multiple values | | reset | function | Reset to default values | | saveDraft | function | Manually save draft | | restoreDraft | function | Restore from draft | | clearDraft | function | Delete the draft |

useOfflineCache

Cache data with TTL and stale-while-revalidate support.

import { useOfflineCache } from '@classic-homes/offline-react';

function UserProfile({ userId }: { userId: string }) {
  const userCache = useOfflineCache<User>({
    key: `user:${userId}`,
    ttl: 5 * 60 * 1000, // 5 minutes
    tags: ['user'],
    staleWhileRevalidate: true,
  });

  useEffect(() => {
    userCache.get(() => fetchUser(userId));
  }, [userId]);

  if (userCache.isLoading) return <Spinner />;
  if (userCache.error) return <Error error={userCache.error} />;
  if (!userCache.data) return null;

  return (
    <div>
      <h1>{userCache.data.name}</h1>
      {userCache.isStale && <span>Updating...</span>}
      <button onClick={() => userCache.refresh(() => fetchUser(userId))}>Refresh</button>
    </div>
  );
}

Options

| Option | Type | Description | | ---------------------- | ---------- | --------------------------------- | | key | string | Cache key | | ttl | number | Time to live in milliseconds | | tags | string[] | Tags for bulk invalidation | | staleWhileRevalidate | boolean | Return stale data while fetching | | staleTime | number | How long stale data is acceptable |

Return Value

| Property | Type | Description | | -------------- | ---------------- | --------------------- | | data | T \| null | Cached data | | isLoading | boolean | Loading state | | isStale | boolean | Whether data is stale | | error | Error \| null | Current error | | ttlRemaining | number \| null | Time until expiration | | get | function | Get cached or fetch | | set | function | Set cache value | | invalidate | function | Invalidate this key | | refresh | function | Force re-fetch | | has | function | Check if cached |

Utility Functions

import { invalidateCacheByTag, invalidateAllCache } from '@classic-homes/offline-react';

// Invalidate all entries with a tag
await invalidateCacheByTag('user');

// Invalidate entire cache
await invalidateAllCache();

TypeScript

All hooks are fully typed with generics:

interface User {
  id: string;
  name: string;
  email: string;
}

const userCache = useOfflineCache<User>({ key: 'user' });
// userCache.data is User | null

const form = usePersistedForm<User>({
  formKey: 'user',
  defaultValues: { id: '', name: '', email: '' },
});
// form.values is User
// form.setValue('name', value) - 'name' is typed

Testing

npm test           # Run tests
npm run test:watch # Watch mode
npm run test:coverage # With coverage

License

MIT