Skip to main content

What are Derived Stores?

Derived stores are read-only stores that automatically compute their value based on other stores or atoms. They are perfect for creating computed state that stays in sync with your source data without manual updates. When you create a store or atom with a getter function instead of an initial value, you get a derived (computed) store that:
  • Automatically tracks dependencies
  • Only recomputes when dependencies change
  • Evaluates lazily (only when accessed)
  • Cannot be directly mutated

Creating Derived Stores

Basic Derived Store

import { createStore } from '@tanstack/store'

const countStore = createStore(0)

// This is a derived store - automatically tracks countStore
const doubledStore = createStore(() => countStore.state * 2)

console.log(doubledStore.state) // 0

countStore.setState(() => 5)
console.log(doubledStore.state) // 10
Derived stores created with createStore use the .state property to access values from other stores.

Derived Atoms

You can also create derived state using atoms directly:
import { createAtom } from '@tanstack/store'

const countAtom = createAtom(0)

// This is a computed atom - automatically tracks countAtom
const doubledAtom = createAtom(() => countAtom.get() * 2)

console.log(doubledAtom.get()) // 0

countAtom.set(5)
console.log(doubledAtom.get()) // 10
Atoms use .get() instead of .state for accessing values.

Multiple Dependencies

Derived stores automatically track all dependencies accessed in their getter function:
const firstNameStore = createStore('John')
const lastNameStore = createStore('Doe')
const ageStore = createStore(30)

// Tracks all three stores
const userSummaryStore = createStore(() => {
  const firstName = firstNameStore.state
  const lastName = lastNameStore.state
  const age = ageStore.state
  return `${firstName} ${lastName}, ${age} years old`
})

console.log(userSummaryStore.state)
// "John Doe, 30 years old"

firstNameStore.setState(() => 'Jane')
console.log(userSummaryStore.state)
// "Jane Doe, 30 years old"

Chaining Derived Stores

You can create chains of derived stores where each depends on the previous:
const radiusStore = createStore(5)

const areaStore = createStore(() => {
  const radius = radiusStore.state
  return Math.PI * radius * radius
})

const formattedAreaStore = createStore(() => {
  const area = areaStore.state
  return `${area.toFixed(2)} sq units`
})

radiusStore.setState(() => 10)
console.log(formattedAreaStore.state) // "314.16 sq units"

Conditional Dependencies

Dependencies are tracked dynamically based on what is actually accessed:
const modeStore = createStore<'simple' | 'detailed'>('simple')
const basicInfoStore = createStore('Basic info')
const detailedInfoStore = createStore('Detailed info')

const displayStore = createStore(() => {
  const mode = modeStore.state
  
  // Only tracks the store that is actually accessed
  if (mode === 'simple') {
    return basicInfoStore.state
  } else {
    return detailedInfoStore.state
  }
})

// Initially only tracks modeStore and basicInfoStore
console.log(displayStore.state) // "Basic info"

// This won't trigger displayStore to update
detailedInfoStore.setState(() => 'New detailed info')

// This will trigger displayStore to update
modeStore.setState(() => 'detailed')
console.log(displayStore.state) // "New detailed info"

Practical Examples

E-commerce Cart Total

interface CartItem {
  id: string
  name: string
  price: number
  quantity: number
}

const cartItemsStore = createStore<CartItem[]>([])
const taxRateStore = createStore(0.08) // 8% tax
const discountStore = createStore(0) // Discount in dollars

// Subtotal (before tax and discounts)
const subtotalStore = createStore(() => {
  const items = cartItemsStore.state
  return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

// Total after discount
const afterDiscountStore = createStore(() => {
  const subtotal = subtotalStore.state
  const discount = discountStore.state
  return Math.max(0, subtotal - discount)
})

// Tax amount
const taxAmountStore = createStore(() => {
  const afterDiscount = afterDiscountStore.state
  const taxRate = taxRateStore.state
  return afterDiscount * taxRate
})

// Final total
const totalStore = createStore(() => {
  const afterDiscount = afterDiscountStore.state
  const tax = taxAmountStore.state
  return afterDiscount + tax
})

// Usage
cartItemsStore.setState(() => [
  { id: '1', name: 'Widget', price: 10, quantity: 2 },
  { id: '2', name: 'Gadget', price: 15, quantity: 1 },
])

discountStore.setState(() => 5)

console.log(subtotalStore.state)      // 35
console.log(afterDiscountStore.state) // 30
console.log(taxAmountStore.state)     // 2.4
console.log(totalStore.state)         // 32.4

Search and Filter

interface Product {
  id: number
  name: string
  category: string
  price: number
  inStock: boolean
}

const productsStore = createStore<Product[]>([])
const searchQueryStore = createStore('')
const selectedCategoryStore = createStore<string | null>(null)
const maxPriceStore = createStore<number>(Infinity)
const showOutOfStockStore = createStore(true)

// Filtered products
const filteredProductsStore = createStore(() => {
  let products = productsStore.state
  const query = searchQueryStore.state.toLowerCase()
  const category = selectedCategoryStore.state
  const maxPrice = maxPriceStore.state
  const showOutOfStock = showOutOfStockStore.state

  // Apply filters
  if (query) {
    products = products.filter(p => 
      p.name.toLowerCase().includes(query)
    )
  }

  if (category) {
    products = products.filter(p => p.category === category)
  }

  if (maxPrice < Infinity) {
    products = products.filter(p => p.price <= maxPrice)
  }

  if (!showOutOfStock) {
    products = products.filter(p => p.inStock)
  }

  return products
})

// Result count
const resultCountStore = createStore(() => {
  return filteredProductsStore.state.length
})

// Available categories from current results
const availableCategoriesStore = createStore(() => {
  const products = filteredProductsStore.state
  return [...new Set(products.map(p => p.category))]
})

Form Validation

const emailStore = createStore('')
const passwordStore = createStore('')
const confirmPasswordStore = createStore('')
const agreeToTermsStore = createStore(false)

// Individual field validations
const isEmailValidStore = createStore(() => {
  const email = emailStore.state
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
})

const isPasswordValidStore = createStore(() => {
  const password = passwordStore.state
  return password.length >= 8
})

const passwordsMatchStore = createStore(() => {
  return passwordStore.state === confirmPasswordStore.state
})

// Field error messages
const emailErrorStore = createStore(() => {
  const email = emailStore.state
  const isValid = isEmailValidStore.state
  
  if (!email) return ''
  if (!isValid) return 'Please enter a valid email'
  return ''
})

const passwordErrorStore = createStore(() => {
  const password = passwordStore.state
  const isValid = isPasswordValidStore.state
  
  if (!password) return ''
  if (!isValid) return 'Password must be at least 8 characters'
  return ''
})

const confirmPasswordErrorStore = createStore(() => {
  const confirmPassword = confirmPasswordStore.state
  const match = passwordsMatchStore.state
  
  if (!confirmPassword) return ''
  if (!match) return 'Passwords do not match'
  return ''
})

// Overall form validity
const isFormValidStore = createStore(() => {
  return (
    isEmailValidStore.state &&
    isPasswordValidStore.state &&
    passwordsMatchStore.state &&
    agreeToTermsStore.state
  )
})

Dashboard Statistics

interface Sale {
  id: string
  amount: number
  date: Date
  status: 'pending' | 'completed' | 'refunded'
}

const salesStore = createStore<Sale[]>([])
const dateRangeStore = createStore<{ start: Date; end: Date }>({
  start: new Date('2024-01-01'),
  end: new Date('2024-12-31'),
})

// Filtered sales by date range
const filteredSalesStore = createStore(() => {
  const sales = salesStore.state
  const { start, end } = dateRangeStore.state
  
  return sales.filter(sale => 
    sale.date >= start && sale.date <= end
  )
})

// Total revenue
const totalRevenueStore = createStore(() => {
  const sales = filteredSalesStore.state
  return sales
    .filter(s => s.status === 'completed')
    .reduce((sum, s) => sum + s.amount, 0)
})

// Average sale amount
const averageSaleStore = createStore(() => {
  const sales = filteredSalesStore.state
  const completed = sales.filter(s => s.status === 'completed')
  
  if (completed.length === 0) return 0
  
  const total = completed.reduce((sum, s) => sum + s.amount, 0)
  return total / completed.length
})

// Refund rate
const refundRateStore = createStore(() => {
  const sales = filteredSalesStore.state
  if (sales.length === 0) return 0
  
  const refunded = sales.filter(s => s.status === 'refunded').length
  return (refunded / sales.length) * 100
})

// Status breakdown
const statusBreakdownStore = createStore(() => {
  const sales = filteredSalesStore.state
  
  return {
    pending: sales.filter(s => s.status === 'pending').length,
    completed: sales.filter(s => s.status === 'completed').length,
    refunded: sales.filter(s => s.status === 'refunded').length,
  }
})

Performance Characteristics

Derived stores only compute when their value is accessed. If a derived store isn’t being used, it won’t compute even if its dependencies change.
const expensiveStore = createStore(() => {
  console.log('Computing...')
  return heavyComputation()
})

// No log - not computed yet

const value = expensiveStore.state // Logs: "Computing..."
const value2 = expensiveStore.state // No log - cached
Derived stores cache their computed value and only recompute when dependencies actually change.
const inputStore = createStore(5)
const computedStore = createStore(() => {
  console.log('Computing...')
  return inputStore.state * 2
})

computedStore.state // Logs: "Computing..."
computedStore.state // No log - uses cache
computedStore.state // No log - uses cache

inputStore.setState(() => 10)
computedStore.state // Logs: "Computing..." - recomputes
Only stores that are actually affected by a change will recompute, creating an efficient reactive graph.

Best Practices

Keep Computations Pure

Derived stores should not have side effects. They should only compute and return a value based on their dependencies.

Avoid Expensive Operations

If a computation is expensive, consider memoizing intermediate results or using separate derived stores.

Use Specific Dependencies

Only access the specific values you need to minimize unnecessary recomputation.

Chain When Logical

Break complex computations into multiple derived stores for better readability and performance.

Stores

Learn about the base store primitive

Atoms

Lower-level reactive primitives

Subscriptions

React to changes in derived stores

Batching

Optimize multiple updates to source stores