Lightweight and powerful extension for Zustand providing debounced JSON state storage.
Zustand Debounce enhances the capabilities of Zustand by introducing a debounced JSON state storage system. By delaying and grouping write operations to storage, you can significantly reduce the number of write operations, improving performance and efficiency in your applications.
β Ultra Lightweight: Only 1.19 kB gzipped π
β Easy Integration: Seamlessly integrates into your existing projects π
β Customizable Debounce Time: Adjust the debounce time to suit your needs β³
β Reduced Write Operations: Avoid frequent writes to storage, optimizing performance π
β Retry Mechanism: Automatically retries failed write operations with customizable settings π
β
Advanced Retry with Exponential Backoff: Configure exponential backoff with maxRetries, retryDelay, and backoffMultiplier for more resilient retries π
β
Multiple Storage Adapters: Choose between 'localStorage', 'sessionStorage', or 'memoryStorage' adapters ποΈ
β
Extended Event System: Additional events like onFlush, onRetry, and onError for better control π£
β TTL Support: Specify a Time-To-Live for stored data β
β Custom Serialization: Use custom serialization and deserialization functions π οΈ
β Full TypeScript Support: Fully typed for TypeScript projects π
β Plugin System: Extensible plugin architecture for custom functionality π
# Using npm
npm install zustand-debounce
# Using yarn
yarn add zustand-debounce
# Using pnpm
pnpm add zustand-debounce- π§ Usage
- βοΈ Options
- π Advanced Usage
- π Plugin System
- π€ Contributing
- π License
- π¬ Support
To start using Zustand Debounce, replace createJSONStorage with createDebouncedJSONStorage in your Zustand store setup. This will enable delayed writes to your storage.
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { createDebouncedJSONStorage } from 'zustand-debounce';
// Your store interface
interface PersonState {
name: string;
age: number;
// Other state properties
}
interface Actions {
setName: (name: string) => void;
setAge: (age: number) => void;
// Other actions
}
// Create the store
export const usePersonStore = create<PersonState & Actions>()(
persist(
(set) => ({
// Initial state
name: '',
age: 0,
// Actions
setName: (name) => set({ name }),
setAge: (age) => set({ age }),
}),
{
name: 'person-storage',
storage: createDebouncedJSONStorage('localStorage', {
debounceTime: 2000, // Debounce time in milliseconds β³
// Other options can be specified here
}),
}
)
);With the above setup, changes to the store will be saved to the storage after a 2-second delay, grouping multiple rapid changes into a single write operation.
createDebouncedJSONStorage accepts a variety of options to customize its behavior:
| Option | Type | Default | Description |
|---|---|---|---|
debounceTime |
number |
0 |
The debounce time in milliseconds. Write operations will be delayed by this amount of time. If multiple writes occur within this period, they will be grouped into a single write. |
throttleTime |
number |
0 |
The throttle time in milliseconds. Ensures that write operations are not performed more frequently than this interval. |
immediately |
boolean |
false |
If set to true, write operations will occur immediately without any delay. |
maxRetries |
number |
0 |
The maximum number of times to retry a failed write operation. |
retryDelay |
number |
0 |
The delay in milliseconds between retry attempts for failed write operations. |
backoffMultiplier |
number |
1 |
The multiplier used for exponential backoff between retry attempts. Each retry will wait retryDelay * (backoffMultiplier ^ attempt) milliseconds. |
onWrite |
(key: string, value: string) => void |
undefined |
A callback function that is called immediately when setItem is invoked, before the debounce delay. |
onSave |
(key: string, value: string) => void |
undefined |
A callback function that is called after the debounce delay when the data is actually saved to storage. |
onFlush |
(key: string, value: string) => void |
undefined |
A callback function that is called when a manual flush operation is executed. |
onRetry |
(key: string, attempt: number, error: any, delay: number) => void |
undefined |
A callback function that is called before each retry attempt, providing information about the retry attempt number, the error that occurred, and the delay before the next attempt. |
onError |
(key: string, error: any) => void |
undefined |
A callback function that is called when all retry attempts have failed. |
serialize |
(state: unknown) => string |
JSON.stringify |
A custom function to serialize the state before saving it to storage. |
deserialize |
(str: string) => unknown |
JSON.parse |
A custom function to deserialize the state after retrieving it from storage. |
ttl |
number |
0 |
Time-to-live in milliseconds for the stored data. After this period, the data will be considered expired and removed from storage. |
Here is an example demonstrating the use of multiple options:
import { createDebouncedJSONStorage } from 'zustand-debounce';
// Example with localStorage
const localStorageExample = createDebouncedJSONStorage('localStorage', {
debounceTime: 1000, // Delay write operations by 1 second
throttleTime: 5000, // Ensure writes are at least 5 seconds apart
immediately: false, // Do not write immediately
maxRetries: 3, // Retry failed writes up to 3 times
retryDelay: 2000, // Wait 2 seconds between retries
ttl: 86400000, // Data expires after 24 hours
onWrite: (key, value) => {
console.log(`Write initiated for ${key}`);
},
onSave: (key, value) => {
console.log(`Data saved for ${key}`);
},
serialize: (state) => {
// Custom serialization logic
return JSON.stringify(state);
},
deserialize: (str) => {
// Custom deserialization logic
return JSON.parse(str);
},
});
// Example with sessionStorage and advanced retry + events
const sessionStorageExample = createDebouncedJSONStorage('sessionStorage', {
debounceTime: 500,
maxRetries: 3,
retryDelay: 1000,
backoffMultiplier: 2, // Each retry will wait longer: 1s, 2s, 4s
onRetry: (key, attempt, error, delay) => {
console.log(`Retry ${attempt} for ${key} after ${delay}ms. Error: ${error.message}`);
},
onError: (key, error) => {
console.error(`All retries failed for ${key}:`, error);
},
onFlush: (key, value) => {
console.log(`Manual flush executed for ${key}`);
}
});
// Example with in-memory storage
const memoryStorageExample = createDebouncedJSONStorage('memoryStorage', {
debounceTime: 100,
immediately: true
});
// Example with custom storage adapter
const customStorageExample = createDebouncedJSONStorage({
getItem: async (key: string) => {
// Implement your custom get logic here
return await myCustomDatabase.get(key);
},
setItem: async (key: string, value: string) => {
// Implement your custom set logic here
await myCustomDatabase.set(key, value);
},
removeItem: async (key: string) => {
// Implement your custom remove logic here
await myCustomDatabase.delete(key);
}
}, {
debounceTime: 1000,
maxRetries: 3,
retryDelay: 1000,
backoffMultiplier: 2
});You can create your own storage adapter by implementing the StateStorage interface. This allows you to integrate any storage solution with Zustand Debounce:
interface StateStorage {
getItem: (key: string) => Promise<string | null> | string | null;
setItem: (key: string, value: string) => Promise<void> | void;
removeItem: (key: string) => Promise<void> | void;
}
// Example: Custom IndexedDB adapter
const createIndexedDBAdapter = (dbName: string, storeName: string): StateStorage => {
// Open IndexedDB connection
const dbPromise = indexedDB.open(dbName, 1);
dbPromise.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore(storeName);
};
return {
async getItem(key) {
const db = await dbPromise;
const transaction = db.transaction(storeName, 'readonly');
const store = transaction.objectStore(storeName);
return await store.get(key);
},
async setItem(key, value) {
const db = await dbPromise;
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
await store.put(value, key);
},
async removeItem(key) {
const db = await dbPromise;
const transaction = db.transaction(storeName, 'readwrite');
const store = transaction.objectStore(storeName);
await store.delete(key);
}
};
};
// Use your custom adapter
const customDBStorage = createDebouncedJSONStorage(
createIndexedDBAdapter('myDB', 'zustand-store'),
{
debounceTime: 1000,
// ... other options
}
);Zustand Debounce now features a powerful plugin system that allows you to extend storage functionality with custom transformations. Plugins can intercept data before it's written to storage and after it's read, enabling features like compression, encryption, and more.
Plugins are passed as an array in the plugins option. They are executed in order, allowing you to compose multiple transformations:
import { createDebouncedJSONStorage } from 'zustand-debounce';
import { compress } from 'zustand-debounce-compress';
const storage = createDebouncedJSONStorage('localStorage', {
debounceTime: 1000,
plugins: [
compress({ algorithm: 'lz-string' })
]
});Automatically compress your state to reduce storage size by 50-80%. Perfect for large state objects or apps approaching localStorage quota limits.
npm install zustand-debounce-compressimport { compress } from 'zustand-debounce-compress';
const storage = createDebouncedJSONStorage('localStorage', {
debounceTime: 1000,
plugins: [
compress({ algorithm: 'lz-string' }) // or 'base64'
]
});Features:
- Reduces storage size by 50-80% for typical JSON data
- Two algorithms:
'lz-string'(fastest, best compression) and'base64'(URI-safe) - Transparent compression/decompression
- Graceful error handling
You can create your own plugins by implementing the Plugin interface:
import type { Plugin } from 'zustand-debounce';
const myPlugin: Plugin = {
name: 'my-custom-plugin',
// Transform data before writing to storage
beforeSetItem: (value: string) => {
// Your transformation logic here
return transformedValue;
},
// Transform data after reading from storage
afterGetItem: (value: string | null) => {
if (!value) return null;
// Your transformation logic here
return transformedValue;
}
};
// Use your plugin
const storage = createDebouncedJSONStorage('localStorage', {
plugins: [myPlugin]
});Plugin execution order:
- On write: Plugin 1 β Plugin 2 β Plugin N β Storage
- On read: Storage β Plugin 1 β Plugin 2 β Plugin N
Plugins can be composed together for powerful transformations:
const storage = createDebouncedJSONStorage('localStorage', {
debounceTime: 1000,
plugins: [
compress({ algorithm: 'lz-string' }), // Compress first
// encrypt({ key: myKey }), // Then encrypt (coming soon)
]
});Note: Order matters! The first plugin processes data first on write, and processes data first on read.
Contributions are welcome! If you have ideas for improvements or have found a bug, please open an issue or submit a pull request.
- Fork the repository
- Create a new branch: git checkout -b feature/your-feature-name
- Make your changes and commit them: git commit -m 'Add some feature'
- Push to the branch: git push origin feature/your-feature-name
- Open a pull request
Please ensure your code follows the project's coding standards and includes appropriate tests.
This project is licensed under the MIT License - see the LICENSE file for details.
If you find this project useful, please consider giving it a β on GitHub. If you have any questions or need support, feel free to open an issue or contact me.
with π by AbianS