npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@asaidimu/react-store

v5.1.1

Published

Efficient react state manager.

Downloads

531

Readme

@asaidimu/react-store

npm version License: MIT Build Status TypeScript

A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware and artifact management system.

⚠️ Beta Warning
This package is currently in beta. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!


Table of Contents


Overview & Features

@asaidimu/react-store provides an efficient and predictable way to manage complex application state in React applications. It goes beyond basic state management by integrating features typically found in separate libraries, such as artifact management, data persistence, and comprehensive observability tools, directly into its core. This allows developers to build robust, high-performance applications with deep insights into state changes and application behavior.

Designed with modern React in mind, it leverages useSyncExternalStore for optimal performance and reactivity, ensuring components re-render only when relevant parts of the state change. Its flexible design supports a variety of use cases, from simple counter applications to complex data flows requiring atomic updates and cross-tab synchronization. The library is built with TypeScript from the ground up, offering strong type safety throughout your application's state, actions, and resolved artifacts.

Key Features

  • 📊 Reactive State Management: Automatically tracks dependencies to optimize component renders and ensure efficient updates using useSyncExternalStore.
  • 🛡️ Type-Safe: Developed entirely in TypeScript, providing strict type checking for state, actions, artifacts, and middleware.
  • ⚙️ Middleware Pipeline: Implement custom logic to transform or validate state changes before they are applied.
  • 💾 Built-in Persistence: Seamlessly integrate with web storage mechanisms like IndexedDB and WebStorage (localStorage/sessionStorage), including cross-tab synchronization.
  • 🔍 Deep Observability: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities via the StoreObserver instance.
  • 🚀 Artifact Management: Define and reactively resolve asynchronous resources, services, or derived data, enabling advanced dependency injection patterns and lazy loading of complex logic.
  • Performance Optimized: Features intelligent selector caching and debounced actions with configurable immediate execution to prevent rapid successive calls and ensure smooth application performance.
  • ⏱️ Action Loading States: Track the real-time loading status of individual actions, providing immediate feedback on asynchronous operations.
  • ⚛️ React 19+ Ready: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
  • 🗑️ Explicit Deletions: Use Symbol.for("delete") to explicitly remove properties from nested state objects.

Installation & Setup

Prerequisites

  • Node.js (v18 or higher recommended)
  • React (v19 or higher recommended)
  • A package manager like bun, npm, or yarn. This project explicitly uses bun.

Installation Steps

To add @asaidimu/react-store to your project, run one of the following commands:

bun add @asaidimu/react-store
# or
npm install @asaidimu/react-store
# or
yarn add @asaidimu/react-store

Configuration

No global configuration is required. All options are passed during store creation via the createStore function. Configuration includes enabling metrics, persistence, and performance thresholds.

Verification

You can verify the installation by importing createStore and setting up a basic store:

import { createStore } from '@asaidimu/react-store';

interface MyState {
  value: string;
  count: number;
}

const useMyStore = createStore<MyState, any, any>({
  state: { value: 'hello', count: 0 },
  actions: {
    setValue: (_, newValue: string) => ({ value: newValue }),
    increment: ({ state }) => ({ count: state.count + 1 }),
  },
});

function MyComponent() {
  const { select, actions } = useMyStore(); // Instantiate the hook
  const currentValue = select(s => s.value);
  const currentCount = select(s => s.count);

  return (
    <div>
      <p>Value: {currentValue}</p>
      <p>Count: {currentCount}</p>
      <button onClick={() => actions.setValue('world')}>Set Value to 'world'</button>
      <button onClick={() => actions.increment()}>Increment Count</button>
    </div>
  );
}

// Render MyComponent in your React app.
// If no errors are thrown during installation or when running this basic example,
// the package is correctly installed and configured.

Usage Documentation

Creating a Store

Define your application state, actions, and optionally artifacts, then create a store using createStore. The actions object maps action names to functions that receive an ActionContext (containing the current state and a resolve function for artifacts) and any additional arguments.

// ui/store.tsx (Example)
import { createStore } from '@asaidimu/react-store'; // Assuming direct import or wrapper

export interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
  image: string;
}

export interface CartItem extends Product {
  quantity: number;
}

export interface Order {
  id: string;
  items: CartItem[];
  total: number;
  date: Date;
}

export interface ECommerceState extends Record<string, any>{
  products: Product[];
  cart: CartItem[];
  orders: Order[];
  topSellers: { id: number; name: string; sales: number }[];
  activeUsers: number;
  // A property to demonstrate artifact dependency
  currency: string;
}

const initialState: ECommerceState = {
  products: [
    { id: 1, name: 'Wireless Mouse', price: 25.99, stock: 150, image: 'https://placehold.co/600x400/white/black?text=Mouse' },
    { id: 2, name: 'Mechanical Keyboard', price: 79.99, stock: 100, image: 'https://placehold.co/600x400/white/black?text=Keyboard' },
    { id: 3, name: '4K Monitor', price: 349.99, stock: 75, image: 'https://placehold.co/600x400/white/black?text=Monitor' },
    { id: 4, name: 'Webcam', price: 45.50, stock: 120, image: 'https://placehold.co/600x400/white/black?text=Webcam' },
    { id: 5, name: 'USB-C Hub', price: 39.99, stock: 200, image: 'https://placehold.co/600x400/white/black?text=Hub' },
  ],
  cart: [],
  orders: [],
  topSellers: [
    { id: 2, name: 'Mechanical Keyboard', sales: 120 },
    { id: 3, name: '4K Monitor', sales: 85 },
    { id: 1, name: 'Wireless Mouse', sales: 80 },
    { id: 5, name: 'USB-C Hub', sales: 70 },
    { id: 4, name: 'Webcam', sales: 65 },
  ],
  activeUsers: 1428,
  currency: 'USD',
};

const actions = {
  addToCart: ({ state }: any, product: Product) => {
    const existingItem = state.cart.find((item:any) => item.id === product.id);
    if (existingItem) {
      return {
        cart: state.cart.map((item:any) =>
          item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
        ),
      };
    }
    return { cart: [...state.cart, { ...product, quantity: 1 }] };
  },
  removeFromCart: ({state}: {state:ECommerceState}, productId: number) => ({
    cart: state.cart.filter((item) => item.id !== productId),
  }),
  updateQuantity: ({state}: {state:ECommerceState}, { productId, quantity }: { productId: number; quantity: number }) => ({
    cart: state.cart.map((item) =>
      item.id === productId ? { ...item, quantity } : item
    ).filter(item => item.quantity > 0),
  }),
  checkout: ({state}: {state:ECommerceState}) => {
    const total = state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
    const newOrder: Order = {
      id: crypto.randomUUID(),
      items: state.cart,
      total,
      date: new Date(),
    };
    return {
      cart: [],
      orders: [newOrder, ...state.orders],
      products: state.products.map(p => {
        const cartItem = state.cart.find(item => item.id === p.id);
        return cartItem ? { ...p, stock: p.stock - cartItem.quantity } : p;
      }),
      topSellers: state.topSellers.map(s => {
        const cartItem = state.cart.find(item => item.id === s.id);
        return cartItem ? { ...s, sales: s.sales + cartItem.quantity } : s;
      }).sort((a, b) => b.sales - a.sales),
    };
  },
  updateStock: ({state}: {state:ECommerceState}) => ({
    products: state.products.map((p:any) => ({
      ...p,
      stock: Math.max(0, p.stock + Math.floor(Math.random() * 10) - 5)
    }))
  }),
  updateActiveUsers: ({state}: {state:ECommerceState}) => ({
    activeUsers: state.activeUsers + Math.floor(Math.random() * 20) - 10,
  }),
  addRandomOrder: ({state}: {state:ECommerceState}) => {
    const randomProduct = state.products[Math.floor(Math.random() * state.products.length)];
    const quantity = Math.floor(Math.random() * 3) + 1;
    const newOrder: Order = {
      id: crypto.randomUUID(),
      items: [{ ...randomProduct, quantity }],
      total: randomProduct.price * quantity,
      date: new Date(),
    };
    return {
      orders: [newOrder, ...state.orders],
    };
  },
  setCurrency: ({state}, newCurrency: string) => ({ currency: newCurrency }),
};

export const useStore = createStore(
  {
    state: initialState,
    actions,
    // Example artifact definition
    artifacts: {
      currencySymbol: {
        factory: async ({ use }) => {
          const currency = await use(({ select }) => select((s: ECommerceState) => s.currency));
          switch (currency) {
            case 'USD': return '$';
            case 'EUR': return '€';
            case 'GBP': return '£';
            default: return currency;
          }
        },
      },
      // Another artifact example, might depend on other artifacts or state
      exchangeRate: {
        factory: async ({ resolve, use }) => {
          const baseCurrency = await use(({ select }) => select((s: ECommerceState) => s.currency));
          const targetCurrency = 'EUR'; // For demonstration
          // In a real app, this would fetch from an API
          await new Promise(r => setTimeout(r, 50)); // Simulate API delay
          if (baseCurrency === 'USD' && targetCurrency === 'EUR') {
            return 0.92; // 1 USD = 0.92 EUR
          }
          return 1.0;
        },
      },
    }
  },
  { enableMetrics: true } // Enables metrics for observability
);

Using in Components

Consume your store's state and actions within your React components using the exported hook. The select function allows you to subscribe to specific parts of the state, ensuring that your components only re-render when the selected data changes.

// ui/App.tsx (Excerpt)
import { useEffect, useMemo } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
import { useStore, Product, CartItem, Order, ECommerceState } from './store';


const ProductCatalog = () => {
  const { select, actions, resolve } = useStore();
  const products = select((state) => state.products); // Granular selection
  const { instance: currencySymbol, ready: currencyReady } = resolve('currencySymbol'); // Reactive artifact resolution

  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
      {products.map((product: Product) => (
        <Card key={product.id}>
          <CardHeader>
            <CardTitle>{product.name}</CardTitle>
          </CardHeader>
          <CardContent className="flex-grow">
            <img src={product.image} alt={product.name} className="w-full h-40 object-cover rounded-lg mb-4" />
            <div className="flex justify-between items-center">
                <p className="text-lg font-semibold text-gray-700">
                  {currencyReady ? currencySymbol : ''}{product.price.toFixed(2)}
                </p>
                <p className="text-sm text-gray-500">{product.stock} in stock</p>
            </div>
          </CardContent>
          <CardFooter>
            <Button onClick={() => actions.addToCart(product)} className="w-full">Add to Cart</Button>
          </CardFooter>
        </Card>
      ))}
    </div>
  );
};

function App() {
  const { actions, select } = useStore();
  const currentCurrency = select((state) => state.currency);

  useEffect(() => {
    // Real-time simulations for the dashboard
    const stockInterval = setInterval(() => actions.updateStock(), 2000);
    const usersInterval = setInterval(() => actions.updateActiveUsers(), 3000);
    const ordersInterval = setInterval(() => actions.addRandomOrder(), 5000);

    return () => {
      clearInterval(stockInterval);
      clearInterval(usersInterval);
      clearInterval(ordersInterval);
    };
  }, [actions]);

  return (
    <div className="bg-gray-50 text-gray-900 min-h-screen">
        <header className="border-b">
            <div className="container mx-auto px-4 h-16 flex items-center justify-between">
                <h1 className="text-xl font-bold">E-Commerce Dashboard</h1>
                <select 
                  value={currentCurrency} 
                  onChange={(e) => actions.setCurrency(e.target.value)}
                  className="p-2 border rounded-md"
                >
                  <option value="USD">USD</option>
                  <option value="EUR">EUR</option>
                  <option value="GBP">GBP</option>
                </select>
            </div>
        </header>
        <main className="container mx-auto p-4 sm:p-6 lg:p-8">
            <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
                <div className="lg:col-span-2 space-y-6">
                    <ProductCatalog />
                    {/* <LiveInventoryChart /> */}
                </div>
                <div className="space-y-6">
                    {/* <ShoppingCart /> */}
                    {/* <UserAnalytics /> */}
                    {/* <TopSellingProducts /> */}
                    {/* <RecentOrdersFeed /> */}
                </div>
            </div>
        </main>
    </div>
  );
}

export default App;

Handling Deletions

To remove a property from the state, use the Symbol.for("delete") symbol in your action’s return value. The store’s internal merge function will remove the specified key from the state.

Example

import { createStore } from '@asaidimu/react-store';

const deleteStore = createStore({
  state: {
    id: 'product-123',
    name: 'Fancy Gadget',
    details: {
      color: 'blue',
      weight: '1kg',
      dimensions: { width: 10, height: 20 }
    },
    tags: ['electronics', 'new']
  },
  actions: {
    removeDetails: (ctx) => ({ details: Symbol.for("delete") }),
    removeDimensions: (ctx) => ({ details: { dimensions: Symbol.for("delete") } }),
    removeTag: ({state}, tagToRemove: string) => ({
      tags: state.tags.filter(tag => tag !== tagToRemove)
    }),
    clearAllExceptId: (ctx) => ({
      name: Symbol.for("delete"),
      details: Symbol.for("delete"),
      tags: Symbol.for("delete")
    })
  },
});

async function runDeleteExample() {
  const { select, actions } = deleteStore();

  console.log("Initial state:", select(s => s));
  // Initial state: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg', dimensions: { width: 10, height: 20 } }, tags: ['electronics', 'new'] }

  await actions.removeDimensions();
  console.log("After removing dimensions:", select(s => s));
  // After removing dimensions: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg' }, tags: ['electronics', 'new'] }

  await actions.removeDetails();
  console.log("After removing details:", select(s => s));
  // After removing details: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics', 'new'] }

  await actions.removeTag('new');
  console.log("After removing 'new' tag:", select(s => s));
  // After removing 'new' tag: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics'] }

  await actions.clearAllExceptId();
  console.log("After clearing all except ID:", select(s => s));
  // After clearing all except ID: { id: 'product-123' }
}

runDeleteExample();

Persistence

Persist your store's state across browser sessions or synchronize it across multiple tabs using persistence adapters from @asaidimu/utils-persistence. You can choose between WebStoragePersistence (for localStorage or sessionStorage) and IndexedDBPersistence for more robust storage.

import { createStore } from '@asaidimu/react-store';
import { WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/utils-persistence';
import React, { useEffect } from 'react';

interface LocalState { sessionCount: number; lastVisited: string; }
interface SessionState { tabSpecificData: string; }
interface UserProfileState { userId: string; preferences: { language: string; darkMode: boolean; }; }

// 1. Using WebStoragePersistence (localStorage by default)
// Data persists even if the browser tab is closed and reopened.
const localStorePersistence = new WebStoragePersistence<LocalState>('my-app-state-key');
const useLocalStore = createStore(
  {
    state: { sessionCount: 0, lastVisited: new Date().toISOString() },
    actions: {
      incrementSessionCount: ({state}) => ({ sessionCount: state.sessionCount + 1 }),
      updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
    },
  },
  { persistence: localStorePersistence },
);

// 2. Using WebStoragePersistence (sessionStorage)
// Data only persists for the duration of the browser tab. Clears on tab close.
const sessionStoragePersistence = new WebStoragePersistence<SessionState>('my-session-state-key', true);
const useSessionStore = createStore(
  {
    state: { tabSpecificData: 'initial' },
    actions: {
      updateTabSpecificData: (_, newData: string) => ({ tabSpecificData: newData }),
    },
  },
  { persistence: sessionStoragePersistence },
);

// 3. Using IndexedDBPersistence
// Ideal for larger amounts of data, offers robust cross-tab synchronization.
const indexedDBPersistence = new IndexedDBPersistence<UserProfileState>('user-profile-data');
const useUserProfileStore = createStore(
  {
    state: { userId: '', preferences: { language: 'en', darkMode: false } },
    actions: {
      setUserId: (_, id: string) => ({ userId: id }),
      toggleDarkMode: ({state}) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
    },
  },
  { persistence: indexedDBPersistence },
);

function AppWithPersistence() {
  const { select: selectLocal, actions: actionsLocal, isReady: localReady } = useLocalStore();
  const { select: selectProfile, actions: actionsProfile, isReady: profileReady } = useUserProfileStore();
  const { select: selectSession, actions: actionsSession } = useSessionStore();


  const sessionCount = selectLocal(s => s.sessionCount);
  const darkMode = selectProfile(s => s.preferences.darkMode);
  const tabData = selectSession(s => s.tabSpecificData);


  useEffect(() => {
    if (localReady) {
      actionsLocal.incrementSessionCount();
      actionsLocal.updateLastVisited();
    }
    if (profileReady && !selectProfile(s => s.userId)) {
      actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
    }
  }, [localReady, profileReady, actionsLocal, actionsProfile, selectProfile]);

  if (!localReady || !profileReady) {
    return <div>Loading persisted data...</div>;
  }

  return (
    <div>
      <h3>Local Store (localStorage)</h3>
      <p>Session Count: {sessionCount}</p>
      
      <h3>Session Store (sessionStorage)</h3>
      <p>Tab Specific Data: {tabData}</p>
      <button onClick={() => actionsSession.updateTabSpecificData('updated-for-this-tab')}>
        Update Tab Data
      </button>

      <h3>User Profile Store (IndexedDB)</h3>
      <p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
      <button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
    </div>
  );
}

Middleware (Transform & Validate)

Middleware functions can intercept and modify or block state updates. The CHANGELOG.md indicates a breaking change, moving from generic middleware and blockingMiddleware to transform and validate properties in the StoreDefinition. These now receive an ActionContext with state and resolve capabilities.

  • transform: Functions that run after an action's core logic but before the state update is committed. They can modify the DeepPartial<TState> 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<false>), the state update is entirely cancelled.
import { createStore } from '@asaidimu/react-store';
import React from 'react';

interface CartState {
  items: Array<{ id: string; name: string; quantity: number; price: number }>;
  total: number;
}

const useCartStore = createStore<CartState, any, any>({ // TArtifactsMap and TActions are inferred
  state: { items: [], total: 0 },
  actions: {
    addItem: ({state}, item: { id: string; name: string; price: number }) => {
      const existingItem = state.items.find(i => i.id === item.id);
      if (existingItem) {
        return {
          items: state.items.map(i =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
          ),
        };
      }
      return {
        items: [...state.items, { ...item, quantity: 1 }],
      };
    },
    updateQuantity: ({state}, id: string, quantity: number) => ({
      items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
    }),
  },
  transform: {
    // Calculates total based on updated items before state merge
    calculateTotal: async ({ state, resolve }, update) => {
      if (update.items) {
        const newItems = update.items as CartState['items'];
        const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
        return { ...update, total: newTotal };
      }
      return update;
    },
  },
  validate: {
    // Blocks update if any item quantity is negative
    validateItemQuantity: async ({ state, resolve }, update) => {
      if (update.items) {
        for (const item of update.items as CartState['items']) {
          if (item.quantity < 0) {
            console.warn('Blocked by validator: Item quantity cannot be negative.');
            return false; // Blocks the update
          }
        }
      }
      return true; // Allows the update
    },
  },
});

function CartComponent() {
  const { select, actions } = useCartStore();
  const items = select(s => s.items);
  const total = select(s => s.total);

  return (
    <div>
      <h2>Shopping Cart</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.name} ({item.quantity}) - ${item.price} each
            <button onClick={() => actions.updateQuantity(item.id, item.quantity - 1)}>-</button>
            <button onClick={() => actions.updateQuantity(item.id, item.quantity + 1)}>+</button>
          </li>
        ))}
      </ul>
      <p>Total: ${total.toFixed(2)}</p>
      <button onClick={() => actions.addItem({ id: 'apple', name: 'Apple', price: 1.50 })}>Add Apple</button>
      <button onClick={() => actions.updateQuantity('apple', -1)}>Set Apple Quantity to -1 (Blocked)</button>
    </div>
  );
}

Artifact Management

The store supports defining and reactively resolving "artifacts," which can be any asynchronous resource, service, or derived value. Artifacts are defined in the artifacts property of the StoreDefinition and resolved using ctx.resolve() within actions or the resolve() hook in components. They can depend on other artifacts or on the store's reactive state.

import { createStore } from '@asaidimu/react-store';
import { ArtifactScopes } from '@asaidimu/utils-artifacts';
import React, { useEffect } from 'react';

interface AppState {
  userId: string | null;
  settingsLoaded: boolean;
  theme: string;
}

// Assume this is an API service or similar
const mockApiService = {
  fetchUserSettings: async (userId: string) => {
    await new Promise(r => setTimeout(r, 200)); // Simulate API delay
    if (userId === 'user-123') {
      return { theme: 'dark', notifications: true };
    }
    return { theme: 'light', notifications: false };
  },
};

const useArtifactStore = createStore<AppState, any, any>({
  state: { userId: null, settingsLoaded: false, theme: 'light' },
  actions: {
    setUserId: (_, id: string) => ({ userId: id, settingsLoaded: false }),
    loadUserSettings: async ({ state, resolve }) => {
      if (!state.userId) return;
      
      const { instance: userSettings } = await resolve('userSettings');
      if (userSettings) {
        return {
          settingsLoaded: true,
          theme: userSettings.theme,
        };
      }
      return {};
    },
    toggleTheme: ({state}) => ({ theme: state.theme === 'light' ? 'dark' : 'light' }),
  },
  artifacts: {
    // A singleton artifact that fetches user settings based on the current userId in state
    userSettings: {
      scope: ArtifactScopes.Singleton, // Ensure only one instance is created globally
      factory: async ({ use }) => {
        const userId = await use(({ select }) => select((s: AppState) => s.userId));
        if (userId) {
          console.log(`Fetching settings for user: ${userId}`);
          return mockApiService.fetchUserSettings(userId);
        }
        return null;
      },
      lazy: true, // Only create/resolve when first requested
    },
    // An artifact that provides a simple logger instance
    logger: {
      factory: async () => console,
      scope: ArtifactScopes.Singleton,
    },
  },
});

function ArtifactConsumer() {
  const { actions, select, resolve, isReady } = useArtifactStore();
  const userId = select(s => s.userId);
  const theme = select(s => s.theme);
  const settingsLoaded = select(s => s.settingsLoaded);

  // Reactively resolve the userSettings artifact in the component
  const { instance: userSettingsArtifact, ready: userSettingsReady } = resolve('userSettings');
  const { instance: logger } = resolve('logger');

  useEffect(() => {
    // Simulate setting a user ID after initial load
    if (isReady && !userId) {
      actions.setUserId('user-123');
    }
  }, [isReady, userId, actions]);

  useEffect(() => {
    // Automatically load settings when userId is available and settings not loaded
    if (userId && !settingsLoaded) {
      actions.loadUserSettings();
    }
  }, [userId, settingsLoaded, actions]);

  useEffect(() => {
    if (logger && userSettingsArtifact) {
      logger.log("User settings artifact updated:", userSettingsArtifact);
    }
  }, [logger, userSettingsArtifact]);

  if (!isReady) {
    return <div>Loading store...</div>;
  }

  return (
    <div>
      <h2>Artifact Management Example</h2>
      <p>Current User ID: {userId || 'Not set'}</p>
      <p>Settings Loaded: {settingsLoaded ? 'Yes' : 'No'}</p>
      <p>Current Theme: {theme}</p>
      {userSettingsReady && userSettingsArtifact && (
        <p>Artifact (userSettings) Resolved Theme: {userSettingsArtifact.theme}</p>
      )}
      <button onClick={() => actions.setUserId(userId === 'user-123' ? 'user-456' : 'user-123')}>
        Toggle User ID
      </button>
      <button onClick={() => actions.toggleTheme()}>Toggle App Theme</button>
    </div>
  );
}

Observability

Enable metrics and debugging via the observer and actionTracker objects. The enableMetrics option in createStore is crucial for activating these features.

import { createStore } from '@asaidimu/react-store';
import React from 'react';

const useObservedStore = createStore(
  {
    state: { task: '', completed: false, count: 0 },
    actions: {
      addTask: (_, taskName: string) => ({ task: taskName, completed: false }),
      completeTask: (_) => ({ completed: true }),
      increment: ({state}) => ({ count: state.count + 1 }),
      longRunningAction: async () => {
        await new Promise(resolve => setTimeout(resolve, 500)); // Simulate async work
        return { count: 100 };
      },
    },
  },
  {
    enableMetrics: true, // Crucial for enabling the 'observer' and 'actionTracker' objects
    enableConsoleLogging: true, // Log events directly to browser console
    logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
    performanceThresholds: {
      updateTime: 50, // Warn if updates take longer than 50ms
      middlewareTime: 20 // Warn if middleware takes longer than 20ms
    },
    maxEvents: 500, // Max number of events to keep in history
    maxStateHistory: 50, // Max number of state snapshots for time travel
    debounceTime: 0, // Actions execute immediately by default
  },
);

function DebugPanel() {
  const { actions, observer, actionTracker, select, state: getStateSnapshot } = useObservedStore();
  const count = select(s => s.count);

  // Access performance metrics
  const metrics = observer?.getPerformanceMetrics();

  // Access state history for time travel (if maxStateHistory > 0)
  const timeTravel = observer?.createTimeTravel();

  // Access action execution history
  const actionHistory = actionTracker?.getExecutions() || []; // actionTracker is only available if enableMetrics is true

  return (
    <div>
      <h2>Debug Panel</h2>
      {observer && ( // Check if observer is enabled
        <>
          <h3>Performance Metrics</h3>
          <p>Update Count: {metrics?.updateCount}</p>
          <p>Avg Update Time: {metrics?.averageUpdateTime?.toFixed(2)}ms</p>
          <p>Largest Update Size (paths): {metrics?.largestUpdateSize}</p>

          <h3>Time Travel</h3>
          <p>Current Count: {count} (via select)</p>
          <button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
          <button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
          <p>State History: {timeTravel?.getHistoryLength()}</p>
          <p>Current Snapshot (non-reactive): {JSON.stringify(getStateSnapshot())}</p>


          <h3>Action History</h3>
          <ul>
            {actionHistory.slice(0, 5).map(exec => (
              <li key={exec.id}>
                <strong>{exec.name}</strong> ({exec.status}) - {exec.duration.toFixed(2)}ms
              </li>
            ))}
          </ul>
        </>
      )}
      <button onClick={() => actions.addTask('Learn React Store')}>Add Task</button>
      <button onClick={() => actions.completeTask()}>Complete Task</button>
      <button onClick={() => actions.increment()}>Increment</button>
      <button onClick={() => actions.longRunningAction()}>Long Action</button>
    </div>
  );
}

Remote Observability

Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring. This functionality typically resides in the @asaidimu/utils-store ecosystem.

import { createStore } from '@asaidimu/react-store';
// Assuming useRemoteObservability is provided by @asaidimu/utils-store or a wrapper
// import { useRemoteObservability } from '@asaidimu/utils-store';
import React, { useEffect } from 'react';

const useRemoteStore = createStore(
  {
    state: { apiCallsMade: 0, lastApiError: null },
    actions: {
      simulateApiCall: async ({state}) => {
        if (Math.random() < 0.1) {
          throw new Error('API request failed');
        }
        return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
      },
      handleApiError: (_, error: string) => ({ lastApiError: error })
    },
  },
  {
    enableMetrics: true, // Required for RemoteObservability
    enableConsoleLogging: false,
  }
);

function MonitoringIntegration() {
  const { store, observer } = useRemoteStore();
  
  // Placeholder for actual useRemoteObservability hook
  // const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
  //   serviceName: 'my-react-app',
  //   environment: 'development',
  //   instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
  //   collectCategories: {
  //     performance: true,
  //     errors: true,
  //     stateChanges: true,
  //     middleware: true,
  //   },
  //   reportingInterval: 10000, // Send metrics every 10 seconds
  //   batchSize: 10, // Send after 10 metrics or interval, whichever comes first
  //   immediateReporting: false, // Don't send immediately after each metric
  // });

  useEffect(() => {
    // In a real implementation, you would use the `remote` object
    // to add destinations and configure reporting.
    // Example:
    // addOpenTelemetryDestination({ endpoint: 'http://localhost:4318', apiKey: 'your-otel-api-key' });
    // addPrometheusDestination({ pushgatewayUrl: 'http://localhost:9091', jobName: 'react-store-metrics' });
    // addGrafanaCloudDestination({ url: 'https://loki-prod-us-central1.grafana.net', apiKey: 'your-grafana-cloud-api-key' });

    const interval = setInterval(() => {
      observer?.reportCurrentMetrics(); // Manually trigger a report if needed
    }, 5000); // Report every 5 seconds

    return () => clearInterval(interval);
  }, [observer]); // Removed placeholder dependencies for actual usage

  return null; // This component doesn't render anything visually
}

// In your App component, you would use it like:
// function MyApp() {
//   return (
//     <>
//       <MonitoringIntegration />
//       <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
//         Simulate API Call
//       </button>
//     </>
//   );
// }

Event System

The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects. This is done via store.onStoreEvent().

import { createStore } from '@asaidimu/react-store';
import React, { useEffect } from 'react';

const useEventStore = createStore(
  {
    state: { data: 'initial', processedCount: 0 },
    actions: {
      processData: ({state}, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
      triggerError: () => { throw new Error("Action failed intentionally"); }
    },
    transform: { // Using the new middleware API
      myLoggingMiddleware: async ({state}, update) => {
        console.log('Middleware processing:', update);
        return update;
      }
    }
  }
);

function EventMonitor() {
  const { store, actions } = useEventStore();
  const [eventLogs, setEventLogs] = React.useState<string[]>([]);

  useEffect(() => {
    const addLog = (message: string) => {
      setEventLogs(prev => [`${new Date().toLocaleTimeString()}: ${message}`, ...prev].slice(0, 10));
    };

    // Subscribe to specific store events
    const unsubscribeUpdateStart = store.onStoreEvent('update:start', (data) => {
      addLog(`Update Started (timestamp: ${data.timestamp})`);
    });

    const unsubscribeUpdateComplete = store.onStoreEvent('update:complete', (data) => {
      if (data.blocked) {
        addLog(`Update BLOCKED by middleware or error. Error: ${data.error?.message || 'unknown'}`);
      } else {
        addLog(`Update Completed in ${data.duration?.toFixed(2)}ms. Paths changed: ${data.changedPaths?.join(', ')}`);
      }
    });

    const unsubscribeMiddlewareStart = store.onStoreEvent('middleware:start', (data) => {
      addLog(`Middleware '${data.name}' started (${data.type})`);
    });

    const unsubscribeMiddlewareError = store.onStoreEvent('middleware:error', (data) => {
      addLog(`Middleware '${data.name}' encountered an error: ${data.error.message}`);
    });

    const unsubscribeTransactionStart = store.onStoreEvent('transaction:start', () => {
      addLog(`Transaction Started`);
    });

    const unsubscribeTransactionError = store.onStoreEvent('transaction:error', (data) => {
      addLog(`Transaction Failed: ${data.error.message}`);
    });

    const unsubscribePersistenceReady = store.onStoreEvent('persistence:ready', () => {
      addLog(`Persistence is READY.`);
    });

    // Cleanup subscriptions on component unmount
    return () => {
      unsubscribeUpdateStart();
      unsubscribeUpdateComplete();
      unsubscribeMiddlewareStart();
      unsubscribeMiddlewareError();
      unsubscribeTransactionStart();
      unsubscribeTransactionError();
      unsubscribePersistenceReady();
    };
  }, [store]); // Re-subscribe if store instance changes (unlikely)

  return (
    <div>
      <h3>Store Event Log</h3>
      <button onClick={() => actions.processData('new data')}>Process Data</button>
      <button onClick={() => actions.triggerError().catch(() => {})}>Trigger Action Error</button>
      <button onClick={() => store.transaction(() => { actions.processData('transaction data'); throw new Error('Transaction error'); }).catch(() => {})}>
        Simulate Transaction Error
      </button>
      <ul style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
        {eventLogs.map((log, index) => <li key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log}</li>)}
      </ul>
    </div>
  );
}

Watching Action Loading States

The watch function returned by the useStore hook allows you to subscribe to the loading status of individual actions. This is particularly useful for displaying loading indicators for asynchronous operations.

import React from 'react';
import { createStore } from '@asaidimu/react-store';

interface DataState {
  items: string[];
}

const useDataStore = createStore({
  state: { items: [] },
  actions: {
    fetchItems: async ({state}) => {
      // Simulate an API call
      await new Promise(resolve => setTimeout(resolve, 2000));
      return { items: ['Item A', 'Item B', 'Item C'] };
    },
    addItem: async ({state}, item: string) => {
      // Simulate a quick add operation
      await new Promise(resolve => setTimeout(resolve, 500));
      return { items: [...state.items, item] };
    },
  },
});

function DataLoader() {
  const { actions, select, watch } = useDataStore();
  const items = select(s => s.items);

  // Watch the loading state of specific actions
  const isFetchingItems = watch('fetchItems');
  const isAddingItem = watch('addItem');

  return (
    <div>
      <h2>Data Loader</h2>
      <button onClick={() => actions.fetchItems()} disabled={isFetchingItems}>
        {isFetchingItems ? 'Fetching...' : 'Fetch Items'}
      </button>
      <button onClick={() => actions.addItem(`New Item ${items.length + 1}`)} disabled={isAddingItem}>
        {isAddingItem ? 'Adding...' : 'Add Item'}
      </button>
      
      <h3>Items:</h3>
      <ul>
        {items.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </div>
  );
}

Advanced Hook Properties

The hook returned by createStore provides several properties for advanced usage and debugging, beyond the commonly used select, actions, and isReady:

import { useStore as useMyStore } from './store'; // Assuming this is your store definition

function MyAdvancedComponent() {
  const {
    select,        // Function to select state parts (memoized, reactive)
    actions,       // Object containing your defined actions (debounced, promise-returning)
    isReady,       // Boolean indicating if persistence is ready
    store,         // Direct access to the ReactiveDataStore instance (from @asaidimu/utils-store)
    observer,      // StoreObserver instance (from @asaidimu/utils-store, if `enableMetrics` was true)
    actionTracker, // Instance of ActionTracker for monitoring action executions (if `enableMetrics` was true)
    state,         // A getter function `() => TState` to get the entire current state object (reactive)
    watch,         // Function to watch the loading status of actions
    resolve,       // Function to reactively resolve an artifact (if artifacts are defined)
  } = useMyStore(); // Assuming useMyStore is defined from createStore

  // Example: Accessing the full state (use with caution for performance; `select` is preferred)
  const fullCurrentState = state();
  console.log("Full reactive state:", fullCurrentState);

  // Example: Accessing observer methods (if enabled)
  if (observer) {
    console.log("Performance metrics:", observer.getPerformanceMetrics());
    console.log("Recent state changes:", observer.getRecentChanges(3));
  }

  // Example: Accessing action history
  if (actionTracker) { // actionTracker is only available if enableMetrics is true
    console.log("Action executions:", actionTracker.getExecutions());
  }

  // Example: Watching a specific action's loading state
  const isLoadingSomeAction = watch('checkout'); // Assuming 'checkout' is an action from the example store
  console.log("Is 'checkout' action loading?", isLoadingSomeAction);

  // Example: Resolving an artifact (from the example store)
  const { instance: currencySymbol, ready: isCurrencySymbolReady } = resolve('currencySymbol');
  if (isCurrencySymbolReady) {
    console.log("Currency Symbol artifact is ready:", currencySymbol);
  }

  return (
    <div>
      {/* ... your component content ... */}
      <p>Store Ready: {isReady ? 'Yes' : 'No'}</p>
    </div>
  );
}

Project Architecture

@asaidimu/react-store is structured to provide a modular yet integrated state management solution. It separates core state logic into reusable utilities while offering a streamlined React-specific API.

Internal Structure

The core logic of this package integrates and extends external utility packages, with src/store.ts serving as the main entry point for the React hook.

  • src/execution.ts: 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.

Key External Dependencies

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.

Core Components

  • ReactiveDataStore (from @asaidimu/utils-store): The heart of the state management. It handles immutable state updates, middleware processing, transaction management, and emits detailed internal events about state changes.
  • StoreObserver (from @asaidimu/utils-store): Built on top 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.

Data Flow

  1. Action Dispatch: A React component calls a bound action (e.g., actions.addItem()).
  2. Action Debouncing: Actions are debounced by default (configurable), 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).
  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.

Extension Points

  • 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<T> interface (from @asaidimu/utils-persistence) to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store).
  • Custom Artifact Factories: Define factories for any external service, resource, or complex derived state, allowing for clear separation of concerns and reactive dependency injection.
  • Remote Observability Destinations: Create new 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.

Development Setup

  1. Clone the repository:
    git clone https://github.com/asaidimu/node-react.git
    cd react-store
  2. Install dependencies: This project uses bun as the package manager.
    bun install

Scripts

  • bun ci: Installs dependencies, typically used in CI/CD environments to ensure a clean install.
  • bun test: Runs all unit tests using Vitest in interactive watch mode.
  • bun test:ci: Runs all unit tests once, suitable for CI/CD pipelines.
  • 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.

Testing

Tests are written using Vitest and React Testing Library for component and hook testing.

To run tests:

bun test
# or to run in watch mode
bun test --watch

Contributing Guidelines

  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.

Issue Reporting

For bugs, feature requests, or questions, please open an issue on the GitHub Issues page.

Additional Information

Best Practices

  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.

API Reference

createStore(definition, options)

The main entry point for creating a store hook.

// From src/types.ts
export interface StoreDefinition<
    TState extends object,
    TArtifactsMap extends ArtifactsMap<TState>,
    TActions extends ActionMap<TState, TArtifactsMap>,
> {
    state: TState;
    actions: TActions;
    artifacts?: TArtifactsMap; // Optional artifact definitions
    transform?: Record<string, Transformer<TState, TArtifactsMap>>; // Optional transforming middleware
    validate?: Record<string, Validator<TState, TArtifactsMap>>; // Optional blocking middleware
}

interface StoreOptions<T> extends ObserverOptions { // ObserverOptions from @asaidimu/utils-store
    enableMetrics?: boolean; // Enable StoreObserver and ActionTracker features (default: false)
    persistence?: SimplePersistence<T>; // Optional persistence adapter instance (from @asaidimu/utils-persistence)
    debounceTime?: number; // Time in milliseconds to debounce actions (default: 0ms)
    // Other ObserverOptions for logging, performanceThresholds, maxEvents, maxStateHistory are inherited
}

const useStoreHook = createStore(definition, options);

Returns: A React hook (useStoreHook) which, when called in a component, returns an object with the following properties:

  • store: Direct access to the 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 (<S>(selector: (state: TState) => S) => S). Extracts specific state slices. Re-renders components only when selected data changes.
  • actions: An object containing your defined actions, fully typed and bound to the store. These actions are debounced (if debounceTime > 0) and their loading states are tracked. Each action returns a Promise<TState> 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 (<K extends keyof TActions>(action: K) => boolean) to watch the loading status of individual actions. Returns true if the action is currently executing.
  • resolve: A reactive artifact resolver (<K extends keyof TArtifactsMap>(key: K) => ResolvedArtifact<ArtifactValue<TArtifactsMap[K]>>). 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<TState>): Promise<void>: Updates the state with a partial object or a function returning a partial object.
  • watch(path: string | string[], callback: (state: TState) => void): () => void: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.
  • transaction<R>(operation: () => R | Promise<R>): Promise<R>: Executes a function as an atomic transaction. Rolls back all changes if an error occurs if the operation throws.
  • use(middleware: StoreMiddleware<TState>): string: Adds a transforming middleware. Returns its ID. (Note: 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<TState>; to: DeepPartial<TState>; }>: Provides a simplified view of recent state changes.
  • getPerformanceMetrics(): StoreMetrics: Returns an object containing performance statistics (e.g., updateCount, averageUpdateTime).
  • createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }: Returns controls for time-travel debugging.
  • clearHistory(): void: Clears the event and state history.
  • disconnect(): void: Cleans up all listeners and resources.

Persistence Adapters (from @asaidimu/utils-persistence)

All adapters implement SimplePersistence<T>:

  • set(id:string, state: T): boolean | Promise<boolean>: Persists data.
  • get(id:string): T | null | Promise<T | null>: Retrieves data.
  • subscribe(id:string, callback: (state:T) => void): () => void: Subscribes to external changes (e.g., from other tabs).
  • clear(id:string): boolean | Promise<boolean>: Clears persisted data.
IndexedDBPersistence(storeId: string)
  • storeId: A unique identifier for the IndexedDB object store (e.g., 'user-data').
WebStoragePersistence(storageKey: string, session?: boolean)
  • storageKey: The key under which data is stored (e.g., 'app-config').
  • session: Optional. If true, uses sessionStorage; otherwise, uses localStorage (default: false).

Comparison with Other State Management Solutions

@asaidimu/react-store aims to be a comprehensive, all-in-one solution for React state management, integrating features that often require multiple libraries in other ecosystems. Here's a comparison to popular alternatives:

| Feature | @asaidimu/react-store | Redux | Zustand | MobX | Recoil | | :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- | | Dev Experience | Intuitive hook-based API with rich tooling. | Verbose setup with reducers and middleware. | Minimalist, hook-friendly API. | Reactive, class-based approach. | Atom-based, React-native feel. | | Learning Curve | Moderate (artifacts, middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). | | API Complexity | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). | | Scalability | High (transactions, persistence, remote metrics, artifacts). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). | | Extensibility | Excellent (middleware, custom persistence, observability, artifacts). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). | | Performance | Optimized (selectors, reactive updates via useSyncExternalStore). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). | | Bundle Size | Moderate (includes observability, persistence, remote observability framework). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). | | Persistence | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). | | Observability | Excellent (metrics, time-travel, event logging, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). | | React Integration | Native (hooks, useSyncExternalStore). | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |

Where @asaidimu/react-store Shines

  • All-in-One: It aims to be a single solution for state management, persistence, observability, and artifact management, reducing the need for multiple external dependencies and their integration complexities.
  • Flexibility: The robust middleware system, transaction support, and artifact management make it highly adaptable to complex business logic, asynchronous data flows, and dependency injection patterns.
  • Modern React: It leverages useSyncExternalStore for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders with minimal overhead.

Trade-Offs

  • Bundle Size: While comprehensive, it naturally has a larger bundle size compared to minimalist alternatives like Zustand, as it includes a wider range of features out-of-the-box. Tree-shaking is applied, but the rich feature set contributes to the baseline.
  • Learning Curve: The rich feature set and advanced concepts (middleware, transactions, observability, artifacts) might present a slightly steeper initial learning curve for developers new to advanced state management, though the API strives for simplicity and clear documentation.

Troubleshooting

  • Components not re-rendering:
    • Ensure you are using select with a specific path (e.g., select(s => s.user.name)) instead of the entire state object.
    • Verify that the data at the selected path is actually changing (reference equality matters for objects/arrays).
  • Persistence not loading/saving:
    • Check if isReady is true before interacting with state dependent on persistence.
    • Ensure your `persis