Efficient react state manager.
npm install @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!
---
* Overview & Features
* Installation & Setup
* Usage Documentation
* Creating a Store
* Using in Components
* Handling Deletions
* Persistence
* Middleware (Transform & Validate)
* Artifact Management
* Observability
* Remote Observability
* Event System
* Watching Action Loading States
* Advanced Hook Properties
* Project Architecture
* Development & Contributing
* Additional Information
* Best Practices
* API Reference
* Comparison with Other State Management Solutions
* Troubleshooting
* FAQ
* Changelog
* License
* Acknowledgments
---
@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.
* 📊 Reactive State Management: Automatically tracks dependencies to optimize component renders and ensure efficient updates using React's useSyncExternalStore hook.
* 🛡️ Type-Safe: Developed entirely in TypeScript, providing strict type checking for state, actions, artifacts, and middleware, minimizing runtime errors.
* ⚙️ Middleware Pipeline: Implement custom logic to transform or validate state changes before they are applied, with full access to the ActionContext including current state and artifact resolution.
* 💾 Built-in Persistence: Seamlessly integrate with web storage mechanisms like IndexedDB and WebStorage (localStorage/sessionStorage) via @asaidimu/utils-persistence, 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 StoreObserver instance.
* 🚀 Artifact Management: Define and reactively resolve asynchronous resources, services, or derived data using @asaidimu/utils-artifacts, enabling advanced dependency injection patterns and lazy loading of complex logic.
* ⚡ Performance Optimized: Features intelligent selector caching and debounced actions with configurable immediate execution (debounceTime: 0) to prevent rapid successive calls and ensure smooth application performance.
* ⏱️ Action Loading States: Track the real-time loading status of individual actions via the watch function, providing immediate feedback on asynchronous operations in the UI.
* ⚛️ 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, providing a clear and type-safe way to manage deletions.
* Node.js (v18 or higher recommended)
* React (v19 or higher recommended)
* A package manager like bun, npm, or yarn. This project explicitly uses bun.
To add @asaidimu/react-store to your project, run one of the following commands:
``bash`
bun add @asaidimu/react-storeor
npm install @asaidimu/react-storeor
yarn add @asaidimu/react-store
No global configuration is required. All options are passed during store creation via the createStore function. Configuration includes enabling metrics, persistence, and performance thresholds.
You can verify the installation by importing createStore and setting up a basic store:
`typescript
import { createStore } from '@asaidimu/react-store';
interface MyState {
value: string;
count: number;
}
const useMyStore = createStore
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 (
Value: {currentValue}
Count: {currentCount}
// 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.
`
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.
`tsx
// 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
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
);
`
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.
`tsx
// 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';
// Assuming Card, CardHeader, CardTitle, CardContent, CardFooter, Button are defined elsewhere
const Card = ({ children }: { children: React.ReactNode }) =>
}>{children}
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 (
{currencyReady ? currencySymbol : ''}{product.price.toFixed(2)}
{product.stock} in stock
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 (
export default App;
`
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
`typescript
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' }
}
// In a real application, you would call runDeleteExample() in a component's useEffect or similar.
// runDeleteExample();
`
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.
`tsx
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
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
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
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
return (
Session Count: {sessionCount}
Tab Specific Data: {tabData}
Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}
$3
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 the DeepPartial that will be merged into the state.
validate: Functions that run before* the state update is committed. If a validator returns false (or a Promise), the state update is entirely cancelled.`typescript
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({ // 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 (
Shopping Cart
{items.map(item => (
{item.name} ({item.quantity}) - ${item.price} each
))}
Total: ${total.toFixed(2)}
);
}
// Render CartComponent in your React app.
`$3
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.`tsx
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({
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
Loading store...;
} return (
Artifact Management Example
Current User ID: {userId || 'Not set'}
Settings Loaded: {settingsLoaded ? 'Yes' : 'No'}
Current Theme: {theme}
{userSettingsReady && userSettingsArtifact && (
Artifact (userSettings) Resolved Theme: {userSettingsArtifact.theme}
)}
);
}
// Render ArtifactConsumer in your React app.
`$3
Enable metrics and debugging via the
observer and actionTracker objects. The enableMetrics option in createStore is crucial for activating these features.`tsx
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 (
Debug Panel
{observer && ( // Check if observer is enabled
<>
Performance Metrics
Update Count: {metrics?.updateCount}
Avg Update Time: {metrics?.averageUpdateTime?.toFixed(2)}ms
Largest Update Size (paths): {metrics?.largestUpdateSize}
Time Travel
Current Count: {count} (via select)
State History: {timeTravel?.getHistoryLength()}
Current Snapshot (non-reactive): {JSON.stringify(getStateSnapshot())}
Action History
{actionHistory.slice(0, 5).map(exec => (
{exec.name} ({exec.status}) - {exec.duration.toFixed(2)}ms
))}
>
)}
);
}
// Render DebugPanel in your React app.
`$3
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.`tsx
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 (
// <>
//
//
// >
// );
// }
`$3
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().`typescript
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([]);
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 (
Store Event Log
{eventLogs.map((log, index) => - {log}
)}
);
}
// Render EventMonitor in your React app.
`$3
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.`tsx
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 (
Data Loader
Items:
{items.map((item, index) => (
- {item}
))}
);
}
// Render DataLoader in your React app.
`$3
The hook returned by
createStore provides several properties for advanced usage and debugging, beyond the commonly used select, actions, and isReady:`tsx
import { useStore as useMyStore } from './store'; // Assuming this is your store definitionfunction 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 (
{/ ... your component content ... /}
Store Ready: {isReady ? 'Yes' : 'No'}
);
}
`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.$3
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: Defines ActionExecution and ActionTracker classes for monitoring and maintaining a history of action dispatches, including their status and performance metrics.
* src/store.ts: Contains the main createStore factory function. This module orchestrates the initialization of the core ReactiveDataStore, StoreObserver, ActionTracker, and ArtifactContainer. It also binds actions, applies middleware (transform, validate), and sets up the React hook using useSyncExternalStore for efficient component updates.
* src/types.ts: Defines all core TypeScript interfaces and types for the store's public API, including ActionContext, StoreDefinition, StoreHook, middleware signatures (Transformer, Validator), and artifact-related types.$3
This library leverages the following
@asaidimu packages for its core functionalities:*
@asaidimu/utils-store: Provides the foundational ReactiveDataStore for immutable state management, transactions, core event emission, and the StoreObserver instance for deep insights into state changes.
* @asaidimu/utils-persistence: Offers various persistence adapters like WebStoragePersistence and IndexedDBPersistence for saving and loading state, including cross-tab synchronization.
* @asaidimu/utils-artifacts: Provides the ArtifactContainer and related types for defining and resolving asynchronous, reactive dependencies (artifacts) within the store.$3
*
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 of ReactiveDataStore'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.
* createStore Hook (src/store.ts): The primary React-facing API. It instantiates ReactiveDataStore, StoreObserver, ActionTracker, and ArtifactContainer. It wraps user-defined actions with debouncing and tracking, and provides the select function (powered by useSyncExternalStore for efficient component updates), the watch function for action loading states, and the resolve function for artifacts.
* Persistence Adapters (from @asaidimu/utils-persistence): Implement the SimplePersistence interface. WebStoragePersistence (for localStorage/sessionStorage) and IndexedDBPersistence provide concrete, ready-to-use storage solutions with cross-tab synchronization capabilities.$3
1. Action Dispatch: A React component calls a bound action (e.g.,
actions.addItem()).
2. Action Debouncing: Actions are debounced by default (configurable via debounceTime in StoreOptions), preventing rapid successive calls.
3. Action Loading State Update: The store immediately updates the loading state for the dispatched action to true via an internal ReactiveDataStore.
4. Action Execution Tracking: The ActionTracker records the action's details (name, parameters, start time) if enableMetrics is true.
5. State Update Request: The action's implementation (receiving ActionContext with state and resolve) returns a partial state update or a promise resolving to one.
6. Transaction Context: If the action is wrapped within store.transaction(), the current state is snapshotted to enable potential rollback.
7. Validator Middleware: The update first passes through any registered validate middleware. If any validator returns false or throws an error, the update is halted, and the state remains unchanged (and rolled back if in a transaction).
8. Transformer Middleware: If not blocked, the update then passes through transform middleware. These functions can modify the partial update payload.
9. State Merging: The final, possibly transformed, update is immutably merged into the current state using ReactiveDataStore's internal utility.
10. Change Detection: ReactiveDataStore performs a deep diff to identify precisely which paths in the state have changed.
11. Persistence: If changes occurred, the new state is saved via the configured SimplePersistence adapter (e.g., localStorage, IndexedDB). The system also subscribes to external changes from persistence for cross-tab synchronization.
12. Artifact Re-evaluation: If state changes affect an artifact that depends on that part of the state, ArtifactContainer may re-evaluate and re-resolve that artifact.
13. Listener Notification: React.useSyncExternalStore subscribers (used by select and resolve) whose selected paths or resolved artifacts have changed are notified, triggering efficient re-renders of only the relevant components.
14. Action Loading State Reset: Once the action completes (either successfully or with an error), the loading state for that action is reset to false.
15. Observability Events: Throughout this entire flow, ReactiveDataStore emits fine-grained events (update:start, middleware:complete, transaction:error, etc.) which StoreObserver captures for debugging, metrics collection, and remote reporting.$3
* Custom Middleware: Easily add your own
Transformer or Validator functions for custom logic (e.g., advanced logging, analytics, data transformation, or complex validation logic).
* Custom Persistence Adapters: Implement the SimplePersistence 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 RemoteDestination implementations (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.
$3
1. Clone the repository:
`bash
git clone https://github.com/asaidimu/node-react.git
cd react-store
`
2. Install dependencies:
This project uses bun as the package manager.
`bash
bun install
`$3
*
bun ci: Installs dependencies, typically used in CI/CD environments to ensure a clean install.
* bun test:ci: Runs all unit tests once, suitable for CI/CD pipelines.
* bun test:watch: Runs all unit tests using Vitest in interactive watch mode.
* bun test: Runs all unit tests once using Vitest.
* bun clean: Removes the dist directory, cleaning up previous build artifacts.
* bun prebuild: Pre-build step that cleans the dist directory and runs an internal package synchronization script (.sync-package.ts).
* bun build: Compiles the TypeScript source into dist/ for CJS and ESM formats, generates type definitions, and minifies the code using Terser.
* bun dev: Starts the e-commerce dashboard demonstration application using Vite.
* bun postbuild: Post-build step that copies README.md, LICENSE.md, and the specialized dist.package.json into the dist folder, preparing the package for publishing.$3
Tests are written using
Vitest and React Testing Library for component and hook testing.To run tests:
`bash
bun test
or to run in watch mode
bun test:watch
`$3
1. Fork the repository and create your branch from
main.
2. Code Standards: Ensure your code adheres to existing coding styles (TypeScript, ESLint, Prettier are configured).
3. Tests: Add unit and integration tests for new features or bug fixes. Ensure all tests pass (bun test).
4. Commits: Follow Conventional Commits for commit messages. This project uses semantic-release for automated versioning and changelog generation.
5. Pull Requests: Submit a pull request to the main branch. Provide a clear description of your changes, referencing any relevant issues.$3
For bugs, feature requests, or questions, please open an issue on the GitHub Issues page.
Additional Information
$3
1. Granular Selectors: Always use
select((state) => state.path.to.value) instead of select((state) => state) to prevent unnecessary re-renders of components. The more specific your selector, the more optimized your component updates will be.
2. Action Design: Keep actions focused on a single responsibility. Use async actions 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.
3. Persistence:
* Use unique storeId or storageKey for each distinct store to avoid data conflicts.
* Always check the isReady flag for UI elements that depend on the initial state loaded from persistence, to prevent rendering incomplete data.
4. Middleware: Leverage transform and validate for cross-cutting concerns like logging, analytics, data transformation, or complex validation logic that applies to multiple actions. They now receive ActionContext, allowing for advanced logic including artifact resolution.
5. 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.
6. Debounce Time: Adjust the debounceTime in createStore options for actions that might be called rapidly (e.g., search input, scroll events) to optimize performance. A debounceTime of 0 means actions execute immediately.
7. 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.$3
####
createStore(definition, options)The main entry point for creating a store hook.
`typescript
// From src/types.ts
export interface StoreDefinition<
TState extends object,
TArtifactsMap extends ArtifactsMap,
TActions extends ActionMap,
> {
state: TState;
actions: TActions;
artifacts?: TArtifactsMap; // Optional artifact definitions
transform?: Record>; // Optional transforming middleware
validate?: Record>; // Optional blocking middleware
}interface StoreOptions extends ObserverOptions { // ObserverOptions from @asaidimu/utils-store
enableMetrics?: boolean; // Enable StoreObserver and ActionTracker features (default: false)
persistence?: SimplePersistence; // 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 underlying ReactiveDataStore instance (from @asaidimu/utils-store). This provides low-level control and event subscription.
* observer: The StoreObserver instance (from @asaidimu/utils-store). Available only if enableMetrics is true. Provides debug, time-travel, and monitoring utilities.
* select: A memoized selector function (. 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 (if debounceTime > 0) and their loading states are tracked. Each action returns a Promise resolving to the new state after the action completes.
* actionTracker: An instance of ActionTracker (from src/execution.ts). Available only if enableMetrics is true. 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. select is 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 ( to watch the loading status of individual actions. Returns true if the action is currently executing.
* resolve: A reactive artifact resolver (. If artifacts are defined in the store, this hook returns ResolvedArtifact (containing instance and ready status) 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. Pass true to get a deep clone (recommended for mutations outside of actions).
* set(update: StateUpdater: 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: Executes a function as an atomic transaction. Rolls back all changes if an error occurs if the operation throws.
* use(middleware: StoreMiddleware: Adds a transforming middleware. Returns its ID. (Note: The createStore API uses transform and validate which internally map to ReactiveDataStore.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 (if maxStateHistory > 0).
* getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: DeepPartial: 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: 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:*
set(id:string, state: T): boolean | Promise: Persists data.
* get(id:string): T | null | Promise: Retrieves data.
* subscribe(id:string, callback: (state:T) => void): () => void: Subscribes to external changes (e.g., from other tabs).
* clear(id:string): boolean | Promise: 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. If true, uses sessionStorage; otherwise, uses localStorage (default: false).$3
@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).