Skip to main content
createDerivedStore creates read-only stores that automatically derive state from one or more source stores. It tracks dependencies and efficiently recomputes only when needed.

Overview

createDerivedStore provides:
  • Automatic dependency tracking - Subscribes to stores you access
  • Efficient recomputation - Only runs when dependencies change
  • Proxy-based selectors - Auto-built selectors for nested properties
  • Debouncing - Optional debounce for expensive computations
  • Type safety - Full TypeScript inference

Basic Usage

import { createDerivedStore } from '@/state/internal/createDerivedStore';

const usePortfolioTotal = createDerivedStore(($) => {
  const tokens = $(useTokensStore).tokens;
  const prices = $(usePricesStore).prices;
  
  return tokens.reduce((total, token) => {
    const price = prices[token.address] || 0;
    return total + (token.balance * price);
  }, 0);
});

In Components

function PortfolioHeader() {
  // Use the entire derived value
  const total = usePortfolioTotal();
  
  return <Text>${total.toFixed(2)}</Text>;
}

Type Signature

function createDerivedStore<Derived>(
  deriveFunction: ($: DeriveGetter) => Derived,
  optionsOrEqualityFn?: DeriveOptions<Derived> | EqualityFn<Derived>
): DerivedStore<Derived>;

Derive Function

The derive function receives a special $ helper:
type DeriveGetter = {
  // Selector-based usage
  <S>(store: Store<S>, selector: (state: S) => Selected, equalityFn?): Selected;
  
  // Proxy-based usage (auto-builds selectors)
  <S>(store: Store<S>): S;
};

The $ Helper

The $ helper tracks which stores and properties you access:

Selector-Based

const useDerived = createDerivedStore(($) => {
  // Specify exactly what to subscribe to
  const user = $(useUserStore, s => s.user, shallowEqual);
  const theme = $(useSettingsStore, s => s.appearance.theme);
  
  return {
    greeting: `Hello, ${user.name}!`,
    isDark: theme === 'dark',
  };
});

Proxy-Based (Auto-Selectors)

const useDerived = createDerivedStore(($) => {
  // Automatically subscribes to `user` property
  const { user } = $(useUserStore);
  
  // Automatically subscribes to `appearance.theme`
  const theme = $(useSettingsStore).appearance.theme;
  
  return {
    greeting: `Hello, ${user.name}!`,
    isDark: theme === 'dark',
  };
});
Proxy mode automatically builds efficient selectors for nested property access.

Real-World Example

Here’s how you might derive search results:
const useSearchResults = createDerivedStore(($) => {
  // Get search query (auto-subscribes to query property)
  const query = $(useSearchStore).query.trim().toLowerCase();
  
  // Get all items (auto-subscribes to items property)
  const items = $(useItemsStore).items;
  
  // Filter items based on query
  if (!query) return items;
  
  return items.filter(item => 
    item.name.toLowerCase().includes(query) ||
    item.description.toLowerCase().includes(query)
  );
}, {
  // Use shallow equality for array comparison
  equalityFn: shallowEqual,
  // Debounce expensive filtering
  debounce: 300,
});

Options

Equality Function

Customize when the derived store notifies subscribers:
import { dequal } from 'dequal';

const useStore = createDerivedStore(
  ($) => {
    /* ... */
  },
  {
    equalityFn: dequal,  // Deep equality
  }
);

// Or pass directly (shorthand)
const useStore = createDerivedStore(
  ($) => { /* ... */ },
  dequal
);

Debouncing

Debounce expensive computations:
const useStore = createDerivedStore(
  ($) => {
    // Expensive computation
    return expensiveCalculation($(useSourceStore));
  },
  {
    debounce: 500,  // Wait 500ms after last change
  }
);

// Or with lodash debounce options
const useStore = createDerivedStore(
  ($) => { /* ... */ },
  {
    debounce: {
      delay: 500,
      leading: false,
      trailing: true,
      maxWait: 1000,
    },
  }
);

Fast Mode

In fast mode, dependencies are established once and never rebuilt:
const useStore = createDerivedStore(
  ($) => { /* ... */ },
  {
    fastMode: true,  // Don't rebuild subscriptions
  }
);
Only use fast mode if dependencies never change. Most stores should NOT use this.

Keep Alive

Prevent automatic cleanup when no subscribers:
const useStore = createDerivedStore(
  ($) => { /* ... */ },
  {
    keepAlive: true,  // Never unsubscribe from sources
  }
);

Debug Mode

Enable logging to debug derivation:
const useStore = createDerivedStore(
  ($) => { /* ... */ },
  {
    debugMode: true,  // or 'verbose'
  }
);

// Console output:
// [🌀 Initial Derive Complete 🌀]: Created...
// [🎯 3 Selector Subscriptions 🎯]
// [📻 Derive Complete 📻]: Notifying 1 watcher

Derived Store Methods

Derived stores are read-only but expose store methods:
const useDerived = createDerivedStore(/* ... */);

// Get current state
const state = useDerived.getState();

// Subscribe to changes
const unsubscribe = useDerived.subscribe((state, prevState) => {
  console.log('Changed from', prevState, 'to', state);
});

// Flush pending debounced updates
useDerived.flushUpdates();

// Clean up (unsubscribe from all sources)
useDerived.destroy();

Multiple Source Stores

const useUserProfile = createDerivedStore(($) => {
  const user = $(useUserStore).user;
  const settings = $(useSettingsStore).settings;
  const wallet = $(useWalletStore).selectedWallet;
  
  return {
    name: user.name,
    email: user.email,
    theme: settings.theme,
    walletAddress: wallet.address,
  };
});
The store automatically:
  1. Subscribes to all three source stores
  2. Recomputes when any dependency changes
  3. Unsubscribes when no components use it

Nested Derivations

Derived stores can derive from other derived stores:
// Base derived store
const useFilteredTokens = createDerivedStore(($) => {
  const tokens = $(useTokensStore).tokens;
  const hideSmallBalances = $(useSettingsStore).hideSmallBalances;
  
  if (!hideSmallBalances) return tokens;
  return tokens.filter(t => t.balance > 0.01);
});

// Derived from derived
const useTotalValue = createDerivedStore(($) => {
  const tokens = $(useFilteredTokens);  // Subscribe to derived store
  const prices = $(usePricesStore).prices;
  
  return tokens.reduce((sum, token) => {
    return sum + (token.balance * prices[token.address]);
  }, 0);
});

Performance Optimization

Selective Subscriptions

Only subscribe to what you need:
// ✅ Good - subscribes only to specific properties
const useDerived = createDerivedStore(($) => {
  const userName = $(useUserStore).user.name;
  return `Hello, ${userName}`;
});

// ❌ Bad - subscribes to entire user object
const useDerived = createDerivedStore(($) => {
  const user = $(useUserStore).user;
  return `Hello, ${user.name}`;
});

Memoization Within Derivation

const useExpensiveCalc = createDerivedStore(($) => {
  const data = $(useDataStore).data;
  
  // Memoize expensive calculation
  return useMemo(() => {
    return expensiveTransform(data);
  }, [data]);
});

Debounce Rapid Changes

const useSearchResults = createDerivedStore(
  ($) => {
    const query = $(useSearchStore).query;
    const items = $(useItemsStore).items;
    return filterItems(items, query);
  },
  {
    debounce: 300,  // Wait for typing to stop
  }
);

Common Patterns

Filtered Lists

const useActiveTokens = createDerivedStore(($) => {
  const tokens = $(useTokensStore).tokens;
  const showHidden = $(useSettingsStore).showHiddenTokens;
  
  return tokens.filter(token => 
    showHidden || !token.isHidden
  );
}, shallowEqual);

Aggregated Metrics

const usePortfolioMetrics = createDerivedStore(($) => {
  const positions = $(usePositionsStore).positions;
  const prices = $(usePricesStore).prices;
  
  const totalValue = positions.reduce((sum, pos) => {
    return sum + (pos.amount * prices[pos.asset]);
  }, 0);
  
  const totalPnL = positions.reduce((sum, pos) => {
    return sum + pos.unrealizedPnL;
  }, 0);
  
  return {
    totalValue,
    totalPnL,
    pnlPercent: (totalPnL / totalValue) * 100,
  };
});

Computed Flags

const useAppState = createDerivedStore(($) => {
  const isAuthenticated = $(useAuthStore).isAuthenticated;
  const hasWallet = $(useWalletStore).wallets.length > 0;
  const isOnboarding = $(useOnboardingStore).isActive;
  
  return {
    canAccessApp: isAuthenticated && hasWallet && !isOnboarding,
    showOnboarding: !hasWallet || isOnboarding,
    needsAuth: !isAuthenticated,
  };
});

Sorted Lists

const useSortedTokens = createDerivedStore(($) => {
  const tokens = $(useTokensStore).tokens;
  const sortBy = $(useSettingsStore).tokenSortBy;
  const prices = $(usePricesStore).prices;
  
  return [...tokens].sort((a, b) => {
    switch (sortBy) {
      case 'value':
        const aValue = a.balance * prices[a.address];
        const bValue = b.balance * prices[b.address];
        return bValue - aValue;
      case 'balance':
        return b.balance - a.balance;
      case 'name':
        return a.name.localeCompare(b.name);
      default:
        return 0;
    }
  });
}, shallowEqual);

Lifecycle

Automatic Subscription Management

const useDerived = createDerivedStore(($) => {
  // When first component mounts:
  // 1. Run derive function
  // 2. Subscribe to all accessed stores
  // 3. Start listening for changes
  
  return $(useSourceStore).data;
});

// When last component unmounts:
// 1. Unsubscribe from all source stores
// 2. Clean up internal state
// 3. Stop listening for changes

Manual Cleanup

const useDerived = createDerivedStore(/* ... */);

// Manually destroy if needed
useDerived.destroy();

Testing

import { createDerivedStore } from '@/state/internal/createDerivedStore';
import { useTokensStore } from '@/state/tokens';
import { usePricesStore } from '@/state/prices';

describe('usePortfolioTotal', () => {
  it('calculates total value', () => {
    // Set up source stores
    useTokensStore.setState({
      tokens: [
        { address: '0x1', balance: 10 },
        { address: '0x2', balance: 5 },
      ],
    });
    
    usePricesStore.setState({
      prices: {
        '0x1': 100,
        '0x2': 200,
      },
    });
    
    // Check derived value
    const total = usePortfolioTotal.getState();
    expect(total).toBe(2000); // (10 * 100) + (5 * 200)
  });
  
  it('updates when sources change', () => {
    let callCount = 0;
    
    const unsubscribe = usePortfolioTotal.subscribe(() => {
      callCount++;
    });
    
    // Update source
    useTokensStore.setState({
      tokens: [{ address: '0x1', balance: 20 }],
    });
    
    expect(callCount).toBe(1);
    unsubscribe();
  });
});

Best Practices

Derivations should be pure functions without side effects:
// ✅ Good - pure
const useDerived = createDerivedStore(($) => {
  const data = $(useStore).data;
  return data.map(transform);
});

// ❌ Bad - side effects
const useDerived = createDerivedStore(($) => {
  const data = $(useStore).data;
  logger.log('Data changed');
  return data;
});
Choose equality functions based on your data:
// Primitives
equalityFn: Object.is

// Shallow objects/arrays
equalityFn: shallowEqual

// Deep objects/arrays
equalityFn: dequal
Add debouncing for heavy operations:
createDerivedStore(
  ($) => expensiveCalculation($(useStore)),
  { debounce: 300 }
);
Not everything needs to be derived. Sometimes a simple selector is better:
// ❌ Overkill
const useUserName = createDerivedStore(($) => 
  $(useUserStore).user.name
);

// ✅ Better
const userName = useUserStore((s) => s.user.name);

Comparison with Selectors

FeatureDerived StoreSelector
Multiple sources✅ Easy❌ Complex
Reusable✅ Yes✅ Yes
Memoization✅ Automatic❌ Manual
Subscription✅ Managed❌ Multiple
Type inference✅ Full✅ Full
OverheadMinimalNone
Use derived stores when:
  • Combining multiple stores
  • Complex transformations
  • Reused across components
Use selectors when:
  • Single store access
  • Simple property access
  • Component-specific logic

Advanced Example

Here’s a complex real-world example:
const usePortfolioDashboard = createDerivedStore(($) => {
  // Get data from multiple stores
  const tokens = $(useTokensStore).tokens;
  const prices = $(usePricesStore).prices;
  const settings = $(useSettingsStore);
  const { hideSmallBalances, sortBy } = settings;
  
  // Filter tokens
  let filteredTokens = hideSmallBalances
    ? tokens.filter(t => t.balance * prices[t.address] > 1)
    : tokens;
  
  // Calculate values
  const tokensWithValue = filteredTokens.map(token => ({
    ...token,
    price: prices[token.address] || 0,
    value: token.balance * (prices[token.address] || 0),
  }));
  
  // Sort
  const sorted = [...tokensWithValue].sort((a, b) => {
    switch (sortBy) {
      case 'value': return b.value - a.value;
      case 'balance': return b.balance - a.balance;
      case 'name': return a.name.localeCompare(b.name);
      default: return 0;
    }
  });
  
  // Aggregate metrics
  const total = sorted.reduce((sum, t) => sum + t.value, 0);
  const count = sorted.length;
  const avgValue = count > 0 ? total / count : 0;
  
  return {
    tokens: sorted,
    metrics: {
      total,
      count,
      avgValue,
    },
  };
}, {
  equalityFn: dequal,
  debounce: 100,
});

Next Steps

Storage System

Learn about MMKV persistence

Navigation

Understand the navigation architecture

Build docs developers (and LLMs) love