react-universal-persistent-storage
v1.0.1
Published
Universal persistent storage for React
Maintainers
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-storagePlatform 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-adaptersFor 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-adapterQuick 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 implementationchildren: 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 keyinitialValue: T- Default value when no stored value existsoptions?: UseUniversalPersistentStorageOptions<T>- Configuration options
Returns:
value: T- Current stored valuesetValue: (newValue: T | ((prev: T) => T)) => Promise<void>- Update stored valueremoveValue: () => Promise<void>- Remove value from storageclearStorage: () => Promise<void>- Clear all storage datagetAllKeys?: () => Promise<string[]>- Get all storage keys (if supported)loading: boolean- Loading state during initial fetcherror: 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 adapterserialize?: (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-adaptersAvailable adapters:
createLocalStorageAdapter()- Browser localStoragecreateSessionStorageAdapter()- Browser sessionStoragecreateIndexedDBAdapter()- IndexedDB for large datacreateMemoryAdapter()- In-memory storage (testing)
React Native Adapter
npm install react-universal-persistent-storage-react-native-adapterAvailable adapters:
createAsyncStorageAdapter()- React Native AsyncStoragecreateSecureStorageAdapter()- 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
localStoragefor persistent data - Use
sessionStoragefor temporary data - Consider storage quotas and cleanup strategies
- Handle storage events for cross-tab synchronization
React Native
- Use
AsyncStoragefor 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
- Avoid Frequent Updates: Batch updates when possible
- Use Appropriate Serialization: Custom serializers for complex data
- Monitor Storage Size: Implement cleanup strategies for large datasets
- 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
