Skip to content

useStoreValue Patterns ​

Core useStoreValue patterns for subscribing to store changes with selective updates, conditional subscriptions, and comparison strategies.

Prerequisites ​

This guide uses store contexts from the Basic Store Setup guide.

Required Store Setup ​

typescript
import { createStoreContext, useStoreValue } from '@context-action/react';

// User Domain Stores
const {
  Provider: UserStoreProvider,
  useStore: useUserStore,
  useStoreManager: useUserStoreManager
} = createStoreContext('User', {
  profile: {
    initialValue: { id: '', name: '', email: '', role: 'guest' as const },
    strategy: 'shallow' as const
  },
  preferences: {
    initialValue: { theme: 'light' as const, language: 'en', notifications: true },
    strategy: 'shallow' as const
  },
  session: {
    initialValue: { isAuthenticated: false, permissions: [], lastActivity: 0 },
    strategy: 'shallow' as const
  }
});

// Product Domain Stores
const {
  Provider: ProductStoreProvider,
  useStore: useProductStore,
  useStoreManager: useProductStoreManager
} = createStoreContext('Product', {
  catalog: [] as Product[],
  cart: {
    initialValue: { items: [], total: 0, discounts: [] },
    strategy: 'shallow' as const
  },
  filters: {
    initialValue: { category: '', priceRange: null, searchTerm: '' },
    strategy: 'shallow' as const
  }
});

Import ​

typescript
import { useStoreValue } from '@context-action/react';

Basic Store Subscription ​

tsx
function UserProfile() {
  // Get store instance from context
  const profileStore = useUserStore('profile');
  const preferencesStore = useUserStore('preferences');
  
  // Simple subscription to entire store value
  const profile = useStoreValue(profileStore);
  
  // Subscribe to specific field only
  const userName = useStoreValue(profileStore, user => user.name);
  const userTheme = useStoreValue(preferencesStore, prefs => prefs.theme);
  
  return (
    <div>
      <h1>{userName}</h1>
      <p>Email: {profile.email}</p>
      <p>Theme: {userTheme}</p>
    </div>
  );
}

Selective Subscriptions ​

Field Selection ​

tsx
function UserBasicInfo() {
  const profileStore = useUserStore('profile');
  
  // Only re-renders when name changes, ignores email/id/role changes
  const userName = useStoreValue(profileStore, user => user.name);
  
  // Multiple field selection
  const userBasicInfo = useStoreValue(profileStore, user => ({
    name: user.name,
    email: user.email
  }));
  
  return (
    <div>
      <h2>{userName}</h2>
      <p>{userBasicInfo.email}</p>
    </div>
  );
}

Deep Property Access ​

tsx
function ProductFilters() {
  const filtersStore = useProductStore('filters');
  const cartStore = useProductStore('cart');
  
  // Access nested properties from Product stores
  const currentCategory = useStoreValue(filtersStore, filters => filters.category);
  const priceRange = useStoreValue(filtersStore, filters => filters.priceRange);
  const cartTotal = useStoreValue(cartStore, cart => cart.total);
  const itemCount = useStoreValue(cartStore, cart => cart.items.length);
  
  return (
    <div>
      <p>Category: {currentCategory}</p>
      <p>Price Range: {priceRange ? `$${priceRange.min} - $${priceRange.max}` : 'All'}</p>
      <p>Cart: {itemCount} items, Total: ${cartTotal}</p>
    </div>
  );
}

Conditional Subscriptions ​

Dynamic Subscription Control ​

tsx
function ConditionalSubscriptions() {
  const sessionStore = useUserStore('session');
  const preferencesStore = useUserStore('preferences');
  
  const [subscribeToUpdates, setSubscribeToUpdates] = useState(true);
  
  // Only subscribe when enabled
  const lastActivity = useStoreValue(
    subscribeToUpdates ? sessionStore : null,
    session => session?.lastActivity
  );
  
  // Toggle subscription based on user preferences  
  const notificationsEnabled = useStoreValue(preferencesStore, prefs => prefs.notifications);
  const sessionData = useStoreValue(
    notificationsEnabled ? sessionStore : null,
    session => session?.permissions
  );
  
  return (
    <div>
      <label>
        <input 
          type="checkbox" 
          checked={subscribeToUpdates}
          onChange={e => setSubscribeToUpdates(e.target.checked)}
        />
        Subscribe to updates
      </label>
      {lastActivity && <p>Last activity: {new Date(lastActivity).toLocaleString()}</p>}
      {sessionData && <p>Permissions: {sessionData.length}</p>}
    </div>
  );
}

Permission-Based Subscriptions ​

tsx
function useProtectedStoreValue<T>(store: any, selector: any, requiredRole: string) {
  const profileStore = useUserStore('profile');
  const userRole = useStoreValue(profileStore, user => user.role);
  const hasPermission = userRole === requiredRole || userRole === 'admin';
  
  return useStoreValue(
    hasPermission ? store : null,
    selector
  );
}

function AdminPanel() {
  const cartStore = useProductStore('cart');
  
  // Only admin and users with admin permissions can see sensitive cart data
  const cartDiscounts = useProtectedStoreValue(
    cartStore,
    cart => cart.discounts,
    'admin'
  );
  
  return (
    <div>
      {cartDiscounts ? (
        <div>
          <h3>Admin: Cart Discounts</h3>
          <pre>{JSON.stringify(cartDiscounts, null, 2)}</pre>
        </div>
      ) : (
        <p>Access denied</p>
      )}
    </div>
  );
}

Comparison Strategies ​

Reference Comparison (Default) ​

tsx
function ReferenceComparison() {
  const profileStore = useUserStore('profile');
  
  // Fast reference comparison - only updates when reference changes
  const profile = useStoreValue(profileStore, undefined, {
    comparison: 'reference'
  });
  
  return <div>User: {profile.name}</div>;
}

Shallow Comparison ​

tsx
function ShallowComparison() {
  const preferencesStore = useUserStore('preferences');
  
  // Re-renders only when top-level properties change
  const preferences = useStoreValue(preferencesStore, undefined, { 
    comparison: 'shallow' 
  });
  
  return (
    <div>
      <p>Theme: {preferences.theme}</p>
      <p>Language: {preferences.language}</p>
      <p>Notifications: {preferences.notifications ? 'On' : 'Off'}</p>
    </div>
  );
}

Deep Comparison ​

tsx
function DeepComparison() {
  const cartStore = useProductStore('cart');
  
  // Most thorough but expensive - use sparingly
  const cart = useStoreValue(cartStore, undefined, {
    comparison: 'deep'
  });
  
  return (
    <div>
      <p>Items: {cart.items.length}</p>
      <p>Total: ${cart.total}</p>
    </div>
  );
}

Custom Comparison ​

tsx
function CustomComparison() {
  const profileStore = useUserStore('profile');
  
  const userData = useStoreValue(profileStore, user => user, {
    customComparator: (prev, next) => {
      // Only update if name or email changed, ignore other fields
      return prev.name === next.name && prev.email === next.email;
    }
  });
  
  return (
    <div>
      <h2>{userData.name}</h2>
      <p>{userData.email}</p>
    </div>
  );
}

Transformation Patterns ​

Data Formatting ​

tsx
function FormattedUserDisplay() {
  const profileStore = useUserStore('profile');
  const sessionStore = useUserStore('session');
  
  const formattedUser = useStoreValue(profileStore, user => ({
    displayName: user.name || 'Unknown User',
    initials: user.name ? user.name.split(' ').map(n => n[0]).join('') : '??',
    roleDisplay: user.role.charAt(0).toUpperCase() + user.role.slice(1),
    status: user.id ? 'Active' : 'Inactive'
  }));
  
  const lastActivity = useStoreValue(sessionStore, session => 
    session.lastActivity ? new Date(session.lastActivity).toLocaleDateString() : 'Never'
  );
  
  return (
    <div>
      <h2>{formattedUser.displayName} ({formattedUser.initials})</h2>
      <p>Role: {formattedUser.roleDisplay}</p>
      <p>Status: {formattedUser.status}</p>
      <p>Last active: {lastActivity}</p>
    </div>
  );
}

Computed Properties ​

tsx
function UserStats() {
  const profileStore = useUserStore('profile');
  const sessionStore = useUserStore('session');
  
  const userStats = useStoreValue(profileStore, user => {
    const hasPermissions = user.role === 'admin' || user.role === 'user';
    
    return {
      ...user,
      hasFullAccess: hasPermissions,
      isGuestUser: user.role === 'guest',
      canModifyData: user.role === 'admin'
    };
  });
  
  const sessionStats = useStoreValue(sessionStore, session => ({
    isAuthenticated: session.isAuthenticated,
    permissionCount: session.permissions.length,
    hasRecentActivity: Date.now() - session.lastActivity < 30 * 60 * 1000 // 30 minutes
  }));
  
  return (
    <div>
      <h3>User Statistics</h3>
      <p>Full Access: {userStats.hasFullAccess ? 'Yes' : 'No'}</p>
      <p>Can Modify: {userStats.canModifyData ? 'Yes' : 'No'}</p>
      <p>Permissions: {sessionStats.permissionCount}</p>
      <p>Recent Activity: {sessionStats.hasRecentActivity ? 'Yes' : 'No'}</p>
    </div>
  );
}

Array Filtering and Mapping ​

tsx
function ProductCatalogSummary() {
  const catalogStore = useProductStore('catalog');
  const cartStore = useProductStore('cart');
  
  const availableProducts = useStoreValue(catalogStore, products => 
    products.filter(product => product.inStock)
  );
  
  const productNames = useStoreValue(catalogStore, products =>
    products.map(product => product.name)
  );
  
  const cartItemsByCategory = useStoreValue(cartStore, cart =>
    cart.items.reduce((acc, item) => {
      const category = item.category || 'uncategorized';
      if (!acc[category]) acc[category] = [];
      acc[category].push(item);
      return acc;
    }, {} as Record<string, any[]>)
  );
  
  return (
    <div>
      <h3>Catalog Summary</h3>
      <p>Available Products: {availableProducts.length}</p>
      <p>Total Products: {productNames.length}</p>
      <div>
        <h4>Cart by Category:</h4>
        {Object.entries(cartItemsByCategory).map(([category, items]) => (
          <p key={category}>{category}: {items.length} items</p>
        ))}
      </div>
    </div>
  );
}

Performance Optimizations ​

Debounced Updates ​

tsx
import { useState } from 'react';

function DebouncedSearch() {
  const filtersStore = useProductStore('filters');
  
  // Debounce rapid search term changes
  const debouncedSearchTerm = useStoreValue(filtersStore, filters => filters.searchTerm, {
    debounce: 300  // Wait 300ms after last change
  });
  
  // Immediate access for input value (no debounce)
  const currentSearchTerm = useStoreValue(filtersStore, filters => filters.searchTerm);
  
  return (
    <div>
      <input 
        value={currentSearchTerm}
        onChange={e => filtersStore.update(prev => ({ ...prev, searchTerm: e.target.value }))}
        placeholder="Search products..."
      />
      <p>Searching for: "{debouncedSearchTerm}"</p>
    </div>
  );
}

Memoized Selectors ​

tsx
import { useCallback } from 'react';

function OptimizedUserDisplay() {
  const profileStore = useUserStore('profile');
  const cartStore = useProductStore('cart');
  
  // Stable selector prevents unnecessary re-renders
  const userName = useStoreValue(profileStore, useCallback(
    user => user.name,
    [] // No dependencies
  ));
  
  // Complex selector with stable reference
  const cartSummary = useStoreValue(cartStore, useCallback(
    cart => ({
      total: cart.items.length,
      totalPrice: cart.total,
      hasItems: cart.items.length > 0,
      averageItemPrice: cart.items.length > 0 
        ? cart.total / cart.items.length
        : 0
    }),
    []
  ));
  
  return (
    <div>
      <h2>Welcome, {userName}</h2>
      <div>
        <p>Cart Items: {cartSummary.total}</p>
        <p>Total Price: ${cartSummary.totalPrice}</p>
        <p>Average Price: ${cartSummary.averageItemPrice.toFixed(2)}</p>
      </div>
    </div>
  );
}

Error Handling ​

Safe Property Access ​

tsx
function SafeUserDisplay() {
  const profileStore = useUserStore('profile');
  
  const safeUserData = useStoreValue(profileStore, user => {
    try {
      return {
        name: user?.name || 'Unknown User',
        email: user?.email || 'No email provided',
        role: user?.role || 'guest',
        hasValidData: Boolean(user?.id)
      };
    } catch (error) {
      console.error('Error accessing user data:', error);
      return { 
        name: 'Error', 
        email: 'Error loading', 
        role: 'guest',
        hasValidData: false 
      };
    }
  });
  
  return (
    <div>
      <h2>{safeUserData.name}</h2>
      <p>Email: {safeUserData.email}</p>
      <p>Role: {safeUserData.role}</p>
      {!safeUserData.hasValidData && <p>⚠️ User data not fully loaded</p>}
    </div>
  );
}

Fallback Values ​

tsx
function UserDisplayWithFallbacks() {
  const profileStore = useUserStore('profile');
  const preferencesStore = useUserStore('preferences');
  
  const userDisplayName = useStoreValue(
    profileStore, 
    user => user?.name || user?.email || 'Guest User'
  );
  
  const userSettings = useStoreValue(preferencesStore, preferences => ({
    theme: preferences?.theme || 'light',
    language: preferences?.language || 'en',
    notifications: preferences?.notifications ?? true
  }));
  
  return (
    <div>
      <h2>{userDisplayName}</h2>
      <p>Theme: {userSettings.theme}</p>
      <p>Language: {userSettings.language}</p>
      <p>Notifications: {userSettings.notifications ? 'On' : 'Off'}</p>
    </div>
  );
}

Null Store Handling ​

tsx
function ConditionalStoreAccess() {
  const [shouldLoadCart, setShouldLoadCart] = useState(false);
  const cartStore = useProductStore('cart');
  
  const conditionalCartData = useStoreValue(
    shouldLoadCart ? cartStore : null,
    cart => cart ? {
      itemCount: cart.items.length,
      total: cart.total,
      hasDiscounts: cart.discounts.length > 0
    } : null
  );
  
  return (
    <div>
      <label>
        <input 
          type="checkbox"
          checked={shouldLoadCart}
          onChange={e => setShouldLoadCart(e.target.checked)}
        />
        Load cart data
      </label>
      
      {conditionalCartData ? (
        <div>
          <p>Items: {conditionalCartData.itemCount}</p>
          <p>Total: ${conditionalCartData.total}</p>
          <p>Has Discounts: {conditionalCartData.hasDiscounts ? 'Yes' : 'No'}</p>
        </div>
      ) : (
        <p>Cart data not loaded</p>
      )}
    </div>
  );
}

Real-World Examples ​

User Profile Display ​

tsx
function UserProfile() {
  const profileStore = useUserStore('profile');
  const sessionStore = useUserStore('session');
  const preferencesStore = useUserStore('preferences');
  
  const userInfo = useStoreValue(profileStore, user => ({
    name: user.name || 'Anonymous User',
    email: user.email,
    role: user.role,
    hasProfile: Boolean(user.id)
  }));
  
  const sessionInfo = useStoreValue(sessionStore, session => ({
    isOnline: Date.now() - session.lastActivity < 5 * 60 * 1000, // 5 minutes
    isAuthenticated: session.isAuthenticated,
    lastActivity: new Date(session.lastActivity).toLocaleDateString()
  }));
  
  const theme = useStoreValue(preferencesStore, prefs => prefs.theme);
  
  return (
    <div className={`user-profile theme-${theme}`}>
      <div className="avatar-placeholder">
        {userInfo.name.charAt(0).toUpperCase()}
      </div>
      <h2>{userInfo.name}</h2>
      <p>{userInfo.email}</p>
      <div className={`status ${sessionInfo.isOnline ? 'online' : 'offline'}`}>
        {sessionInfo.isOnline ? 'Online' : 'Offline'}
      </div>
      <p>Role: {userInfo.role}</p>
      <small>Last active: {sessionInfo.lastActivity}</small>
    </div>
  );
}

Shopping Cart Badge ​

tsx
function CartBadge() {
  const cartStore = useProductStore('cart');
  
  const cartInfo = useStoreValue(cartStore, cart => ({
    itemCount: cart.items.reduce((sum, item) => sum + item.quantity, 0),
    hasItems: cart.items.length > 0,
    total: cart.total,
    discountCount: cart.discounts.length
  }));
  
  if (!cartInfo.hasItems) {
    return (
      <div className="cart-badge empty">
        <span className="cart-icon">🛒</span>
      </div>
    );
  }
  
  return (
    <div className="cart-badge">
      <span className="cart-icon">🛒</span>
      <span className="badge">{cartInfo.itemCount}</span>
      <span className="total">${cartInfo.total.toFixed(2)}</span>
      {cartInfo.discountCount > 0 && (
        <span className="discount">-{cartInfo.discountCount}</span>
      )}
    </div>
  );
}

Product Search Results ​

tsx
function ProductSearchResults() {
  const catalogStore = useProductStore('catalog');
  const filtersStore = useProductStore('filters');
  
  const searchState = useStoreValue(filtersStore, filters => ({
    query: filters.searchTerm,
    category: filters.category,
    priceRange: filters.priceRange
  }));
  
  const searchResults = useStoreValue(catalogStore, products => 
    products.filter(product => {
      const matchesSearch = !searchState.query || 
        product.name.toLowerCase().includes(searchState.query.toLowerCase());
      const matchesCategory = !searchState.category || 
        product.category === searchState.category;
      const matchesPrice = !searchState.priceRange || 
        (product.price >= searchState.priceRange.min && product.price <= searchState.priceRange.max);
      
      return matchesSearch && matchesCategory && matchesPrice;
    })
  );
  
  // Debounce search query updates for better performance
  const debouncedQuery = useStoreValue(filtersStore, filters => filters.searchTerm, {
    debounce: 300
  });
  
  if (!searchState.query && !searchState.category && !searchState.priceRange) {
    return <div>Enter search criteria to find products</div>;
  }
  
  return (
    <div>
      <h3>
        Search Results
        {debouncedQuery && ` for "${debouncedQuery}"`}
        ({searchResults.length} found)
      </h3>
      
      {searchState.category && <p>Category: {searchState.category}</p>}
      {searchState.priceRange && (
        <p>Price: ${searchState.priceRange.min} - ${searchState.priceRange.max}</p>
      )}
      
      {searchResults.length === 0 ? (
        <p>No products found matching your criteria</p>
      ) : (
        <div className="results">
          {searchResults.map(product => (
            <div key={product.id} className="product-item">
              <h4>{product.name}</h4>
              <p>${product.price}</p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Best Practices ​

1. Use Specific Selectors ​

tsx
function UserDisplay() {
  const profileStore = useUserStore('profile');
  
  // ✅ Good: Select only what you need
  const userName = useStoreValue(profileStore, user => user.name);
  
  // ❌ Avoid: Subscribing to entire store when only name is needed
  // const user = useStoreValue(profileStore);
  // return <div>{user.name}</div>;
  
  return <div>{userName}</div>;
}

2. Memoize Complex Selectors ​

tsx
function OptimizedCartSummary() {
  const cartStore = useProductStore('cart');
  
  // ✅ Good: Memoized selector
  const processedCart = useStoreValue(cartStore, useCallback(
    cart => ({
      total: cart.items.length,
      totalValue: cart.total,
      hasExpensiveItems: cart.items.some(item => item.price > 100),
      categoryBreakdown: cart.items.reduce((acc, item) => {
        acc[item.category] = (acc[item.category] || 0) + 1;
        return acc;
      }, {})
    }),
    []
  ));
  
  // ❌ Avoid: New function on every render
  // const processedCart = useStoreValue(cartStore, cart => expensiveProcessing(cart));
  
  return (
    <div>
      <p>Items: {processedCart.total}</p>
      <p>Value: ${processedCart.totalValue}</p>
      <p>Has expensive items: {processedCart.hasExpensiveItems ? 'Yes' : 'No'}</p>
    </div>
  );
}

3. Handle Edge Cases ​

tsx
function SafeUserDisplay() {
  const profileStore = useUserStore('profile');
  
  // ✅ Good: Safe property access
  const userName = useStoreValue(profileStore, user => user?.name || 'Guest');
  
  // ❌ Avoid: Assuming properties exist
  // const userName = useStoreValue(profileStore, user => user.name);
  
  return <div>Welcome, {userName}</div>;
}

4. Choose Appropriate Comparison Strategy ​

tsx
function ComparisonExamples() {
  const profileStore = useUserStore('profile');
  const preferencesStore = useUserStore('preferences');
  const cartStore = useProductStore('cart');
  
  // For simple objects - use reference (default)
  const userId = useStoreValue(profileStore, user => user.id);
  
  // For objects with shallow changes - use shallow
  const preferences = useStoreValue(preferencesStore, undefined, { comparison: 'shallow' });
  
  // For deep nested objects - use deep sparingly
  const cartData = useStoreValue(cartStore, undefined, { comparison: 'deep' });
  
  return (
    <div>
      <p>User ID: {userId}</p>
      <p>Theme: {preferences.theme}</p>
      <p>Cart Items: {cartData.items.length}</p>
    </div>
  );
}

Provider Setup ​

To use these patterns, wrap your components with the required store providers:

tsx
import { 
  UserStoreProvider,
  ProductStoreProvider 
} from '../stores';

function App() {
  return (
    <UserStoreProvider>
      <ProductStoreProvider>
        <UserProfile />
        <CartBadge />
        <ProductSearchResults />
      </ProductStoreProvider>
    </UserStoreProvider>
  );
}

Released under the Apache-2.0 License.