@asaidimu/react-store
v5.1.1
Published
Efficient react state manager.
Downloads
531
Readme
@asaidimu/react-store
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware and artifact management system.
⚠️ Beta Warning
This package is currently in beta. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
Table of Contents
- Overview & Features
- Installation & Setup
- Usage Documentation
- Project Architecture
- Development & Contributing
- Additional Information
Overview & Features
@asaidimu/react-store provides an efficient and predictable way to manage complex application state in React applications. It goes beyond basic state management by integrating features typically found in separate libraries, such as artifact management, data persistence, and comprehensive observability tools, directly into its core. This allows developers to build robust, high-performance applications with deep insights into state changes and application behavior.
Designed with modern React in mind, it leverages useSyncExternalStore for optimal performance and reactivity, ensuring components re-render only when relevant parts of the state change. Its flexible design supports a variety of use cases, from simple counter applications to complex data flows requiring atomic updates and cross-tab synchronization. The library is built with TypeScript from the ground up, offering strong type safety throughout your application's state, actions, and resolved artifacts.
Key Features
- 📊 Reactive State Management: Automatically tracks dependencies to optimize component renders and ensure efficient updates using
useSyncExternalStore. - 🛡️ Type-Safe: Developed entirely in TypeScript, providing strict type checking for state, actions, artifacts, and middleware.
- ⚙️ Middleware Pipeline: Implement custom logic to
transformorvalidatestate changes before they are applied. - 💾 Built-in Persistence: Seamlessly integrate with web storage mechanisms like
IndexedDBandWebStorage(localStorage/sessionStorage), including cross-tab synchronization. - 🔍 Deep Observability: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities via the
StoreObserverinstance. - 🚀 Artifact Management: Define and reactively resolve asynchronous resources, services, or derived data, enabling advanced dependency injection patterns and lazy loading of complex logic.
- ⚡ Performance Optimized: Features intelligent selector caching and debounced actions with configurable immediate execution to prevent rapid successive calls and ensure smooth application performance.
- ⏱️ Action Loading States: Track the real-time loading status of individual actions, providing immediate feedback on asynchronous operations.
- ⚛️ React 19+ Ready: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
- 🗑️ Explicit Deletions: Use
Symbol.for("delete")to explicitly remove properties from nested state objects.
Installation & Setup
Prerequisites
- Node.js (v18 or higher recommended)
- React (v19 or higher recommended)
- A package manager like
bun,npm, oryarn. This project explicitly usesbun.
Installation Steps
To add @asaidimu/react-store to your project, run one of the following commands:
bun add @asaidimu/react-store
# or
npm install @asaidimu/react-store
# or
yarn add @asaidimu/react-storeConfiguration
No global configuration is required. All options are passed during store creation via the createStore function. Configuration includes enabling metrics, persistence, and performance thresholds.
Verification
You can verify the installation by importing createStore and setting up a basic store:
import { createStore } from '@asaidimu/react-store';
interface MyState {
value: string;
count: number;
}
const useMyStore = createStore<MyState, any, any>({
state: { value: 'hello', count: 0 },
actions: {
setValue: (_, newValue: string) => ({ value: newValue }),
increment: ({ state }) => ({ count: state.count + 1 }),
},
});
function MyComponent() {
const { select, actions } = useMyStore(); // Instantiate the hook
const currentValue = select(s => s.value);
const currentCount = select(s => s.count);
return (
<div>
<p>Value: {currentValue}</p>
<p>Count: {currentCount}</p>
<button onClick={() => actions.setValue('world')}>Set Value to 'world'</button>
<button onClick={() => actions.increment()}>Increment Count</button>
</div>
);
}
// Render MyComponent in your React app.
// If no errors are thrown during installation or when running this basic example,
// the package is correctly installed and configured.Usage Documentation
Creating a Store
Define your application state, actions, and optionally artifacts, then create a store using createStore. The actions object maps action names to functions that receive an ActionContext (containing the current state and a resolve function for artifacts) and any additional arguments.
// ui/store.tsx (Example)
import { createStore } from '@asaidimu/react-store'; // Assuming direct import or wrapper
export interface Product {
id: number;
name: string;
price: number;
stock: number;
image: string;
}
export interface CartItem extends Product {
quantity: number;
}
export interface Order {
id: string;
items: CartItem[];
total: number;
date: Date;
}
export interface ECommerceState extends Record<string, any>{
products: Product[];
cart: CartItem[];
orders: Order[];
topSellers: { id: number; name: string; sales: number }[];
activeUsers: number;
// A property to demonstrate artifact dependency
currency: string;
}
const initialState: ECommerceState = {
products: [
{ id: 1, name: 'Wireless Mouse', price: 25.99, stock: 150, image: 'https://placehold.co/600x400/white/black?text=Mouse' },
{ id: 2, name: 'Mechanical Keyboard', price: 79.99, stock: 100, image: 'https://placehold.co/600x400/white/black?text=Keyboard' },
{ id: 3, name: '4K Monitor', price: 349.99, stock: 75, image: 'https://placehold.co/600x400/white/black?text=Monitor' },
{ id: 4, name: 'Webcam', price: 45.50, stock: 120, image: 'https://placehold.co/600x400/white/black?text=Webcam' },
{ id: 5, name: 'USB-C Hub', price: 39.99, stock: 200, image: 'https://placehold.co/600x400/white/black?text=Hub' },
],
cart: [],
orders: [],
topSellers: [
{ id: 2, name: 'Mechanical Keyboard', sales: 120 },
{ id: 3, name: '4K Monitor', sales: 85 },
{ id: 1, name: 'Wireless Mouse', sales: 80 },
{ id: 5, name: 'USB-C Hub', sales: 70 },
{ id: 4, name: 'Webcam', sales: 65 },
],
activeUsers: 1428,
currency: 'USD',
};
const actions = {
addToCart: ({ state }: any, product: Product) => {
const existingItem = state.cart.find((item:any) => item.id === product.id);
if (existingItem) {
return {
cart: state.cart.map((item:any) =>
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
),
};
}
return { cart: [...state.cart, { ...product, quantity: 1 }] };
},
removeFromCart: ({state}: {state:ECommerceState}, productId: number) => ({
cart: state.cart.filter((item) => item.id !== productId),
}),
updateQuantity: ({state}: {state:ECommerceState}, { productId, quantity }: { productId: number; quantity: number }) => ({
cart: state.cart.map((item) =>
item.id === productId ? { ...item, quantity } : item
).filter(item => item.quantity > 0),
}),
checkout: ({state}: {state:ECommerceState}) => {
const total = state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
const newOrder: Order = {
id: crypto.randomUUID(),
items: state.cart,
total,
date: new Date(),
};
return {
cart: [],
orders: [newOrder, ...state.orders],
products: state.products.map(p => {
const cartItem = state.cart.find(item => item.id === p.id);
return cartItem ? { ...p, stock: p.stock - cartItem.quantity } : p;
}),
topSellers: state.topSellers.map(s => {
const cartItem = state.cart.find(item => item.id === s.id);
return cartItem ? { ...s, sales: s.sales + cartItem.quantity } : s;
}).sort((a, b) => b.sales - a.sales),
};
},
updateStock: ({state}: {state:ECommerceState}) => ({
products: state.products.map((p:any) => ({
...p,
stock: Math.max(0, p.stock + Math.floor(Math.random() * 10) - 5)
}))
}),
updateActiveUsers: ({state}: {state:ECommerceState}) => ({
activeUsers: state.activeUsers + Math.floor(Math.random() * 20) - 10,
}),
addRandomOrder: ({state}: {state:ECommerceState}) => {
const randomProduct = state.products[Math.floor(Math.random() * state.products.length)];
const quantity = Math.floor(Math.random() * 3) + 1;
const newOrder: Order = {
id: crypto.randomUUID(),
items: [{ ...randomProduct, quantity }],
total: randomProduct.price * quantity,
date: new Date(),
};
return {
orders: [newOrder, ...state.orders],
};
},
setCurrency: ({state}, newCurrency: string) => ({ currency: newCurrency }),
};
export const useStore = createStore(
{
state: initialState,
actions,
// Example artifact definition
artifacts: {
currencySymbol: {
factory: async ({ use }) => {
const currency = await use(({ select }) => select((s: ECommerceState) => s.currency));
switch (currency) {
case 'USD': return '$';
case 'EUR': return '€';
case 'GBP': return '£';
default: return currency;
}
},
},
// Another artifact example, might depend on other artifacts or state
exchangeRate: {
factory: async ({ resolve, use }) => {
const baseCurrency = await use(({ select }) => select((s: ECommerceState) => s.currency));
const targetCurrency = 'EUR'; // For demonstration
// In a real app, this would fetch from an API
await new Promise(r => setTimeout(r, 50)); // Simulate API delay
if (baseCurrency === 'USD' && targetCurrency === 'EUR') {
return 0.92; // 1 USD = 0.92 EUR
}
return 1.0;
},
},
}
},
{ enableMetrics: true } // Enables metrics for observability
);Using in Components
Consume your store's state and actions within your React components using the exported hook. The select function allows you to subscribe to specific parts of the state, ensuring that your components only re-render when the selected data changes.
// ui/App.tsx (Excerpt)
import { useEffect, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { useStore, Product, CartItem, Order, ECommerceState } from './store';
const ProductCatalog = () => {
const { select, actions, resolve } = useStore();
const products = select((state) => state.products); // Granular selection
const { instance: currencySymbol, ready: currencyReady } = resolve('currencySymbol'); // Reactive artifact resolution
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product: Product) => (
<Card key={product.id}>
<CardHeader>
<CardTitle>{product.name}</CardTitle>
</CardHeader>
<CardContent className="flex-grow">
<img src={product.image} alt={product.name} className="w-full h-40 object-cover rounded-lg mb-4" />
<div className="flex justify-between items-center">
<p className="text-lg font-semibold text-gray-700">
{currencyReady ? currencySymbol : ''}{product.price.toFixed(2)}
</p>
<p className="text-sm text-gray-500">{product.stock} in stock</p>
</div>
</CardContent>
<CardFooter>
<Button onClick={() => actions.addToCart(product)} className="w-full">Add to Cart</Button>
</CardFooter>
</Card>
))}
</div>
);
};
function App() {
const { actions, select } = useStore();
const currentCurrency = select((state) => state.currency);
useEffect(() => {
// Real-time simulations for the dashboard
const stockInterval = setInterval(() => actions.updateStock(), 2000);
const usersInterval = setInterval(() => actions.updateActiveUsers(), 3000);
const ordersInterval = setInterval(() => actions.addRandomOrder(), 5000);
return () => {
clearInterval(stockInterval);
clearInterval(usersInterval);
clearInterval(ordersInterval);
};
}, [actions]);
return (
<div className="bg-gray-50 text-gray-900 min-h-screen">
<header className="border-b">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
<h1 className="text-xl font-bold">E-Commerce Dashboard</h1>
<select
value={currentCurrency}
onChange={(e) => actions.setCurrency(e.target.value)}
className="p-2 border rounded-md"
>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
</select>
</div>
</header>
<main className="container mx-auto p-4 sm:p-6 lg:p-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<ProductCatalog />
{/* <LiveInventoryChart /> */}
</div>
<div className="space-y-6">
{/* <ShoppingCart /> */}
{/* <UserAnalytics /> */}
{/* <TopSellingProducts /> */}
{/* <RecentOrdersFeed /> */}
</div>
</div>
</main>
</div>
);
}
export default App;Handling Deletions
To remove a property from the state, use the Symbol.for("delete") symbol in your action’s return value. The store’s internal merge function will remove the specified key from the state.
Example
import { createStore } from '@asaidimu/react-store';
const deleteStore = createStore({
state: {
id: 'product-123',
name: 'Fancy Gadget',
details: {
color: 'blue',
weight: '1kg',
dimensions: { width: 10, height: 20 }
},
tags: ['electronics', 'new']
},
actions: {
removeDetails: (ctx) => ({ details: Symbol.for("delete") }),
removeDimensions: (ctx) => ({ details: { dimensions: Symbol.for("delete") } }),
removeTag: ({state}, tagToRemove: string) => ({
tags: state.tags.filter(tag => tag !== tagToRemove)
}),
clearAllExceptId: (ctx) => ({
name: Symbol.for("delete"),
details: Symbol.for("delete"),
tags: Symbol.for("delete")
})
},
});
async function runDeleteExample() {
const { select, actions } = deleteStore();
console.log("Initial state:", select(s => s));
// Initial state: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg', dimensions: { width: 10, height: 20 } }, tags: ['electronics', 'new'] }
await actions.removeDimensions();
console.log("After removing dimensions:", select(s => s));
// After removing dimensions: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg' }, tags: ['electronics', 'new'] }
await actions.removeDetails();
console.log("After removing details:", select(s => s));
// After removing details: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics', 'new'] }
await actions.removeTag('new');
console.log("After removing 'new' tag:", select(s => s));
// After removing 'new' tag: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics'] }
await actions.clearAllExceptId();
console.log("After clearing all except ID:", select(s => s));
// After clearing all except ID: { id: 'product-123' }
}
runDeleteExample();Persistence
Persist your store's state across browser sessions or synchronize it across multiple tabs using persistence adapters from @asaidimu/utils-persistence. You can choose between WebStoragePersistence (for localStorage or sessionStorage) and IndexedDBPersistence for more robust storage.
import { createStore } from '@asaidimu/react-store';
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
import React, { useEffect } from 'react';
interface LocalState { sessionCount: number; lastVisited: string; }
interface SessionState { tabSpecificData: string; }
interface UserProfileState { userId: string; preferences: { language: string; darkMode: boolean; }; }
// 1. Using WebStoragePersistence (localStorage by default)
// Data persists even if the browser tab is closed and reopened.
const localStorePersistence = new WebStoragePersistence<LocalState>('my-app-state-key');
const useLocalStore = createStore(
{
state: { sessionCount: 0, lastVisited: new Date().toISOString() },
actions: {
incrementSessionCount: ({state}) => ({ sessionCount: state.sessionCount + 1 }),
updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
},
},
{ persistence: localStorePersistence },
);
// 2. Using WebStoragePersistence (sessionStorage)
// Data only persists for the duration of the browser tab. Clears on tab close.
const sessionStoragePersistence = new WebStoragePersistence<SessionState>('my-session-state-key', true);
const useSessionStore = createStore(
{
state: { tabSpecificData: 'initial' },
actions: {
updateTabSpecificData: (_, newData: string) => ({ tabSpecificData: newData }),
},
},
{ persistence: sessionStoragePersistence },
);
// 3. Using IndexedDBPersistence
// Ideal for larger amounts of data, offers robust cross-tab synchronization.
const indexedDBPersistence = new IndexedDBPersistence<UserProfileState>('user-profile-data');
const useUserProfileStore = createStore(
{
state: { userId: '', preferences: { language: 'en', darkMode: false } },
actions: {
setUserId: (_, id: string) => ({ userId: id }),
toggleDarkMode: ({state}) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
},
},
{ persistence: indexedDBPersistence },
);
function AppWithPersistence() {
const { select: selectLocal, actions: actionsLocal, isReady: localReady } = useLocalStore();
const { select: selectProfile, actions: actionsProfile, isReady: profileReady } = useUserProfileStore();
const { select: selectSession, actions: actionsSession } = useSessionStore();
const sessionCount = selectLocal(s => s.sessionCount);
const darkMode = selectProfile(s => s.preferences.darkMode);
const tabData = selectSession(s => s.tabSpecificData);
useEffect(() => {
if (localReady) {
actionsLocal.incrementSessionCount();
actionsLocal.updateLastVisited();
}
if (profileReady && !selectProfile(s => s.userId)) {
actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
}
}, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]);
if (!localReady || !profileReady) {
return <div>Loading persisted data...</div>;
}
return (
<div>
<h3>Local Store (localStorage)</h3>
<p>Session Count: {sessionCount}</p>
<h3>Session Store (sessionStorage)</h3>
<p>Tab Specific Data: {tabData}</p>
<button onClick={() => actionsSession.updateTabSpecificData('updated-for-this-tab')}>
Update Tab Data
</button>
<h3>User Profile Store (IndexedDB)</h3>
<p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
<button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
</div>
);
}Middleware (Transform & Validate)
Middleware functions can intercept and modify or block state updates. The CHANGELOG.md indicates a breaking change, moving from generic middleware and blockingMiddleware to transform and validate properties in the StoreDefinition. These now receive an ActionContext with state and resolve capabilities.
transform: Functions that run after an action's core logic but before the state update is committed. They can modify theDeepPartial<TState>that will be merged into the state.validate: Functions that run before the state update is committed. If a validator returnsfalse(or aPromise<false>), the state update is entirely cancelled.
import { createStore } from '@asaidimu/react-store';
import React from 'react';
interface CartState {
items: Array<{ id: string; name: string; quantity: number; price: number }>;
total: number;
}
const useCartStore = createStore<CartState, any, any>({ // TArtifactsMap and TActions are inferred
state: { items: [], total: 0 },
actions: {
addItem: ({state}, item: { id: string; name: string; price: number }) => {
const existingItem = state.items.find(i => i.id === item.id);
if (existingItem) {
return {
items: state.items.map(i =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
),
};
}
return {
items: [...state.items, { ...item, quantity: 1 }],
};
},
updateQuantity: ({state}, id: string, quantity: number) => ({
items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
}),
},
transform: {
// Calculates total based on updated items before state merge
calculateTotal: async ({ state, resolve }, update) => {
if (update.items) {
const newItems = update.items as CartState['items'];
const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
return { ...update, total: newTotal };
}
return update;
},
},
validate: {
// Blocks update if any item quantity is negative
validateItemQuantity: async ({ state, resolve }, update) => {
if (update.items) {
for (const item of update.items as CartState['items']) {
if (item.quantity < 0) {
console.warn('Blocked by validator: Item quantity cannot be negative.');
return false; // Blocks the update
}
}
}
return true; // Allows the update
},
},
});
function CartComponent() {
const { select, actions } = useCartStore();
const items = select(s => s.items);
const total = select(s => s.total);
return (
<div>
<h2>Shopping Cart</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name} ({item.quantity}) - ${item.price} each
<button onClick={() => actions.updateQuantity(item.id, item.quantity - 1)}>-</button>
<button onClick={() => actions.updateQuantity(item.id, item.quantity + 1)}>+</button>
</li>
))}
</ul>
<p>Total: ${total.toFixed(2)}</p>
<button onClick={() => actions.addItem({ id: 'apple', name: 'Apple', price: 1.50 })}>Add Apple</button>
<button onClick={() => actions.updateQuantity('apple', -1)}>Set Apple Quantity to -1 (Blocked)</button>
</div>
);
}Artifact Management
The store supports defining and reactively resolving "artifacts," which can be any asynchronous resource, service, or derived value. Artifacts are defined in the artifacts property of the StoreDefinition and resolved using ctx.resolve() within actions or the resolve() hook in components. They can depend on other artifacts or on the store's reactive state.
import { createStore } from '@asaidimu/react-store';
import { ArtifactScopes } from '@asaidimu/utils-artifacts';
import React, { useEffect } from 'react';
interface AppState {
userId: string | null;
settingsLoaded: boolean;
theme: string;
}
// Assume this is an API service or similar
const mockApiService = {
fetchUserSettings: async (userId: string) => {
await new Promise(r => setTimeout(r, 200)); // Simulate API delay
if (userId === 'user-123') {
return { theme: 'dark', notifications: true };
}
return { theme: 'light', notifications: false };
},
};
const useArtifactStore = createStore<AppState, any, any>({
state: { userId: null, settingsLoaded: false, theme: 'light' },
actions: {
setUserId: (_, id: string) => ({ userId: id, settingsLoaded: false }),
loadUserSettings: async ({ state, resolve }) => {
if (!state.userId) return;
const { instance: userSettings } = await resolve('userSettings');
if (userSettings) {
return {
settingsLoaded: true,
theme: userSettings.theme,
};
}
return {};
},
toggleTheme: ({state}) => ({ theme: state.theme === 'light' ? 'dark' : 'light' }),
},
artifacts: {
// A singleton artifact that fetches user settings based on the current userId in state
userSettings: {
scope: ArtifactScopes.Singleton, // Ensure only one instance is created globally
factory: async ({ use }) => {
const userId = await use(({ select }) => select((s: AppState) => s.userId));
if (userId) {
console.log(`Fetching settings for user: ${userId}`);
return mockApiService.fetchUserSettings(userId);
}
return null;
},
lazy: true, // Only create/resolve when first requested
},
// An artifact that provides a simple logger instance
logger: {
factory: async () => console,
scope: ArtifactScopes.Singleton,
},
},
});
function ArtifactConsumer() {
const { actions, select, resolve, isReady } = useArtifactStore();
const userId = select(s => s.userId);
const theme = select(s => s.theme);
const settingsLoaded = select(s => s.settingsLoaded);
// Reactively resolve the userSettings artifact in the component
const { instance: userSettingsArtifact, ready: userSettingsReady } = resolve('userSettings');
const { instance: logger } = resolve('logger');
useEffect(() => {
// Simulate setting a user ID after initial load
if (isReady && !userId) {
actions.setUserId('user-123');
}
}, [isReady, userId, actions]);
useEffect(() => {
// Automatically load settings when userId is available and settings not loaded
if (userId && !settingsLoaded) {
actions.loadUserSettings();
}
}, [userId, settingsLoaded, actions]);
useEffect(() => {
if (logger && userSettingsArtifact) {
logger.log("User settings artifact updated:", userSettingsArtifact);
}
}, [logger, userSettingsArtifact]);
if (!isReady) {
return <div>Loading store...</div>;
}
return (
<div>
<h2>Artifact Management Example</h2>
<p>Current User ID: {userId || 'Not set'}</p>
<p>Settings Loaded: {settingsLoaded ? 'Yes' : 'No'}</p>
<p>Current Theme: {theme}</p>
{userSettingsReady && userSettingsArtifact && (
<p>Artifact (userSettings) Resolved Theme: {userSettingsArtifact.theme}</p>
)}
<button onClick={() => actions.setUserId(userId === 'user-123' ? 'user-456' : 'user-123')}>
Toggle User ID
</button>
<button onClick={() => actions.toggleTheme()}>Toggle App Theme</button>
</div>
);
}Observability
Enable metrics and debugging via the observer and actionTracker objects. The enableMetrics option in createStore is crucial for activating these features.
import { createStore } from '@asaidimu/react-store';
import React from 'react';
const useObservedStore = createStore(
{
state: { task: '', completed: false, count: 0 },
actions: {
addTask: (_, taskName: string) => ({ task: taskName, completed: false }),
completeTask: (_) => ({ completed: true }),
increment: ({state}) => ({ count: state.count + 1 }),
longRunningAction: async () => {
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async work
return { count: 100 };
},
},
},
{
enableMetrics: true, // Crucial for enabling the 'observer' and 'actionTracker' objects
enableConsoleLogging: true, // Log events directly to browser console
logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
performanceThresholds: {
updateTime: 50, // Warn if updates take longer than 50ms
middlewareTime: 20 // Warn if middleware takes longer than 20ms
},
maxEvents: 500, // Max number of events to keep in history
maxStateHistory: 50, // Max number of state snapshots for time travel
debounceTime: 0, // Actions execute immediately by default
},
);
function DebugPanel() {
const { actions, observer, actionTracker, select, state: getStateSnapshot } = useObservedStore();
const count = select(s => s.count);
// Access performance metrics
const metrics = observer?.getPerformanceMetrics();
// Access state history for time travel (if maxStateHistory > 0)
const timeTravel = observer?.createTimeTravel();
// Access action execution history
const actionHistory = actionTracker?.getExecutions() || []; // actionTracker is only available if enableMetrics is true
return (
<div>
<h2>Debug Panel</h2>
{observer && ( // Check if observer is enabled
<>
<h3>Performance Metrics</h3>
<p>Update Count: {metrics?.updateCount}</p>
<p>Avg Update Time: {metrics?.averageUpdateTime?.toFixed(2)}ms</p>
<p>Largest Update Size (paths): {metrics?.largestUpdateSize}</p>
<h3>Time Travel</h3>
<p>Current Count: {count} (via select)</p>
<button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
<button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
<p>State History: {timeTravel?.getHistoryLength()}</p>
<p>Current Snapshot (non-reactive): {JSON.stringify(getStateSnapshot())}</p>
<h3>Action History</h3>
<ul>
{actionHistory.slice(0, 5).map(exec => (
<li key={exec.id}>
<strong>{exec.name}</strong> ({exec.status}) - {exec.duration.toFixed(2)}ms
</li>
))}
</ul>
</>
)}
<button onClick={() => actions.addTask('Learn React Store')}>Add Task</button>
<button onClick={() => actions.completeTask()}>Complete Task</button>
<button onClick={() => actions.increment()}>Increment</button>
<button onClick={() => actions.longRunningAction()}>Long Action</button>
</div>
);
}Remote Observability
Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring. This functionality typically resides in the @asaidimu/utils-store ecosystem.
import { createStore } from '@asaidimu/react-store';
// Assuming useRemoteObservability is provided by @asaidimu/utils-store or a wrapper
// import { useRemoteObservability } from '@asaidimu/utils-store';
import React, { useEffect } from 'react';
const useRemoteStore = createStore(
{
state: { apiCallsMade: 0, lastApiError: null },
actions: {
simulateApiCall: async ({state}) => {
if (Math.random() < 0.1) {
throw new Error('API request failed');
}
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
},
handleApiError: (_, error: string) => ({ lastApiError: error })
},
},
{
enableMetrics: true, // Required for RemoteObservability
enableConsoleLogging: false,
}
);
function MonitoringIntegration() {
const { store, observer } = useRemoteStore();
// Placeholder for actual useRemoteObservability hook
// const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
// serviceName: 'my-react-app',
// environment: 'development',
// instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
// collectCategories: {
// performance: true,
// errors: true,
// stateChanges: true,
// middleware: true,
// },
// reportingInterval: 10000, // Send metrics every 10 seconds
// batchSize: 10, // Send after 10 metrics or interval, whichever comes first
// immediateReporting: false, // Don't send immediately after each metric
// });
useEffect(() => {
// In a real implementation, you would use the `remote` object
// to add destinations and configure reporting.
// Example:
// addOpenTelemetryDestination({ endpoint: 'http://localhost:4318', apiKey: 'your-otel-api-key' });
// addPrometheusDestination({ pushgatewayUrl: 'http://localhost:9091', jobName: 'react-store-metrics' });
// addGrafanaCloudDestination({ url: 'https://loki-prod-us-central1.grafana.net', apiKey: 'your-grafana-cloud-api-key' });
const interval = setInterval(() => {
observer?.reportCurrentMetrics(); // Manually trigger a report if needed
}, 5000); // Report every 5 seconds
return () => clearInterval(interval);
}, [observer]); // Removed placeholder dependencies for actual usage
return null; // This component doesn't render anything visually
}
// In your App component, you would use it like:
// function MyApp() {
// return (
// <>
// <MonitoringIntegration />
// <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
// Simulate API Call
// </button>
// </>
// );
// }Event System
The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects. This is done via store.onStoreEvent().
import { createStore } from '@asaidimu/react-store';
import React, { useEffect } from 'react';
const useEventStore = createStore(
{
state: { data: 'initial', processedCount: 0 },
actions: {
processData: ({state}, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
triggerError: () => { throw new Error("Action failed intentionally"); }
},
transform: { // Using the new middleware API
myLoggingMiddleware: async ({state}, update) => {
console.log('Middleware processing:', update);
return update;
}
}
}
);
function EventMonitor() {
const { store, actions } = useEventStore();
const [eventLogs, setEventLogs] = React.useState<string[]>([]);
useEffect(() => {
const addLog = (message: string) => {
setEventLogs(prev => [`${new Date().toLocaleTimeString()}: ${message}`, ...prev].slice(0, 10));
};
// Subscribe to specific store events
const unsubscribeUpdateStart = store.onStoreEvent('update:start', (data) => {
addLog(`Update Started (timestamp: ${data.timestamp})`);
});
const unsubscribeUpdateComplete = store.onStoreEvent('update:complete', (data) => {
if (data.blocked) {
addLog(`Update BLOCKED by middleware or error. Error: ${data.error?.message || 'unknown'}`);
} else {
addLog(`Update Completed in ${data.duration?.toFixed(2)}ms. Paths changed: ${data.changedPaths?.join(', ')}`);
}
});
const unsubscribeMiddlewareStart = store.onStoreEvent('middleware:start', (data) => {
addLog(`Middleware '${data.name}' started (${data.type})`);
});
const unsubscribeMiddlewareError = store.onStoreEvent('middleware:error', (data) => {
addLog(`Middleware '${data.name}' encountered an error: ${data.error.message}`);
});
const unsubscribeTransactionStart = store.onStoreEvent('transaction:start', () => {
addLog(`Transaction Started`);
});
const unsubscribeTransactionError = store.onStoreEvent('transaction:error', (data) => {
addLog(`Transaction Failed: ${data.error.message}`);
});
const unsubscribePersistenceReady = store.onStoreEvent('persistence:ready', () => {
addLog(`Persistence is READY.`);
});
// Cleanup subscriptions on component unmount
return () => {
unsubscribeUpdateStart();
unsubscribeUpdateComplete();
unsubscribeMiddlewareStart();
unsubscribeMiddlewareError();
unsubscribeTransactionStart();
unsubscribeTransactionError();
unsubscribePersistenceReady();
};
}, [store]); // Re-subscribe if store instance changes (unlikely)
return (
<div>
<h3>Store Event Log</h3>
<button onClick={() => actions.processData('new data')}>Process Data</button>
<button onClick={() => actions.triggerError().catch(() => {})}>Trigger Action Error</button>
<button onClick={() => store.transaction(() => { actions.processData('transaction data'); throw new Error('Transaction error'); }).catch(() => {})}>
Simulate Transaction Error
</button>
<ul style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
{eventLogs.map((log, index) => <li key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log}</li>)}
</ul>
</div>
);
}Watching Action Loading States
The watch function returned by the useStore hook allows you to subscribe to the loading status of individual actions. This is particularly useful for displaying loading indicators for asynchronous operations.
import React from 'react';
import { createStore } from '@asaidimu/react-store';
interface DataState {
items: string[];
}
const useDataStore = createStore({
state: { items: [] },
actions: {
fetchItems: async ({state}) => {
// Simulate an API call
await new Promise(resolve => setTimeout(resolve, 2000));
return { items: ['Item A', 'Item B', 'Item C'] };
},
addItem: async ({state}, item: string) => {
// Simulate a quick add operation
await new Promise(resolve => setTimeout(resolve, 500));
return { items: [...state.items, item] };
},
},
});
function DataLoader() {
const { actions, select, watch } = useDataStore();
const items = select(s => s.items);
// Watch the loading state of specific actions
const isFetchingItems = watch('fetchItems');
const isAddingItem = watch('addItem');
return (
<div>
<h2>Data Loader</h2>
<button onClick={() => actions.fetchItems()} disabled={isFetchingItems}>
{isFetchingItems ? 'Fetching...' : 'Fetch Items'}
</button>
<button onClick={() => actions.addItem(`New Item ${items.length + 1}`)} disabled={isAddingItem}>
{isAddingItem ? 'Adding...' : 'Add Item'}
</button>
<h3>Items:</h3>
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}Advanced Hook Properties
The hook returned by createStore provides several properties for advanced usage and debugging, beyond the commonly used select, actions, and isReady:
import { useStore as useMyStore } from './store'; // Assuming this is your store definition
function MyAdvancedComponent() {
const {
select, // Function to select state parts (memoized, reactive)
actions, // Object containing your defined actions (debounced, promise-returning)
isReady, // Boolean indicating if persistence is ready
store, // Direct access to the ReactiveDataStore instance (from @asaidimu/utils-store)
observer, // StoreObserver instance (from @asaidimu/utils-store, if `enableMetrics` was true)
actionTracker, // Instance of ActionTracker for monitoring action executions (if `enableMetrics` was true)
state, // A getter function `() => TState` to get the entire current state object (reactive)
watch, // Function to watch the loading status of actions
resolve, // Function to reactively resolve an artifact (if artifacts are defined)
} = useMyStore(); // Assuming useMyStore is defined from createStore
// Example: Accessing the full state (use with caution for performance; `select` is preferred)
const fullCurrentState = state();
console.log("Full reactive state:", fullCurrentState);
// Example: Accessing observer methods (if enabled)
if (observer) {
console.log("Performance metrics:", observer.getPerformanceMetrics());
console.log("Recent state changes:", observer.getRecentChanges(3));
}
// Example: Accessing action history
if (actionTracker) { // actionTracker is only available if enableMetrics is true
console.log("Action executions:", actionTracker.getExecutions());
}
// Example: Watching a specific action's loading state
const isLoadingSomeAction = watch('checkout'); // Assuming 'checkout' is an action from the example store
console.log("Is 'checkout' action loading?", isLoadingSomeAction);
// Example: Resolving an artifact (from the example store)
const { instance: currencySymbol, ready: isCurrencySymbolReady } = resolve('currencySymbol');
if (isCurrencySymbolReady) {
console.log("Currency Symbol artifact is ready:", currencySymbol);
}
return (
<div>
{/* ... your component content ... */}
<p>Store Ready: {isReady ? 'Yes' : 'No'}</p>
</div>
);
}Project Architecture
@asaidimu/react-store is structured to provide a modular yet integrated state management solution. It separates core state logic into reusable utilities while offering a streamlined React-specific API.
Internal Structure
The core logic of this package integrates and extends external utility packages, with src/store.ts serving as the main entry point for the React hook.
src/execution.ts: DefinesActionExecutionandActionTrackerclasses for monitoring and maintaining a history of action dispatches, including their status and performance metrics.src/store.ts: Contains the maincreateStorefactory function. This module orchestrates the initialization of the coreReactiveDataStore,StoreObserver,ActionTracker, andArtifactContainer. It also binds actions, applies middleware (transform,validate), and sets up the React hook usinguseSyncExternalStorefor efficient component updates.src/types.ts: Defines all core TypeScript interfaces and types for the store's public API, includingActionContext,StoreDefinition,StoreHook, middleware signatures (Transformer,Validator), and artifact-related types.
Key External Dependencies
This library leverages the following @asaidimu packages for its core functionalities:
@asaidimu/utils-store: Provides the foundationalReactiveDataStorefor immutable state management, transactions, core event emission, and theStoreObserverinstance for deep insights into state changes.@asaidimu/utils-persistence: Offers various persistence adapters likeWebStoragePersistenceandIndexedDBPersistencefor saving and loading state, including cross-tab synchronization.@asaidimu/utils-artifacts: Provides theArtifactContainerand related types for defining and resolving asynchronous, reactive dependencies (artifacts) within the store.
Core Components
ReactiveDataStore(from@asaidimu/utils-store): The heart of the state management. It handles immutable state updates, middleware processing, transaction management, and emits detailed internal events about state changes.StoreObserver(from@asaidimu/utils-store): Built on top ofReactiveDataStore's event system, this component provides comprehensive debugging and monitoring features. This includes event history, state snapshots for time-travel, performance metrics, and utilities for logging or validation middleware.ActionTracker(src/execution.ts): A dedicated class for tracking the lifecycle and performance of individual action executions, capturing details like start/end times, duration, parameters, and outcomes (success/error).ArtifactContainer(from@asaidimu/utils-artifacts): Manages the registration and resolution of artifacts. It handles dependencies between artifacts and reacts to state changes for artifacts that depend on store state.createStoreHook (src/store.ts): The primary React-facing API. It instantiatesReactiveDataStore,StoreObserver,ActionTracker, andArtifactContainer. It wraps user-defined actions with debouncing and tracking, and provides theselectfunction (powered byuseSyncExternalStorefor efficient component updates), thewatchfunction for action loading states, and theresolvefunction for artifacts.- Persistence Adapters (from
@asaidimu/utils-persistence): Implement theSimplePersistenceinterface.WebStoragePersistence(forlocalStorage/sessionStorage) andIndexedDBPersistenceprovide concrete, ready-to-use storage solutions with cross-tab synchronization capabilities.
Data Flow
- Action Dispatch: A React component calls a bound action (e.g.,
actions.addItem()). - Action Debouncing: Actions are debounced by default (configurable), preventing rapid successive calls.
- Action Loading State Update: The store immediately updates the loading state for the dispatched action to
truevia an internalReactiveDataStore. - Action Execution Tracking: The
ActionTrackerrecords the action's details (name, parameters, start time). - State Update Request: The action's implementation (receiving
ActionContextwithstateandresolve) returns a partial state update or a promise resolving to one. - Transaction Context: If the action is wrapped within
store.transaction(), the current state is snapshotted to enable potential rollback. - Validator Middleware: The update first passes through any registered
validatemiddleware. If any validator returnsfalseor throws an error, the update is halted, and the state remains unchanged (and rolled back if in a transaction). - Transformer Middleware: If not blocked, the update then passes through
transformmiddleware. These functions can modify the partial update payload. - State Merging: The final, possibly transformed, update is immutably merged into the current state using
ReactiveDataStore's internal utility. - Change Detection:
ReactiveDataStoreperforms a deep diff to identify precisely which paths in the state have changed. - Persistence: If changes occurred, the new state is saved via the configured
SimplePersistenceadapter (e.g.,localStorage,IndexedDB). The system also subscribes to external changes from persistence for cross-tab synchronization. - Artifact Re-evaluation: If state changes affect an artifact that depends on that part of the state,
ArtifactContainermay re-evaluate and re-resolve that artifact. - Listener Notification:
React.useSyncExternalStoresubscribers (used byselectandresolve) whose selected paths or resolved artifacts have changed are notified, triggering efficient re-renders of only the relevant components. - Action Loading State Reset: Once the action completes (either successfully or with an error), the loading state for that action is reset to
false. - Observability Events: Throughout this entire flow,
ReactiveDataStoreemits fine-grained events (update:start,middleware:complete,transaction:error, etc.) whichStoreObservercaptures for debugging, metrics collection, and remote reporting.
Extension Points
- Custom Middleware: Easily add your own
TransformerorValidatorfunctions for custom logic (e.g., advanced logging, analytics, data transformation, or complex validation logic). - Custom Persistence Adapters: Implement the
SimplePersistence<T>interface (from@asaidimu/utils-persistence) to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store). - Custom Artifact Factories: Define factories for any external service, resource, or complex derived state, allowing for clear separation of concerns and reactive dependency injection.
- Remote Observability Destinations: Create new
RemoteDestinationimplementations (part of@asaidimu/utils-store) to send metrics and traces to any external observability platform not already supported by default.
Development & Contributing
We welcome contributions! Please follow the guidelines below.
Development Setup
- Clone the repository:
git clone https://github.com/asaidimu/node-react.git cd react-store - Install dependencies:
This project uses
bunas the package manager.bun install
Scripts
bun ci: Installs dependencies, typically used in CI/CD environments to ensure a clean install.bun test: Runs all unit tests usingVitestin interactive watch mode.bun test:ci: Runs all unit tests once, suitable for CI/CD pipelines.bun clean: Removes thedistdirectory, cleaning up previous build artifacts.bun prebuild: Pre-build step that cleans thedistdirectory and runs an internal package synchronization script (.sync-package.ts).bun build: Compiles the TypeScript source intodist/for CJS and ESM formats, generates type definitions, and minifies the code usingTerser.bun dev: Starts the e-commerce dashboard demonstration application usingVite.bun postbuild: Post-build step that copiesREADME.md,LICENSE.md, and the specializeddist.package.jsoninto thedistfolder, preparing the package for publishing.
Testing
Tests are written using Vitest and React Testing Library for component and hook testing.
To run tests:
bun test
# or to run in watch mode
bun test --watchContributing Guidelines
- Fork the repository and create your branch from
main. - Code Standards: Ensure your code adheres to existing coding styles (TypeScript, ESLint, Prettier are configured).
- Tests: Add unit and integration tests for new features or bug fixes. Ensure all tests pass (
bun test). - Commits: Follow Conventional Commits for commit messages. This project uses
semantic-releasefor automated versioning and changelog generation. - Pull Requests: Submit a pull request to the
mainbranch. Provide a clear description of your changes, referencing any relevant issues.
Issue Reporting
For bugs, feature requests, or questions, please open an issue on the GitHub Issues page.
Additional Information
Best Practices
- Granular Selectors: Always use
select((state) => state.path.to.value)instead ofselect((state) => state)to prevent unnecessary re-renders of components. The more specific your selector, the more optimized your component updates will be. - Action Design: Keep actions focused on a single responsibility. Use
asyncactions for asynchronous operations (e.g., API calls) and return partial updates or promises resolving to partial updates upon completion. Actions should describe what happened, not how. - Persistence:
- Use unique
storeIdorstorageKeyfor each distinct store to avoid data conflicts. - Always check the
isReadyflag for UI elements that depend on the initial state loaded from persistence, to prevent rendering incomplete data.
- Use unique
- Middleware: Leverage
transformandvalidatefor cross-cutting concerns like logging, analytics, data transformation, or complex validation logic that applies to multiple actions. They now receiveActionContext, allowing for advanced logic including artifact resolution. Symbol.for("delete"): Use this explicit symbol for property removal to maintain clarity and avoid accidental data mutations or unexpected behavior when merging partial updates.- Debounce Time: Adjust the
debounceTimeincreateStoreoptions for actions that might be called rapidly (e.g., search input, scroll events) to optimize performance. AdebounceTimeof0means actions execute immediately. - Artifacts: Use artifacts to manage external dependencies, services, or complex derived values that might change over time or have their own lifecycle. This promotes better separation of concerns and testability.
API Reference
createStore(definition, options)
The main entry point for creating a store hook.
// From src/types.ts
export interface StoreDefinition<
TState extends object,
TArtifactsMap extends ArtifactsMap<TState>,
TActions extends ActionMap<TState, TArtifactsMap>,
> {
state: TState;
actions: TActions;
artifacts?: TArtifactsMap; // Optional artifact definitions
transform?: Record<string, Transformer<TState, TArtifactsMap>>; // Optional transforming middleware
validate?: Record<string, Validator<TState, TArtifactsMap>>; // Optional blocking middleware
}
interface StoreOptions<T> extends ObserverOptions { // ObserverOptions from @asaidimu/utils-store
enableMetrics?: boolean; // Enable StoreObserver and ActionTracker features (default: false)
persistence?: SimplePersistence<T>; // Optional persistence adapter instance (from @asaidimu/utils-persistence)
debounceTime?: number; // Time in milliseconds to debounce actions (default: 0ms)
// Other ObserverOptions for logging, performanceThresholds, maxEvents, maxStateHistory are inherited
}
const useStoreHook = createStore(definition, options);Returns: A React hook (useStoreHook) which, when called in a component, returns an object with the following properties:
store: Direct access to the underlyingReactiveDataStoreinstance (from@asaidimu/utils-store). This provides low-level control and event subscription.observer: TheStoreObserverinstance (from@asaidimu/utils-store). Available only ifenableMetricsistrue. Provides debug, time-travel, and monitoring utilities.select: A memoized selector function(<S>(selector: (state: TState) => S) => S). Extracts specific state slices. Re-renders components only when selected data changes.actions: An object containing your defined actions, fully typed and bound to the store. These actions are debounced (ifdebounceTime> 0) and their loading states are tracked. Each action returns aPromise<TState>resolving to the new state after the action completes.actionTracker: An instance ofActionTracker(fromsrc/execution.ts). Available only ifenableMetricsistrue. Provides methods for monitoring the execution history of your actions.state: A getter function(() => TState)that returns the entire current state object. Use sparingly, as components relying on this will re-render on any state change.selectis generally preferred for performance.isReady: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.watch: A function(<K extends keyof TActions>(action: K) => boolean)to watch the loading status of individual actions. Returnstrueif the action is currently executing.resolve: A reactive artifact resolver(<K extends keyof TArtifactsMap>(key: K) => ResolvedArtifact<ArtifactValue<TArtifactsMap[K]>>). Ifartifactsare defined in the store, this hook returnsResolvedArtifact(containinginstanceandreadystatus) for a specific artifact, reactively updating if the artifact instance changes or becomes ready.
ReactiveDataStore (accessed via useStoreHook().store from @asaidimu/utils-store)
get(clone?: boolean): TState: Retrieves the current state. Passtrueto get a deep clone (recommended for mutations outside of actions).set(update: StateUpdater<TState>): Promise<void>: Updates the state with a partial object or a function returning a partial object.watch(path: string | string[], callback: (state: TState) => void): () => void: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.transaction<R>(operation: () => R | Promise<R>): Promise<R>: Executes a function as an atomic transaction. Rolls back all changes if an error occurs if the operation throws.use(middleware: StoreMiddleware<TState>): string: Adds a transforming middleware. Returns its ID. (Note: ThecreateStoreAPI usestransformandvalidatewhich internally map toReactiveDataStore.use).removeMiddleware(id: string): boolean: Removes a middleware by its ID.isReady(): boolean: Checks if the persistence layer has loaded its initial state.onStoreEvent(event: StoreEvent, listener: (data: any) => void): () => void: Subscribes to internal store events (e.g.,'update:complete','middleware:error','transaction:start').
StoreObserver (accessed via useStoreHook().observer from @asaidimu/utils-store)
Available only if enableMetrics is true in createStore options.
getEventHistory(): DebugEvent[]: Retrieves a history of all captured store events.getStateHistory(): TState[]: Returns a history of state snapshots, enabling time-travel debugging (ifmaxStateHistory> 0).getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: DeepPartial<TState>; to: DeepPartial<TState>; }>: Provides a simplified view of recent state changes.getPerformanceMetrics(): StoreMetrics: Returns an object containing performance statistics (e.g.,updateCount,averageUpdateTime).createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }: Returns controls for time-travel debugging.clearHistory(): void: Clears the event and state history.disconnect(): void: Cleans up all listeners and resources.
Persistence Adapters (from @asaidimu/utils-persistence)
All adapters implement SimplePersistence<T>:
set(id:string, state: T): boolean | Promise<boolean>: Persists data.get(id:string): T | null | Promise<T | null>: Retrieves data.subscribe(id:string, callback: (state:T) => void): () => void: Subscribes to external changes (e.g., from other tabs).clear(id:string): boolean | Promise<boolean>: Clears persisted data.
IndexedDBPersistence(storeId: string)
storeId: A unique identifier for the IndexedDB object store (e.g.,'user-data').
WebStoragePersistence(storageKey: string, session?: boolean)
storageKey: The key under which data is stored (e.g.,'app-config').session: Optional. Iftrue, usessessionStorage; otherwise, useslocalStorage(default:false).
Comparison with Other State Management Solutions
@asaidimu/react-store aims to be a comprehensive, all-in-one solution for React state management, integrating features that often require multiple libraries in other ecosystems. Here's a comparison to popular alternatives:
| Feature | @asaidimu/react-store | Redux | Zustand | MobX | Recoil |
| :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
| Dev Experience | Intuitive hook-based API with rich tooling. | Verbose setup with reducers and middleware. | Minimalist, hook-friendly API. | Reactive, class-based approach. | Atom-based, React-native feel. |
| Learning Curve | Moderate (artifacts, middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). |
| API Complexity | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
| Scalability | High (transactions, persistence, remote metrics, artifacts). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
| Extensibility | Excellent (middleware, custom persistence, observability, artifacts). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
| Performance | Optimized (selectors, reactive updates via useSyncExternalStore). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
| Bundle Size | Moderate (includes observability, persistence, remote observability framework). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
| Persistence | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
| Observability | Excellent (metrics, time-travel, event logging, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
| React Integration | Native (hooks, useSyncExternalStore). | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
Where @asaidimu/react-store Shines
- All-in-One: It aims to be a single solution for state management, persistence, observability, and artifact management, reducing the need for multiple external dependencies and their integration complexities.
- Flexibility: The robust middleware system, transaction support, and artifact management make it highly adaptable to complex business logic, asynchronous data flows, and dependency injection patterns.
- Modern React: It leverages
useSyncExternalStorefor direct integration with React's concurrency model, ensuring efficient and up-to-date component renders with minimal overhead.
Trade-Offs
- Bundle Size: While comprehensive, it naturally has a larger bundle size compared to minimalist alternatives like Zustand, as it includes a wider range of features out-of-the-box. Tree-shaking is applied, but the rich feature set contributes to the baseline.
- Learning Curve: The rich feature set and advanced concepts (middleware, transactions, observability, artifacts) might present a slightly steeper initial learning curve for developers new to advanced state management, though the API strives for simplicity and clear documentation.
Troubleshooting
- Components not re-rendering:
- Ensure you are using
selectwith a specific path (e.g.,select(s => s.user.name)) instead of the entire state object. - Verify that the data at the selected path is actually changing (reference equality matters for objects/arrays).
- Ensure you are using
- Persistence not loading/saving:
- Check if
isReadyistruebefore interacting with state dependent on persistence. - Ensure your `persis
- Check if
