@superutils/store
A generic, reactive, persistent and fully-typed Map-like data store with advanced search, filtering, and sorting capabilities. It supports both in-memory caching and persistent storage (LocalStorage in browsers, or JSON files in NodeJS).
Built on RxJS for reactive data handling, it is optimized for small to medium datasets and provides a seamless way to manage application state with optional persistence.
Table of Contents
Installation
NPM
Install using your favorite package manager (e.g., npm, yarn, pnpm, bun, etc.):
npm install @superutils/storeDependency: @superutils/core will be automatically installed by package manager
CDN / Browser
If you are not using a bundler, you can include the minified browser build directly:
<script src="https://unpkg.com/@superutils/store@latest/dist/browser/index.min.js"></script>OR,
<script src="https://cdn.jsdelivr.net/npm/@superutils/store/dist/browser/index.min.js"></script>Basic Usage
Map-based Store
The Store class can be used just like a standard JavaScript Map, but with the added benefit of optional persistence and reactivity.
import { createStore } from '@superutils/store'
// Initialize the store that saves the stringified data to `localStorage.users` in the browser.
// Bypassing the name or using `null` will create an in-memory store.
const userStorage = createStore({ name: 'users' })
// Set and get values
userStorage.set('alice', { name: 'Alice', age: 30 })
// functional update
userStorage.set('alice', alice => alice ?? { name: 'Alice', age: 30 })
console.log(userStorage.get('alice')) // prints: { name: 'Alice', age: 30 }
console.log(userStorage.size) // 1Object-based Store
createObjectStore provides a type-safe way to manage a single plain object as a store, where keys of the object become keys in the store.
import { createObjectStore } from '@superutils/store'
const userStore = createObjectStore({
name: 'user-profile',
initialValue: {
age: 25,
name: 'Jane Doe',
roles: ['guest'],
},
})
console.log(userStore.get('name'), userStore.get('age')) // Prints: 'Jane Doe' 25
console.log(userStore.toObject()) // prints: { age: 25, name: 'Jane Doe', roles: [ 'guest' ] }Persistent Storage (NodeJS)
In NodeJS environments, you can use node-localstorage to persist your data to the file system.
import { createStore } from '@superutils/store'
import { LocalStorage } from 'node-localstorage'
// Provide a localStorage implementation for NodeJS that can be used throughout the application mimicking the browser LocalStorage behavior.
globalThis.localStorage = new LocalStorage(
'./data', // directory to store files in
1e7, // max file size
)
// Create a store that saves the data to ./data/settings.json
// Bypassing the name or using `null` will create an in-memory store.
const store = createStore({ name: 'settings.json' })
store.set('theme', 'dark') // Automatically saved to ./data/settings.json
/**
* Alternatively, you can also provide a LocalStorage instance to each Store instance.
*/
createStore({
name: 'settings.json',
storage: new LocalStorage('./data', 1e9),
})Advanced Usage
Data Validation
To ensure data integrity, you can provide a validate object containing hooks for various operations (set, setAll, delete, clear, write). These hooks are executed immediately before the store's internal state is updated. If a validator throws an error, the operation is aborted.
import { createObjectStore } from '@superutils/store'
const settingsStore = createObjectStore({
name: 'app-settings',
initialValue: {
theme: 'light',
version: '1.0.0',
},
validate: {
set([key, value]) {
console.log('size:', this.size) // "this" refers to the store instance
if (key !== 'theme' || ['light', 'dark', 'system'].includes(value)) return
// throw error to abort operation
throw new Error(`Invalid theme: ${value}`)
},
delete: ([keys]) => {
if (!keys.includes('version')) return
throw new Error('The "version" key is protected and cannot be deleted')
},
},
})
settingsStore.set('theme', 'system')
console.log(settingsStore.get('theme')) // 'system'
try {
settingsStore.set('theme', 'invalid') // throws error
} catch (err) {
console.log(err)
}Reactive Updates (RxJS & Callbacks)
You can subscribe to changes using the internal RxJS Subject or a simple onChange callback.
import { createStore } from '@superutils/store'
const store = createStore({
name: 'my-data',
onChange: data => console.log('Data changed!', data),
})
// Or use the RxJS subject directly
const sub = store.subject$.subscribe(data => {
console.log('Reactive update:', data)
})
store.set('key', 'value')Search and Filtering
Store provides powerful search and filter capabilities directly on your data.
import { createStore } from '@superutils/store'
const store = createStore({
name: 'products',
// Pre-populate the storage with sample data
initialValue: new Map([
[1, { id: 1, name: 'Laptop', category: 'electronics', price: 1000 }],
[2, { id: 2, name: 'Chair', category: 'furniture', price: 150 }],
]),
})
// Search for items using a query object
const searchResult = store.search({
asMap: false,
query: { category: 'electronics' },
})
console.log(searchResult) // [{ id: 1, name: 'Laptop', ... }]
// Filter items using a predicate
const expensiveItems = store.filter(val => val.price > 500)Attaching Business Logic: Store Augmentation
Using createStore, you can attach custom business logic to your store instance, allowing you to encapsulate operations without having to create a subclass.
import { createStore } from '@superutils/store'
const getContext = store => ({
// Context can be an object or a function that returns an object store => ({
get isAuthenticated() {
return store.has('token')
},
login: async () => {
store.set('token', 'some-token')
},
logout: () => store.delete('token'),
})
const authStore = createStore(
{
initialValue: new Map(),
name: 'auth',
},
getContext,
)
// Access your custom logic directly from the store instance
if (!authStore.isAuthenticated) {
authStore.login().then(() => console.log('Logged in'))
}Object-based Store With Augmentation
createObjectStore supports augmentation the same way as createStore.
import { createObjectStore } from '@superutils/store'
type UserProfile = {
age: number
name: string
roles: string[]
}
const userStore = createObjectStore(
{
name: 'user-profile',
initialValue: {
age: 25,
name: 'Jane Doe',
roles: ['guest'],
} as UserProfile,
},
store => ({
promoteToAdmin() {
// Update properties with type safety
store.set('roles', (roles = []) => [...roles, 'admin'])
},
}),
)
userStore.promoteToAdmin()
console.log(userStore.get('roles')) // ['guest', 'admin']OOP: Subclassing Store
You can extend the Store class to create custom store implementations with specialized logic or default behaviors.
import { Store } from '@superutils/store'
interface Product {
id: number
name: string
price: number
inStock: boolean
}
class ProductStore extends Store<number, Product, false> {
constructor(
...[name, options]: ConstructorParameters<
typeof Store<number, Product, false>
>
) {
super(name, { ...options, delay: 100 }) // Set a default delay for this store type
}
getInStockProducts(limit?: number, asMap = false) {
return this.filter(product => product.inStock, limit, asMap)
}
}
const products = new ProductStore('my-products')
products.set(1, { id: 1, name: 'Laptop', price: 1200, inStock: true })
products.set(2, { id: 2, name: 'Mouse', price: 25, inStock: false })
console.log(products.getInStockProducts()) // { id: 1, name: 'Laptop', price: 1200, inStock: true }Enumerations
Classes
Interfaces
Type Aliases
- ContextExcludeProps
- ContextReturn
- ContextValidate
- ObjectStore_Options
- StorageCompact
- Store_DelayOptions
- Store_OptionKeys
- Store_Options
- Store_Parse
- Store_Search
- Store_Sort
- Store_SortByComparator
- Store_SortByKey
- Store_SortByPropertyName
- Store_SortOptions
- Store_Stringify
- Store_ToJSON
- Store_Validate
- Store_ValidateAction
- TypedMap
Variables
Functions
References
default
Renames and re-exports Store