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 🙏

© 2025 – Pkg Stats / Ryan Hefner

react-universal-persistent-storage

v1.0.1

Published

Universal persistent storage for React

Readme

React Universal Persistent Storage

A universal, cross-platform persistent storage solution for React applications with full TypeScript support and adapter-based architecture.

Features

  • Cross-Platform: Works in browser (React) and React Native environments
  • Adapter Architecture: Port-based design for maximum flexibility
  • Type Safety: Full TypeScript support with generics
  • Automatic Loading: Handles data loading and synchronization
  • Real-time Sync: Optional subscription support for cross-tab synchronization
  • Custom Serialization: Support for custom serialize/deserialize functions
  • Error Handling: Built-in error handling with custom error callbacks
  • Rich API: Complete CRUD operations with additional utilities
  • Memory Efficient: Optimized re-renders and memory usage

Installation

Core Library

npm install react-universal-persistent-storage
# or
yarn add react-universal-persistent-storage

Platform Adapters

Choose the appropriate adapter for your platform:

For Web Applications:

npm install react-universal-persistent-storage react-universal-persistent-storage-web-adapters
# or
yarn add react-universal-persistent-storage react-universal-persistent-storage-web-adapters

For React Native:

npm install react-universal-persistent-storage react-universal-persistent-storage-react-native-adapter
# or
yarn add react-universal-persistent-storage react-universal-persistent-storage-react-native-adapter

Quick Start

Web Application

import React from 'react';
import { 
  UniversalPersistentStorageProvider, 
  useUniversalPersistentStorage 
} from 'react-universal-persistent-storage';
import { createLocalStorageAdapter } from 'react-universal-persistent-storage-web-adapters';

const storageAdapter = createLocalStorageAdapter();

function App() {
  return (
    <UniversalPersistentStorageProvider adapter={storageAdapter}>
      <UserProfile />
    </UniversalPersistentStorageProvider>
  );
}

function UserProfile() {
  const { value: user, setValue: setUser, loading, error } = useUniversalPersistentStorage(
    'user-profile',
    { name: 'Guest', email: '' }
  );

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

  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      <button onClick={() => setUser({ ...user, name: 'John Doe' })}>
        Update Name
      </button>
    </div>
  );
}

React Native Application

import React from 'react';
import { 
  UniversalPersistentStorageProvider, 
  useUniversalPersistentStorage 
} from 'react-universal-persistent-storage';
import { createAsyncStorageAdapter } from 'react-universal-persistent-storage-react-native-adapter';

const storageAdapter = createAsyncStorageAdapter();

function App() {
  return (
    <UniversalPersistentStorageProvider adapter={storageAdapter}>
      <Settings />
    </UniversalPersistentStorageProvider>
  );
}

function Settings() {
  const { value: settings, setValue: setSettings, loading } = useUniversalPersistentStorage(
    'app-settings',
    { theme: 'light', notifications: true }
  );

  if (loading) return <Text>Loading settings...</Text>;

  return (
    <View>
      <Text>Theme: {settings.theme}</Text>
      <Button
        title="Toggle Theme"
        onPress={() => setSettings({
          ...settings,
          theme: settings.theme === 'light' ? 'dark' : 'light'
        })}
      />
    </View>
  );
}

Architecture

Adapter Pattern (Ports)

The library uses a port-based architecture where storage implementations conform to the StoragePort interface:

interface StoragePort {
  getItem: (key: string) => Promise<string | null> | string | null;
  setItem: (key: string, value: string) => Promise<void> | void;
  removeItem: (key: string) => Promise<void> | void;
  clear: () => Promise<void> | void;
  getAllKeys?: () => Promise<string[]> | string[];
  subscribe?: (callback: (key: string, value: string | null) => void) => () => void;
}

This design allows you to:

  • Switch between storage implementations without changing your components
  • Test your components with mock storage adapters
  • Create custom storage solutions for specific needs
  • Maintain consistency across different platforms

API Reference

Provider

<UniversalPersistentStorageProvider>

Provides a storage adapter to the component tree.

import { UniversalPersistentStorageProvider } from 'react-universal-persistent-storage';

<UniversalPersistentStorageProvider adapter={storageAdapter}>
  <App />
</UniversalPersistentStorageProvider>

Props:

  • adapter: StoragePort - Storage adapter implementation
  • children: ReactNode - Child components

Hook

useUniversalPersistentStorage<T>(key, initialValue, options?)

Main hook for persistent storage management.

const {
  value,
  setValue,
  removeValue,
  clearStorage,
  getAllKeys,
  loading,
  error
} = useUniversalPersistentStorage('my-key', initialValue, options);

Parameters:

  • key: string - Storage key
  • initialValue: T - Default value when no stored value exists
  • options?: UseUniversalPersistentStorageOptions<T> - Configuration options

Returns:

  • value: T - Current stored value
  • setValue: (newValue: T | ((prev: T) => T)) => Promise<void> - Update stored value
  • removeValue: () => Promise<void> - Remove value from storage
  • clearStorage: () => Promise<void> - Clear all storage data
  • getAllKeys?: () => Promise<string[]> - Get all storage keys (if supported)
  • loading: boolean - Loading state during initial fetch
  • error: Error | null - Last error that occurred

Options

UseUniversalPersistentStorageOptions<T>

interface UseUniversalPersistentStorageOptions<T> {
  adapter?: StoragePort;
  serialize?: (value: T) => string;
  deserialize?: (value: string) => T;
  onError?: (error: Error) => void;
}

Properties:

  • adapter?: StoragePort - Override context adapter
  • serialize?: (value: T) => string - Custom serialization (default: JSON.stringify)
  • deserialize?: (value: string) => T - Custom deserialization (default: JSON.parse)
  • onError?: (error: Error) => void - Error callback

Available Adapters

Web Adapters

npm install react-universal-persistent-storage-web-adapters

Available adapters:

  • createLocalStorageAdapter() - Browser localStorage
  • createSessionStorageAdapter() - Browser sessionStorage
  • createIndexedDBAdapter() - IndexedDB for large data
  • createMemoryAdapter() - In-memory storage (testing)

React Native Adapter

npm install react-universal-persistent-storage-react-native-adapter

Available adapters:

  • createAsyncStorageAdapter() - React Native AsyncStorage
  • createSecureStorageAdapter() - Encrypted storage for sensitive data

Advanced Usage Examples

Custom Serialization

interface User {
  id: number;
  name: string;
  createdAt: Date;
}

function UserComponent() {
  const { value: user, setValue: setUser } = useUniversalPersistentStorage<User>(
    'user-data',
    { id: 0, name: '', createdAt: new Date() },
    {
      serialize: (user) => JSON.stringify({
        ...user,
        createdAt: user.createdAt.toISOString()
      }),
      deserialize: (data) => {
        const parsed = JSON.parse(data);
        return {
          ...parsed,
          createdAt: new Date(parsed.createdAt)
        };
      }
    }
  );

  return (
    <div>
      <p>User: {user.name}</p>
      <p>Created: {user.createdAt.toLocaleDateString()}</p>
      <button onClick={() => setUser({
        ...user,
        name: 'Updated User',
        createdAt: new Date()
      })}>
        Update User
      </button>
    </div>
  );
}

Error Handling

function RobustComponent() {
  const { value, setValue, error } = useUniversalPersistentStorage(
    'sensitive-data',
    { secret: '' },
    {
      onError: (error) => {
        console.error('Storage error:', error);
        // Send to error tracking service
        errorTracker.captureException(error);
      }
    }
  );

  if (error) {
    return (
      <div className="error-container">
        <h2>Storage Error</h2>
        <p>Unable to access persistent storage: {error.message}</p>
        <button onClick={() => window.location.reload()}>
          Retry
        </button>
      </div>
    );
  }

  return <div>Data: {value.secret}</div>;
}

Multiple Storage Adapters

import { createLocalStorageAdapter, createSessionStorageAdapter } from 'react-universal-persistent-storage-web-adapters';

const persistentAdapter = createLocalStorageAdapter();
const sessionAdapter = createSessionStorageAdapter();

function MultiStorageComponent() {
  // Persistent data
  const { value: settings } = useUniversalPersistentStorage(
    'user-settings',
    { theme: 'light' },
    { adapter: persistentAdapter }
  );
  
  // Session-only data
  const { value: tempData } = useUniversalPersistentStorage(
    'temp-data',
    { lastAction: '' },
    { adapter: sessionAdapter }
  );

  return (
    <div>
      <p>Theme: {settings.theme}</p>
      <p>Last Action: {tempData.lastAction}</p>
    </div>
  );
}

Cross-Tab Synchronization

// Using an adapter that supports subscription (like localStorage)
import { createLocalStorageAdapter } from 'react-universal-persistent-storage-web-adapters';

const adapter = createLocalStorageAdapter({ enableSubscription: true });

function SyncedCounter() {
  const { value: count, setValue: setCount } = useUniversalPersistentStorage(
    'shared-counter',
    0,
    { adapter }
  );

  // This will automatically sync across browser tabs
  return (
    <div>
      <h2>Synced Counter: {count}</h2>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      <p>Open this page in multiple tabs to see sync in action!</p>
    </div>
  );
}

Storage Management

function StorageManager() {
  const { getAllKeys, clearStorage } = useUniversalPersistentStorage(
    'manager',
    {}
  );

  const [keys, setKeys] = useState<string[]>([]);

  const loadKeys = async () => {
    if (getAllKeys) {
      const allKeys = await getAllKeys();
      setKeys(allKeys);
    }
  };

  const handleClearAll = async () => {
    await clearStorage();
    setKeys([]);
  };

  return (
    <div>
      <h2>Storage Manager</h2>
      <button onClick={loadKeys}>Load All Keys</button>
      <button onClick={handleClearAll}>Clear All Storage</button>
      
      <ul>
        {keys.map(key => (
          <li key={key}>{key}</li>
        ))}
      </ul>
    </div>
  );
}

Form Persistence

interface FormData {
  name: string;
  email: string;
  message: string;
}

function PersistentForm() {
  const { value: formData, setValue: setFormData, removeValue } = useUniversalPersistentStorage<FormData>(
    'draft-form',
    { name: '', email: '', message: '' }
  );

  const updateField = (field: keyof FormData, value: string) => {
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // Submit form data
    await submitForm(formData);
    
    // Clear draft after successful submission
    await removeValue();
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>Name:</label>
        <input
          value={formData.name}
          onChange={(e) => updateField('name', e.target.value)}
        />
      </div>
      <div>
        <label>Email:</label>
        <input
          value={formData.email}
          onChange={(e) => updateField('email', e.target.value)}
        />
      </div>
      <div>
        <label>Message:</label>
        <textarea
          value={formData.message}
          onChange={(e) => updateField('message', e.target.value)}
        />
      </div>
      <button type="submit">Submit</button>
      
      {(formData.name || formData.email || formData.message) && (
        <p>✓ Draft saved automatically</p>
      )}
    </form>
  );
}

Creating Custom Adapters

You can create custom storage adapters by implementing the StoragePort interface:

import { StoragePort } from 'react-universal-persistent-storage';

class CustomStorageAdapter implements StoragePort {
  private storage = new Map<string, string>();
  private subscribers = new Set<(key: string, value: string | null) => void>();

  async getItem(key: string): Promise<string | null> {
    return this.storage.get(key) || null;
  }

  async setItem(key: string, value: string): Promise<void> {
    this.storage.set(key, value);
    this.notifySubscribers(key, value);
  }

  async removeItem(key: string): Promise<void> {
    this.storage.delete(key);
    this.notifySubscribers(key, null);
  }

  async clear(): Promise<void> {
    const keys = Array.from(this.storage.keys());
    this.storage.clear();
    keys.forEach(key => this.notifySubscribers(key, null));
  }

  async getAllKeys(): Promise<string[]> {
    return Array.from(this.storage.keys());
  }

  subscribe(callback: (key: string, value: string | null) => void): () => void {
    this.subscribers.add(callback);
    return () => this.subscribers.delete(callback);
  }

  private notifySubscribers(key: string, value: string | null) {
    this.subscribers.forEach(callback => callback(key, value));
  }
}

export const createCustomAdapter = () => new CustomStorageAdapter();

Testing

The adapter architecture makes testing straightforward:

// test-utils/mockAdapter.ts
import { StoragePort } from 'react-universal-persistent-storage';

export const createMockAdapter = (initialData: Record<string, string> = {}): StoragePort => {
  const storage = new Map(Object.entries(initialData));
  
  return {
    getItem: jest.fn(async (key: string) => storage.get(key) || null),
    setItem: jest.fn(async (key: string, value: string) => {
      storage.set(key, value);
    }),
    removeItem: jest.fn(async (key: string) => {
      storage.delete(key);
    }),
    clear: jest.fn(async () => {
      storage.clear();
    }),
    getAllKeys: jest.fn(async () => Array.from(storage.keys())),
  };
};

// Component.test.tsx
import { render, screen } from '@testing-library/react';
import { UniversalPersistentStorageProvider } from 'react-universal-persistent-storage';
import { createMockAdapter } from './test-utils/mockAdapter';
import MyComponent from './MyComponent';

test('loads data from storage', async () => {
  const mockAdapter = createMockAdapter({
    'user-data': JSON.stringify({ name: 'John Doe' })
  });

  render(
    <UniversalPersistentStorageProvider adapter={mockAdapter}>
      <MyComponent />
    </UniversalPersistentStorageProvider>
  );

  expect(await screen.findByText('Welcome, John Doe!')).toBeInTheDocument();
});

Best Practices

1. Choose the Right Adapter

  • localStorage: Long-term data that should persist across sessions
  • sessionStorage: Temporary data for current session only
  • AsyncStorage: React Native persistent storage
  • IndexedDB: Large amounts of data or complex queries
  • Memory: Testing or temporary runtime data

2. Handle Loading States

Always handle the loading state for better UX:

function MyComponent() {
  const { value, loading, error } = useUniversalPersistentStorage('data', {});

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  
  return <div>{/* Your content */}</div>;
}

3. Use Meaningful Keys

Use descriptive, namespaced keys to avoid conflicts:

// Good
'user:profile'
'app:settings'
'form:contact-draft'

// Avoid
'data'
'info'
'temp'

4. Type Your Data

Always use TypeScript interfaces for your stored data:

interface UserPreferences {
  theme: 'light' | 'dark';
  language: string;
  notifications: boolean;
}

const { value: preferences } = useUniversalPersistentStorage<UserPreferences>(
  'user:preferences',
  { theme: 'light', language: 'en', notifications: true }
);

5. Handle Errors Gracefully

Implement proper error handling:

const { value, error } = useUniversalPersistentStorage(
  'critical-data',
  defaultValue,
  {
    onError: (error) => {
      // Log error
      console.error('Storage error:', error);
      
      // Fallback to different storage
      // or show user-friendly message
    }
  }
);

Platform Considerations

Web Applications

  • Use localStorage for persistent data
  • Use sessionStorage for temporary data
  • Consider storage quotas and cleanup strategies
  • Handle storage events for cross-tab synchronization

React Native

  • Use AsyncStorage for most persistent data
  • Consider encrypted storage for sensitive information
  • Handle app backgrounding and restoration
  • Be mindful of storage performance on slower devices

Cross-Platform Development

  • Use the same key naming conventions
  • Test on all target platforms
  • Consider data migration strategies
  • Handle platform-specific edge cases

Performance Tips

  1. Avoid Frequent Updates: Batch updates when possible
  2. Use Appropriate Serialization: Custom serializers for complex data
  3. Monitor Storage Size: Implement cleanup strategies for large datasets
  4. Optimize Re-renders: Use proper dependency arrays and memoization

Troubleshooting

Common Issues

Error: "Storage adapter is not provided"

  • Ensure your component is wrapped in UniversalPersistentStorageProvider
  • Or pass adapter directly in hook options

Data not persisting across app restarts

  • Check if you're using the correct adapter (localStorage vs sessionStorage)
  • Verify storage quotas aren't exceeded

Cross-tab synchronization not working

  • Ensure your adapter supports subscription
  • Check browser security settings

Performance issues

  • Consider using IndexedDB for large datasets
  • Implement data pagination or lazy loading

Migration Guide

From useState to Universal Persistent Storage

// Before
const [user, setUser] = useState({ name: 'Guest' });

// After
const { value: user, setValue: setUser } = useUniversalPersistentStorage(
  'user-data',
  { name: 'Guest' }
);

From localStorage directly

// Before
const [user, setUser] = useState(() => {
  const stored = localStorage.getItem('user');
  return stored ? JSON.parse(stored) : { name: 'Guest' };
});

useEffect(() => {
  localStorage.setItem('user', JSON.stringify(user));
}, [user]);

// After
const { value: user, setValue: setUser } = useUniversalPersistentStorage(
  'user',
  { name: 'Guest' }
);

License

MIT License