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

hybrid-storage

v0.0.2

Published

This repository contains a set of React hooks for managing storage operations using the [unstorage](https://unstorage.unjs.io/) library, with specialized hooks for IndexedDB and hybrid signal systems. Below is the comprehensive documentation for all avail

Readme

Hybrid-Storage

This repository contains a set of React hooks for managing storage operations using the unstorage library, with specialized hooks for IndexedDB and hybrid signal systems. Below is the comprehensive documentation for all available hooks.

Installation

npm install hybrid-storage

Table of Contents

useUnstorage Hook

A comprehensive React hook for managing unstorage operations with built-in state management, loading states, and error handling. Note: For signal-like behavior with storage, refer to related hooks such as useStorageSignal discussed in later sections.

Features

  • Automatic loading on component mount
  • Loading states for all operations
  • Error handling with custom error callbacks
  • TypeScript support with generic types
  • Manual control options (disable auto-load)
  • Raw value operations (get/set without JSON stringify and parsing; particularly optimized for IndexedDB drivers)
  • Utility functions (clear errors, reset state)

Basic Usage

import { useUnstorage } from './utils/hooks';
import { createStorage } from 'unstorage';
import localStorageDriver from 'unstorage/drivers/localStorage';

// Create a storage instance
const storage = createStorage({
  driver: localStorageDriver()
});

function MyComponent() {
  const { 
    value, 
    loading, 
    error, 
    setValue, 
    removeValue 
  } = useUnstorage(storage, "my-key", {
    defaultValue: "Hello World"
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <p>Value: {value}</p>
      <button onClick={() => setValue("New Value")}>
        Update Value
      </button>
      <button onClick={removeValue}>
        Remove Value
      </button>
    </div>
  );
}

API Reference

Hook Parameters

useUnstorage<T = string>(
  storage: Storage,
  key: string,
  options?: UseUnstorageOptions<T>
)
Parameters
  • storage: The unstorage instance
  • key: The storage key
  • options: Configuration options (optional)
Options
interface UseUnstorageOptions<T = string> {
  defaultValue?: T;        // Default value if key doesn't exist
  autoLoad?: boolean;      // Auto-load value on mount (default: true)
  onError?: (error: Error) => void;  // Custom error handler
  fallbackToDefault?: boolean;  // Fall back to defaultValue when key doesn't exist (default: true)
  subscribe?: boolean;     // Subscribe to external changes (default: true)
  pollInterval?: number;   // Polling interval in ms for change detection (default: 1000)
}

Return Value

{
  // State
  value: T | null;         // Current value
  loading: boolean;        // Loading state
  error: Error | null;     // Error state
  
  // Actions
  setValue: (value: T) => Promise<void>;     // Set value
  removeValue: () => Promise<void>;          // Remove value
  loadValue: () => Promise<void>;            // Manually load value
  hasValue: () => Promise<boolean>;          // Check if key exists
  getRawValue: () => Promise<string | null>; // Get raw string value (particularly useful with IndexedDB for non-string data)
  setRawValue: (value: string) => Promise<void>; // Set raw string value (particularly useful with IndexedDB for non-string data)
  
  // Utilities
  clearError: () => void;  // Clear error state
  reset: () => void;       // Reset to default state
}

Advanced Examples

Manual Control (No Auto-load)

const { 
  value, 
  loading, 
  loadValue, 
  setValue 
} = useUnstorage(storage, "manual-key", {
  autoLoad: false
});

// Value won't load automatically
// Call loadValue() when you want to load it

Complex Data Types

interface User {
  name: string;
  age: number;
}

const { value, setValue } = useUnstorage<User>(storage, "user", {
  defaultValue: { name: "John", age: 30 }
});

// TypeScript will ensure type safety
setValue({ name: "Jane", age: 25 });

Default Value Behavior

By default, the hook will fall back to the defaultValue when:

  • The storage key doesn't exist (returns null)
  • The value is removed from storage

You can disable this behavior by setting fallbackToDefault: false:

const { value, setValue, removeValue } = useUnstorage(storage, "key", {
  defaultValue: "Hello",
  fallbackToDefault: false  // Will use null instead of defaultValue
});

// After removeValue(), value will be null instead of "Hello"

Error Handling

const { 
  value, 
  error, 
  setValue, 
  clearError 
} = useUnstorage(storage, "error-key", {
  onError: (error) => {
    console.error("Storage operation failed:", error);
    // Custom error handling logic
  }
});

Real-time Updates & Subscriptions

The hook automatically subscribes to external changes by default. This means if another component or external source updates the same storage key, all subscribed components will update automatically. Note: Detection of external changes may vary by driver; for some drivers like cookies, changes might be detected with a delay based on the polling interval or might require specific driver support.

// Basic subscription (enabled by default)
const { value, setValue } = useUnstorage(storage, "shared-key", {
  defaultValue: "Hello",
  subscribe: true, // Default behavior
  pollInterval: 1000 // Check for changes every second
});

// Disable subscription for performance
const { value, setValue } = useUnstorage(storage, "local-key", {
  subscribe: false // No real-time updates
});

// Custom polling interval
const { value, setValue } = useUnstorage(storage, "fast-updates", {
  pollInterval: 100 // Check every 100ms for faster updates
});

// Disable polling but keep subscription
const { value, setValue } = useUnstorage(storage, "no-polling", {
  subscribe: true,
  pollInterval: 0 // Disables polling, only in-tab updates will be detected
});
Cross-Component Synchronization

Multiple components can share the same storage key and stay synchronized:

// Component A
function ComponentA() {
  const { value, setValue } = useUnstorage(storage, "shared-data");
  // ... component logic
}

// Component B
function ComponentB() {
  const { value, setValue } = useUnstorage(storage, "shared-data");
  // ... component logic
}

IndexedDB Storage Hooks

This section covers a collection of specialized React hooks designed specifically for IndexedDB drivers. These hooks provide native object storage capabilities without JSON stringification overhead, while maintaining the same API as the regular storage hooks.

Why IndexedDB-Specific Hooks?

IndexedDB can store JavaScript objects natively without requiring JSON serialization/deserialization, unlike localStorage, sessionStorage, and cookies which only store strings. This provides:

  • Better Performance: No JSON.stringify/parse overhead
  • Native Object Storage: Direct storage of complex objects, arrays, dates, etc. using setItemRaw method from unstorage to avoid JSON stringification
  • Type Safety: Better TypeScript support for complex data structures
  • Reduced Memory Usage: No intermediate string representations

Available Hooks

1. useIndexedDBStorage

The basic IndexedDB storage hook, equivalent to useUnstorage but optimized for IndexedDB.

Note: The API for useIndexedDBStorage is consistent with useUnstorage, providing the same return value structure and options for ease of use and migration.

import { useIndexedDBStorage } from './useIndexedDBStorage';
import { createStorage } from 'unstorage';
import indexeddbDriver from 'unstorage/drivers/indexedb';

// Create a storage instance for IndexedDB
const storage = createStorage({
  driver: indexeddbDriver({ base: 'my-app' })
});

function MyComponent() {
  const { value, setValue, loading, error } = useIndexedDBStorage<User>(
    storage,
    'user-profile',
    {
      defaultValue: { name: 'Default User', email: '[email protected]' }
    }
  );

  return (
    <div>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      <pre>{JSON.stringify(value, null, 2)}</pre>
      <button onClick={() => setValue({ name: 'John', email: '[email protected]' })}>
        Update User
      </button>
    </div>
  );
}

2. useIndexedDBSignal

Signal-like IndexedDB storage hook, equivalent to useStorageSignal but optimized for IndexedDB.

import { useIndexedDBSignal } from './useIndexedDBSignal';

function TodoList() {
  const { value: todos, set: setTodos } = useIndexedDBSignal<Todo[]>(
    storage,
    'todos',
    []
  );

  const addTodo = async (text: string) => {
    const newTodo = { id: Date.now().toString(), text, completed: false };
    await setTodos([...todos, newTodo]);
  };

  return (
    <div>
      {todos.map(todo => (
        <div key={todo.id}>
          <input 
            type="checkbox" 
            checked={todo.completed}
            onChange={() => setTodos(todos.map(t => 
              t.id === todo.id ? { ...t, completed: !t.completed } : t
            ))}
          />
          {todo.text}
        </div>
      ))}
    </div>
  );
}

3. useIndexedDBHybridSignal

Hybrid signal that combines in-memory reactivity with persistent IndexedDB storage, equivalent to useHybridSignal but optimized for IndexedDB.

Note: Unlike useUnstorage and useIndexedDBStorage, this hook uses a signal-like API with methods such as set and update for immediate reactivity, while still maintaining persistence.

import { useIndexedDBHybridSignal } from './useIndexedDBHybridSignal';

function AppSettings() {
  const { value: settings, set: setSettings } = useIndexedDBHybridSignal<AppSettings>(
    storage,
    'app-settings',
    {
      defaultValue: { theme: 'light', language: 'en', autoSave: true },
      immediate: true,
      debounceMs: 200
    }
  );

  return (
    <div>
      <select 
        value={settings.theme}
        onChange={(e) => setSettings(prev => ({ ...prev, theme: e.target.value }))}
      >
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
      
      <label>
        <input 
          type="checkbox"
          checked={settings.autoSave}
          onChange={(e) => setSettings(prev => ({ ...prev, autoSave: e.target.checked }))}
        />
        Auto Save
      </label>
    </div>
  );
}

Driver Validation

All IndexedDB hooks include runtime validation to ensure they're used with IndexedDB drivers:

// This will throw an error if used with localStorage
const localStorage = createStorage({
  driver: localStorageDriver()
});

// ❌ This will throw an error
const { value } = useIndexedDBStorage(localStorage, 'key', { defaultValue: 'value' });

// ✅ This works correctly
const indexedDBStorage = createStorage({
  driver: indexeddbDriver({ base: 'my-app' })
});
const { value } = useIndexedDBStorage(indexedDBStorage, 'key', { defaultValue: 'value' });

Performance Monitoring

The hooks include built-in performance monitoring utilities:

import { indexedDBPerformanceMonitor } from './useIndexedDBStorage.utils';

// Measure operation performance
const stopTimer = indexedDBPerformanceMonitor.startTimer('user-update');
await updateUser();
stopTimer();

// Get performance statistics
const stats = indexedDBPerformanceMonitor.getStats('user-update');
console.log(`Average time: ${stats.avg.toFixed(2)}ms`);

Utility Functions

isIndexedDBDriver(storage: Storage): boolean

Type guard to check if a storage driver is an IndexedDB driver.

warnIfNotIndexedDB(storage: Storage, hookName: string): void

Development warning to help prevent misuse of IndexedDB hooks.

notifyIndexedDBChange(key: string): void

Manually notify subscribers of IndexedDB storage changes.

createNotifyingIndexedDBDriver(driver: Storage): Storage

Create a custom IndexedDB storage driver wrapper that automatically notifies subscribers.

Migration Guide

From useUnstorage to useIndexedDBStorage

// Before
const { value, setValue } = useUnstorage(storage, 'key', { defaultValue: 'value' });

// After (for IndexedDB drivers)
const { value, setValue } = useIndexedDBStorage(storage, 'key', { defaultValue: 'value' });

From useStorageSignal to useIndexedDBSignal

// Before
// const { value, set } = useStorageSignal(storage, 'key', defaultValue);

// After (for IndexedDB drivers)
const { value, set } = useIndexedDBSignal(storage, 'key', defaultValue);

Hybrid Signal System

The Hybrid Signal System combines the immediate reactivity of signals with the persistence and cross-tab synchronization of storage. This gives you the performance of signals with the persistence of storage.

🎯 What Problem Does This Solve?

Traditional Signals (SolidJS, Vue 3, etc.)

  • Immediate updates - UI responds instantly
  • Excellent performance - No async operations
  • No persistence - Lost on page refresh
  • No cross-tab sync - Single app instance only

Storage-Based Systems

  • Persistent - Survives page refresh
  • Cross-tab sync - Multiple tabs stay in sync
  • Async updates - UI waits for storage
  • Polling overhead - Performance impact

Hybrid Signal System

  • Immediate updates - UI responds instantly (like signals)
  • Persistent storage - Survives page refresh
  • Cross-tab sync - Multiple tabs stay in sync
  • Configurable performance - Choose your trade-offs
  • Debounced writes - Optimize storage performance
  • Native IndexedDB Storage - Uses setItemRaw for storing objects as-is in IndexedDB without JSON stringification
  • Simultaneous signals and polling - Supports immediate signal updates alongside polling for external change detection

Note: This dual mechanism of signals and polling is crucial for collaborative, real-time applications where both user experience and data consistency are priorities.

🚀 Quick Start

import { useHybridSignal, useComputedHybridSignal } from './utils/hooks';

function MyComponent() {
  // Basic hybrid signal
  const { value, set, update, setAsync } = useHybridSignal(storage, "count", {
    defaultValue: 0,
    subscribe: true,
    pollInterval: 500,
    immediate: true,
    debounceMs: 100
  });

  // Computed hybrid signal
  const doubled = useComputedHybridSignal(storage, "count", 0, v => v * 2);

  return (
    <div>
      <p>Count: {value}</p>
      <p>Doubled: {doubled}</p>
      <button onClick={() => set(value + 1)}>+1 (Immediate)</button>
      <button onClick={() => setAsync(value + 10)}>+10 (Async)</button>
    </div>
  );
}

📚 API Reference

useHybridSignal<T>

const { 
  value,           // Current value
  set,             // Signal-like immediate setter
  update,          // Signal-like updater function
  setAsync,        // Async setter (waits for storage)
  updateAsync,     // Async updater (waits for storage)
  remove,          // Remove value
  checkForChanges, // Force check for external changes
  currentValue     // Signal-like getter
} = useHybridSignal(storage, key, options);
Options
interface UseHybridSignalOptions<T> {
  defaultValue: T;           // Required: Default value
  subscribe?: boolean;       // Subscribe to external changes (default: true)
  pollInterval?: number;     // Polling interval in ms (default: 1000)
  onError?: (error: Error) => void;  // Error handler
  immediate?: boolean;       // Immediate updates (default: true)
  debounceMs?: number;       // Debounce storage writes (default: 100)
}

useComputedHybridSignal<T, R>

const computedValue = useComputedHybridSignal(
  storage, 
  key, 
  defaultValue, 
  computeFunction
);

🎛️ Usage Patterns

1. Immediate Updates (Signal-like)

const { value, set } = useHybridSignal(storage, "counter", { defaultValue: 0 });

// Updates immediately, storage writes are debounced
set(value + 1);
set(prev => prev + 1);

2. Async Updates (Storage-first)

const { value, setAsync } = useHybridSignal(storage, "important-data", { defaultValue: {} });

// Waits for storage to complete
await setAsync(newData);

3. Performance Optimization

const { set } = useHybridSignal(storage, "frequent-updates", { defaultValue: 0 });

// Memory only (no storage writes)
set(value + 1, { persist: false });

// Immediate storage (no debouncing)
set(value + 1, { persist: true, debounce: false });

// Debounced storage (default)
set(value + 1, { persist: true, debounce: true });

4. Complex Object Management

const { value: user, update } = useHybridSignal(storage, "user", {
  defaultValue: { name: "John", age: 30, preferences: { theme: "dark" } }
});

// Immutable updates (like signals)
update(u => ({ ...u, age: u.age + 1 }));
update(u => ({ 
  ...u, 
  preferences: { ...u.preferences, theme: "light" }
}));

5. Computed Values

// Used to establish a base signal with useHybridSignal that holds the user's data (specifically their age)
const { value: userData } = useHybridSignal(storage, "user", { defaultValue: { age: 30 } });

// Computed values that update automatically based on the base signal
const isAdult = useComputedHybridSignal(storage, "user", { age: 30 }, u => u.age >= 18);
const ageInDays = useComputedHybridSignal(storage, "user", { age: 30 }, u => u.age * 365);

// These computed values react to changes in userData.age
console.log(`User is adult: ${isAdult}`);
console.log(`Age in days: ${ageInDays}`);

⚡ Performance Comparison

| Operation | Traditional Signals | Storage-Based | Hybrid Signal | |-----------|-------------------|---------------|---------------| | UI Updates | ✅ Immediate | ⏳ Async | ✅ Immediate | | Storage Writes | ❌ None | ✅ Immediate | ⏳ Debounced | | Cross-tab Sync | ❌ No | ✅ Yes | ✅ Yes | | Persistence | ❌ No | ✅ Yes | ✅ Yes | | Memory Usage | ✅ Low | ⚡ Medium | ⚡ Medium | | CPU Usage | ✅ Low | ⚡ Medium | ✅ Low |

🔧 Advanced Features

Custom Error Handling

const { value, set } = useHybridSignal(storage, "data", {
  defaultValue: null,
  onError: (error) => {
    console.error('Storage error:', error);
    // Show user notification, retry logic, etc.
  }
});

Manual Change Detection

const { value, checkForChanges } = useHybridSignal(storage, "data", { defaultValue: 0 });

// Force check for external changes
const hasChanges = await checkForChanges();
if (hasChanges) {
  console.log('External changes detected!');
}

Disable Subscriptions

const { value, set } = useHybridSignal(storage, "local-data", {
  defaultValue: 0,
  subscribe: false, // No external change detection
  pollInterval: 0   // No polling
});

🎯 Best Practices

// ... TODO ...

Screen shot of testing (npm run dev and open http://localhost:5173/ in multiple tabs)

image

Importing Hooks

This library provides flexibility in how you can import the hooks:

  • Via the barrel file (index.js): Import all hooks and utilities from the main entry point for convenience.
    import { useUnstorage, useHybridSignal } from 'hybrid-storage';
  • Via individual hook files: Import specific hooks to reduce bundle size by only loading what you need.
    import { useUnstorage } from 'hybrid-storage/useUnstorage';
    import { useHybridSignal } from 'hybrid-storage/useHybridSignal';

Choose the method that best suits your project's needs for either ease of use or optimized bundle size.