@asaidimu/react-store v2.0.0
@asaidimu/react-store
A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware 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 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.
Key Features
- π Reactive State Management: Automatically tracks dependencies to optimize component renders and ensure efficient updates.
- π‘οΈ Type-Safe: Built with TypeScript from the ground up, providing strict type checking and a safer development experience.
- βοΈ Middleware Pipeline: Implement custom logic to transform, validate, or log state changes before they are applied. Supports both transforming and blocking middleware.
- π¦ Transaction Support: Group multiple state updates into a single atomic operation, with automatic rollback if any part of the transaction fails.
- πΎ Built-in Persistence: Seamlessly integrate with web storage mechanisms like
IndexedDB
andWebStorage
(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.
- π Remote Observability: Extend monitoring by sending collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud.
- β‘ Performance Optimized: Features intelligent selector caching and debounced actions to prevent rapid successive calls and ensure smooth application performance.
- βοΈ React 18+ 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 (v18 or higher recommended)
- A package manager like
bun
,npm
, oryarn
.
Installation Steps
To add @asaidimu/react-store
to your project, run one of the following commands:
bun install @asaidimu/react-store
# or
npm install @asaidimu/react-store
# or
yarn add @asaidimu/react-store
Configuration
No global configuration is required. All options are passed during store creation.
Verification
You can verify the installation by importing createStore
and setting up a basic store:
import { createStore } from '@asaidimu/react-store';
const myStore = createStore({
state: { value: 'hello' },
actions: {
setValue: (state, newValue: string) => ({ value: newValue }),
},
});
console.log(myStore().select(s => s.value)); // Should output 'hello'
If no errors are thrown, the package is correctly installed.
Usage Documentation
Creating a Store
Define your application state and actions, then create a store using createStore
.
// src/stores/myStore.ts
import { createStore } from '@asaidimu/react-store';
interface AppState {
count: number;
user: { name: string; loggedIn: boolean; email?: string };
settings: { theme: 'light' | 'dark'; notifications: boolean };
}
const initialState: AppState = {
count: 0,
user: { name: 'Guest', loggedIn: false },
settings: { theme: 'light', notifications: true },
};
const myStore = createStore({
state: initialState,
actions: {
increment: (state, amount: number) => ({ count: state.count + amount }),
login: async (state, username: string, email: string) => {
// Simulate an asynchronous API call
await new Promise(resolve => setTimeout(resolve, 500));
return { user: { name: username, loggedIn: true, email } };
},
toggleTheme: (state) => ({
settings: { theme: state.settings.theme === 'light' ? 'dark' : 'light' },
}),
},
}, {
debounceTime: 100, // Debounce actions by 100ms
enableConsoleLogging: true, // Enable console logs for store events
logEvents: { updates: true, transactions: true, middleware: true },
performanceThresholds: { updateTime: 20, middlewareTime: 5 }, // Warn for slow operations
});
// Export the hook for use in components
export const useMyStore = myStore;
Using in Components
Consume your store's state and actions within your React components using the exported hook.
// src/components/MyComponent.tsx
import React from 'react';
import { useMyStore } from '../stores/myStore';
function MyComponent() {
const { select, actions, isReady } = useMyStore();
// Select specific parts of the state for granular re-renders
const count = select((state) => state.count);
const userName = select((state) => state.user.name);
const theme = select((state) => state.settings.theme);
// isReady indicates if persistence has loaded initial state
if (!isReady) {
return <div>Loading store data...</div>;
}
return (
<div style={{ background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333' }}>
<h1>Welcome, {userName}!</h1>
<p>Current Count: {count}</p>
<button onClick={() => actions.increment(1)}>Increment</button>
<button onClick={() => actions.increment(5)}>Increment by 5 (debounced)</button>
<button onClick={() => actions.login('Alice', 'alice@example.com')}>Login as Alice</button>
<button onClick={() => actions.toggleTheme()}>Toggle Theme</button>
</div>
);
}
export default MyComponent;
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: (state) => ({ details: Symbol.for("delete") }),
removeDimensions: (state) => ({ details: { dimensions: Symbol.for("delete") } }),
removeTag: (state, tagToRemove: string) => ({
tags: state.tags.filter(tag => tag !== tagToRemove)
}),
clearAllExceptId: (state) => ({
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.
import { createStore, WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/react-store';
// 1. Using WebStoragePersistence (localStorage by default)
// Data persists even if the browser tab is closed and reopened.
const localStorePersistence = new WebStoragePersistence('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('my-session-state-key', true);
const useSessionStore = createStore(
{
state: { tabSpecificData: 'initial' },
actions: {
updateTabSpecificData: (state, newData: string) => ({ tabSpecificData: newData }),
},
},
{ persistence: sessionStoragePersistence },
);
// 3. Using IndexedDBPersistence
// Ideal for larger amounts of data, offers robust cross-tab synchronization.
const indexedDBPersistence = new IndexedDBPersistence('user-profile-data');
const useUserProfileStore = createStore(
{
state: { userId: '', preferences: { language: 'en', darkMode: false } },
actions: {
setUserId: (state, 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 sessionCount = selectLocal(s => s.sessionCount);
const darkMode = selectProfile(s => s.preferences.darkMode);
React.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]);
if (!localReady || !profileReady) {
return <div>Loading persisted data...</div>;
}
return (
<div>
<h3>Local Store</h3>
<p>Session Count: {sessionCount}</p>
<h3>User Profile Store (IndexedDB)</h3>
<p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
<button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
</div>
);
}
Middleware
Middleware functions can intercept and modify or block state updates. They are executed in the order they are added.
import { createStore, Middleware, BlockingMiddleware } from '@asaidimu/react-store';
interface CartState {
items: Array<{ id: string; name: string; quantity: number; price: number }>;
total: number;
}
const calculateTotalMiddleware: Middleware<CartState> = (state, 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;
};
const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) => {
if (update.items) {
for (const item of update.items as CartState['items']) {
if (item.quantity < 0) {
console.warn('Blocked: Item quantity cannot be negative.');
return false; // Blocks the update
}
}
}
return true; // Allows the update
};
const useCartStore = createStore({
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)),
}),
},
middleware: { calculateTotal: calculateTotalMiddleware },
blockingMiddleware: { validateItem: validateItemMiddleware },
});
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>
);
}
Observability
Enable metrics and debugging via the store
and observer
objects:
import { createStore } from '@asaidimu/react-store';
const useObservedStore = createStore(
{
state: { task: '', completed: false },
actions: {
addTask: (state, taskName: string) => ({ task: taskName, completed: false }),
completeTask: (state) => ({ completed: true }),
},
},
{
enableMetrics: true, // Crucial for enabling the 'observer' object
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: 300, // Debounce actions by 300ms to prevent rapid calls
},
);
function DebugPanel() {
const { actions, observer, actionTracker } = useObservedStore();
// Access performance metrics
const metrics = observer?.getPerformanceMetrics();
// Access state history for time travel
const timeTravel = observer?.createTimeTravel();
// Access action execution history
const actionHistory = actionTracker.getExecutions();
return (
<div>
<h2>Debug Panel</h2>
{observer && (
<>
<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>
<button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
<button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
<p>State History: {timeTravel?.getHistoryLength()}</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>
</div>
);
}
Remote Observability
Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring.
import { createStore, useRemoteObservability } from '@asaidimu/react-store';
import React, { useEffect } from 'react';
const useRemoteStore = createStore(
{
state: { apiCallsMade: 0, lastApiError: null },
actions: {
simulateApiCall: async (state) => {
// Simulate an error 10% of the time
if (Math.random() < 0.1) {
throw new Error('API request failed');
}
return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
},
handleApiError: (state, error: string) => ({ lastApiError: error })
},
},
{
enableMetrics: true, // Required for RemoteObservability
enableConsoleLogging: false,
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
}
);
function MonitoringIntegration() {
const { store, observer } = useRemoteStore();
const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
serviceName: 'my-react-app',
environment: 'development',
instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
});
useEffect(() => {
// Add OpenTelemetry Collector as a destination
addOpenTelemetryDestination({
endpoint: 'http://localhost:4318', // Default OpenTelemetry HTTP endpoint
apiKey: 'your-otel-api-key',
resource: { 'app.version': '1.0.0', 'host.name': 'frontend-server' }
});
// Add Prometheus Pushgateway as a destination
addPrometheusDestination({
pushgatewayUrl: 'http://localhost:9091', // Default Prometheus Pushgateway
jobName: 'react-store-metrics',
username: 'promuser', // Optional basic auth
password: 'prompassword',
});
// Add Grafana Cloud Loki as a destination (for logs/traces)
addGrafanaCloudDestination({
url: 'https://loki-prod-us-central1.grafana.net', // Example Loki endpoint
apiKey: 'your-grafana-cloud-api-key',
});
// Report current store metrics periodically (in addition to event-driven metrics)
const interval = setInterval(() => {
observer?.reportCurrentMetrics();
}, 5000); // Report every 5 seconds
return () => clearInterval(interval);
}, []);
return null; // This component doesn't render anything visually
}
// In your App component:
// <MonitoringIntegration />
// <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
// Simulate API Call
// </button>
Transaction Support
Group related updates that should succeed or fail together. If an error occurs within the transaction, all changes made during that transaction are automatically rolled back.
import { createStore } from '@asaidimu/react-store';
interface BankState {
checking: number;
savings: number;
transactions: string[];
}
const useBankStore = createStore<BankState, any>({
state: { checking: 1000, savings: 500, transactions: [] },
actions: {
transferFunds: async (state, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
if (amount <= 0) {
throw new Error('Transfer amount must be positive.');
}
const newChecking = fromAccount === 'checking' ? state.checking - amount : state.checking + amount;
const newSavings = fromAccount === 'savings' ? state.savings - amount : state.savings + amount;
if ((fromAccount === 'checking' && newChecking < 0) || (fromAccount === 'savings' && newSavings < 0)) {
throw new Error('Insufficient funds.');
}
// Simulate a complex operation that might fail
if (amount > 700 && fromAccount === 'checking') {
throw new Error('Large transfers from checking require additional verification.');
}
const newTransactions = [...state.transactions, `Transfer ${amount} from ${fromAccount} to ${toAccount}`];
return {
checking: newChecking,
savings: newSavings,
transactions: newTransactions,
};
},
},
});
function BankApp() {
const { select, actions } = useBankStore();
const checkingBalance = select(s => s.checking);
const savingsBalance = select(s => s.savings);
const transactions = select(s => s.transactions);
const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
try {
await actions.transferFunds(from, to, amount);
alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
} catch (error) {
alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
// State is automatically rolled back if an error occurs within the transaction
}
};
return (
<div>
<h2>Bank Accounts</h2>
<p>Checking: ${checkingBalance.toFixed(2)}</p>
<p>Savings: ${savingsBalance.toFixed(2)}</p>
<h3>Recent Transactions</h3>
<ul>
{transactions.map((t, i) => <li key={i}>{t}</li>)}
</ul>
<button onClick={() => handleTransfer('checking', 'savings', 100)}>Transfer $100 (Checking to Savings)</button>
<button onClick={() => handleTransfer('savings', 'checking', 200)}>Transfer $200 (Savings to Checking)</button>
<button onClick={() => handleTransfer('checking', 'savings', 800)}>Transfer $800 (Will Fail)</button>
<button onClick={() => handleTransfer('checking', 'savings', 1500)}>Transfer $1500 (Insufficient Funds)</button>
</div>
);
}
Event System
The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects.
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"); }
},
middleware: {
myLoggingMiddleware: (state, update) => {
console.log('Middleware processing:', update);
return update;
}
}
}
);
function EventMonitor() {
const { store } = 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)
const { actions } = useEventStore();
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>
);
}
Advanced Hook Properties
The hook returned by createStore
provides several properties for advanced usage and debugging, beyond the commonly used select
, actions
, and isReady
:
function MyAdvancedComponent() {
const {
select, // Function to select state parts (memoized)
actions, // Object containing your defined actions (debounced)
isReady, // Boolean indicating if persistence is ready
store, // Direct access to the ReactiveDataStore instance
observer, // StoreObservability instance (if `enableMetrics` was true)
actionTracker, // Instance of ActionTracker for monitoring action executions
state, // A hook `() => T` to get the entire reactive state object
} = 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
console.log("Action executions:", actionTracker.getExecutions());
return (
<div>
{/* ... your component content ... */}
</div>
);
}
Project Architecture
@asaidimu/react-store
is structured to provide a modular yet integrated state management solution.
.
βββ src/
β βββ hooks/
β β βββ observability.ts # React hook for remote observability
β βββ persistence/
β β βββ indexedb.ts # IndexedDB persistence adapter
β β βββ local-storage.ts # WebStorage (localStorage/sessionStorage) persistence adapter
β β βββ types.ts # Interface for persistence adapters
β βββ state/
β β βββ diff.ts # Utility for deep diffing state objects
β β βββ merge.ts # Utility for immutable deep merging state objects
β β βββ observability.ts # Core observability logic for ReactiveDataStore
β β βββ store.ts # Core ReactiveDataStore implementation (the state machine)
β βββ store/
β β βββ compare.ts # Utilities for fast comparison (e.g., array hashing)
β β βββ execution.ts # Action tracking interface and class
β β βββ hash.ts # Utilities for hashing objects (e.g., for selectors)
β β βββ index.ts # Main `createStore` React hook
β β βββ paths.ts # Utility for building selector paths
β β βββ selector.ts # Selector memoization manager
β βββ types.ts # Core TypeScript types for the library
β βββ utils/
β βββ destinations.ts # Concrete remote observability destinations (OTel, Prometheus, Grafana)
β βββ remote-observability.ts # Remote observability extension for StoreObservability
βββ index.ts # Main entry point for the library
βββ package.json
βββ tsconfig.json
Core Components
ReactiveDataStore
(src/state/store.ts
): The heart of the library. It manages the immutable state, processes updates (including debouncing), handles middleware, transactions, and interacts with persistence adapters. It also emits detailed internal events for observability.StoreObservability
(src/state/observability.ts
): An extension built on top ofReactiveDataStore
's event system. It provides debugging features like event history, state snapshots for time-travel, performance metrics, and utilities to create logging/validation middleware.createStore
Hook (src/store/index.ts
): The primary React-facing API. It instantiatesReactiveDataStore
andStoreObservability
, wraps actions with debouncing and tracking, and provides theselect
hook powered byuseSyncExternalStore
for efficient component updates.- Persistence Adapters (
src/persistence/
): Implement theDataStorePersistence
interface.WebStoragePersistence
(for localStorage/sessionStorage) andIndexedDBPersistence
provide concrete storage solutions with cross-tab synchronization. RemoteObservability
(src/utils/remote-observability.ts
): ExtendsStoreObservability
to enable sending metrics, logs, and traces to external monitoring systems. It defines a pluggableRemoteDestination
interface and provides out-of-the-box implementations.
Data Flow
- Action Dispatch: A React component calls an action (e.g.,
actions.increment(1)
). Actions are debounced by default. - Action Execution Tracking: The
ActionTracker
records the action's details (name, params, start time). - State Update Request: The action, after potential debouncing, initiates a
store.set()
call with a partial state update or a function. - Transaction Context: If within a
store.transaction()
, the state is snapshotted for potential rollback. - Blocking Middleware: Updates first pass through
blockingMiddleware
. If any middleware returnsfalse
or throws, the update is halted, and the state is not modified. - Transform Middleware: If not blocked, updates then pass through transforming
middleware
. These functions can modify the partial update. - State Merging: The final transformed update is immutably merged into the current state using the
merge
utility.Symbol.for("delete")
is handled here for property removal. - Change Detection: The
diff
utility identifies which paths in the state have truly changed. - Persistence: If changes occurred, the new state is saved via the configured
DataStorePersistence
adapter (e.g.,localStorage
,IndexedDB
). External changes from persistence are also subscribed to and applied. - Listener Notification: Only
React.useSyncExternalStore
subscribers whose selected paths have changed are notified, triggering re-renders of relevant components. - Observability Events: Throughout this flow, the
ReactiveDataStore
emits fine-grained events (update:start
,middleware:complete
,transaction:error
, etc.) thatStoreObservability
captures for debugging, metrics, and remote reporting.
Extension Points
- Custom Middleware: Easily add your own
Middleware
orBlockingMiddleware
functions for custom logic. - Custom Persistence Adapters: Implement the
DataStorePersistence<T>
interface to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store). - Remote Observability Destinations: Create new
RemoteDestination
implementations to send metrics and traces to any external observability platform not already supported.
Development & Contributing
We welcome contributions! Please follow the guidelines below.
Development Setup
- Clone the repository:
git clone https://github.com/asaidimu/react-store.git cd react-store
- Install dependencies:
This project uses
bun
as the package manager.bun install
Scripts
bun ci
: Installs dependencies (for CI/CD environments).bun test
: Runs all unit tests usingVitest
.bun test:ci
: Runs tests in CI mode (single run).bun clean
: Removes thedist
directory.bun prebuild
: Cleansdist
and runs a sync script (internal).bun build
: Compiles the TypeScript source intodist/
for CJS and ESM formats, generates type definitions, and minifies.bun dev
: Starts a development server (likely for a UI example).bun postbuild
: CopiesREADME.md
,LICENSE.md
, anddist.package.json
into thedist
folder.
Testing
Tests are written using Vitest
and React Testing Library
.
To run tests:
bun test
# or to run in watch mode
bun test --watch
Contributing 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.
- Commits: Follow Conventional Commits for commit messages.
- Pull Requests: Submit a pull request to the
main
branch. Provide a clear description of your changes.
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. - Action Design: Keep actions focused on a single responsibility. Use
async
actions for asynchronous operations and return partial updates upon completion. - Persistence:
- Use unique
storeId
orstorageKey
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.
- Use unique
- Middleware: Leverage middleware for cross-cutting concerns like logging, analytics, or complex validation logic.
Symbol.for("delete")
: Use this explicit symbol for property removal to maintain clarity and avoid accidental data mutations.
API Reference
createStore(definition, options)
The main entry point for creating a store.
type StoreDefinition<T, R extends Actions<T>> = {
state: T; // Initial state object
actions: R; // Object mapping action names to action functions
middleware?: Record<string, Middleware<T>>; // Optional transforming middleware
blockingMiddleware?: Record<string, BlockingMiddleware<T>>; // Optional blocking middleware
};
interface StoreOptions<T> {
enableMetrics?: boolean; // Enable StoreObservability features (default: false)
enableConsoleLogging?: boolean; // Log store events to console (default: false)
maxEvents?: number; // Maximum number of events to keep in history (default: 500)
maxStateHistory?: number; // Maximum number of state snapshots for time travel (default: 20)
logEvents?: { // Which event categories to log (defaults to all true if enableConsoleLogging is true)
updates?: boolean;
middleware?: boolean;
transactions?: boolean;
};
performanceThresholds?: { // Thresholds for logging slow operations (in ms)
updateTime?: number; // default: 50ms
middlewareTime?: number; // default: 20ms
};
persistence?: DataStorePersistence<T>; // Optional persistence adapter instance
debounceTime?: number; // Time in milliseconds to debounce actions (default: 250ms)
}
const useStore = createStore(definition, options);
Returns: A useStore
hook which, when called in a component, returns an object with:
store
: Direct access to theReactiveDataStore
instance.observer
: TheStoreObservability
instance (available ifenableMetrics
istrue
). Provides debug and monitoring utilities.select
: A memoized selector function to extract specific state slices. Re-renders components only when selected data changes.actions
: An object containing your defined actions. These actions are debounced and tracked.actionTracker
: An instance ofActionTracker
for monitoring the execution history of your actions.state
: A hook() => T
that returns the entire current state object. Use sparingly as it will cause re-renders on any state change.isReady
: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
ReactiveDataStore
(accessed via useStore().store
)
get(clone?: boolean): T
: Retrieves the current state. Passtrue
to get a deep clone (recommended for mutations outside of actions).set(update: StateUpdater<T>): Promise<void>
: Updates the state with a partial object or a function returning a partial object.subscribe(path: string | string[], listener: (state: T) => 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.use(middleware: Middleware<T>, name?: string): string
: Adds a transforming middleware. Returns its ID.useBlockingMiddleware(middleware: BlockingMiddleware<T>, name?: string): string
: Adds a blocking middleware. Returns its ID.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'
).
StoreObservability
(accessed via useStore().observer
)
getEventHistory(): DebugEvent[]
: Retrieves a history of all captured store events.getStateHistory(): T[]
: Returns a history of state snapshots, enabling time-travel.getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: Partial<T>; to: Partial<T>; }>
: 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.createLoggingMiddleware(options?: object): Middleware<T>
: A factory for a simple logging middleware.createValidationMiddleware(validator: (state: T, update: DeepPartial<T>) => boolean | { valid: boolean; reason?: string }): BlockingMiddleware<T>
: A factory for a schema validation middleware.clearHistory(): void
: Clears the event and state history.disconnect(): void
: Cleans up all listeners and resources.
RemoteObservability
(accessed via useRemoteObservability
hook)
Extends StoreObservability
with methods for sending metrics and traces externally.
addDestination(destination: RemoteDestination): boolean
: Adds a remote destination for metrics.removeDestination(id: string): boolean
: Removes a remote destination by ID.getDestinations(): Array<{ id: string; name: string }>
: Gets a list of configured destinations.testAllConnections(): Promise<Record<string, boolean>>
: Tests connectivity to all destinations.beginTrace(name: string): string
: Starts a new performance trace, returning its ID.beginSpan(traceId: string, name: string, labels?: Record<string, string>): string
: Starts a new span within a trace, returning its ID.endSpan(traceId: string, spanName: string): void
: Ends a specific span within a trace.endTrace(traceId: string): void
: Ends a performance trace and sends it to remote destinations.trackMetric(metric: RemoteMetricsPayload['metrics'][0]): void
: Manually add a metric to the batch for reporting.
Persistence Adapters
All adapters implement DataStorePersistence<T>
:
set(id:string, state: T): boolean | Promise<boolean>
: Persists data.get(): T | null | Promise<T | null>
: Retrieves data.subscribe(id:string, callback: (state:T) => void): () => void
: Subscribes to external changes.clear(): 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
).
LocalStoragePersistence(storageKey: string)
(Deprecated)
- This is an alias for
WebStoragePersistence
. UseWebStoragePersistence
instead.
Comparison with Other State Management Solutions
@asaidimu/react-store
aims to provide a comprehensive, all-in-one solution for React state management. 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 (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). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
Extensibility | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
Performance | Optimized (selectors, reactive updates). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
Bundle Size | Moderate (includes observability, persistence, remote observability). | 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, 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, and observability, reducing the need for multiple external dependencies.
- Flexibility: The robust middleware system and transaction support make it highly adaptable to complex business logic and data flows.
- Modern React: It leverages
useSyncExternalStore
for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders.
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.
- Learning Curve: The rich feature set might present a slightly steeper learning curve for developers new to advanced state management concepts, though the API strives for simplicity.
Changelog
For a detailed history of changes and new features, please refer to the CHANGELOG.md file.
License
This project is licensed under the MIT License. See the LICENSE.md file for full details.
Acknowledgments
Developed by Saidimu.
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago