Skip to main content

Overview

TanStack Store is built on alien-signals, a high-performance reactive system that provides automatic optimization for most use cases. This guide covers advanced performance patterns for building highly efficient applications.

The Reactive Core

TanStack Store uses a fine-grained reactive system that tracks dependencies automatically:
  • Automatic dependency tracking: Only recomputes when dependencies change
  • Diamond dependency resolution: Efficiently handles complex dependency graphs
  • Lazy evaluation: Derived stores only compute when accessed
  • Minimal re-renders: Subscribers only fire when values actually change
import { createStore } from '@tanstack/store'

const count = createStore(10)

// Automatically tracks dependency on count
const doubled = createStore(() => count.state * 2)

// Only recomputes when count changes
const message = createStore(() => {
  return `Count is ${doubled.state}`
})

Batching Updates

Why Batch?

Batching multiple state updates prevents unnecessary intermediate computations and notifications:
import { batch, createStore } from '@tanstack/store'

const store = createStore({ count: 0, multiplier: 1 })

// ❌ Without batching: Triggers 2 updates
store.setState((s) => ({ ...s, count: s.count + 1 }))
store.setState((s) => ({ ...s, multiplier: 2 }))

// ✅ With batching: Triggers 1 update
batch(() => {
  store.setState((s) => ({ ...s, count: s.count + 1 }))
  store.setState((s) => ({ ...s, multiplier: 2 }))
})

Batch API

The batch function defers notifications until all updates complete:
import { batch, createStore } from '@tanstack/store'

const firstName = createStore('John')
const lastName = createStore('Doe')

const fullName = createStore(() => {
  return `${firstName.state} ${lastName.state}`
})

let notificationCount = 0
fullName.subscribe(() => {
  notificationCount++
})

batch(() => {
  firstName.setState('Jane')
  lastName.setState('Smith')
})

// Only 1 notification instead of 2!
console.log(notificationCount) // 1
console.log(fullName.state) // "Jane Smith"

Batching Best Practices

Batch Related Updates

Group state updates that logically belong together:
batch(() => {
  userStore.setState(newUser)
  settingsStore.setState(newSettings)
  preferencesStore.setState(newPrefs)
})

Batch in Event Handlers

Use batching in user interactions:
function handleSubmit() {
  batch(() => {
    formStore.setState({ submitted: true })
    errorsStore.setState({})
    loadingStore.setState(true)
  })
}

Fine-Grained Subscriptions

Selective Updates

Subscribe only to the data you need to avoid unnecessary re-renders:
interface AppState {
  user: User
  settings: Settings
  notifications: Notification[]
}

const appStore = createStore<AppState>(initialState)

// ❌ Subscribes to all changes
appStore.subscribe((state) => {
  console.log(state.user.name)
})

// ✅ Create derived store for just the user name
const userNameStore = createStore(() => appStore.state.user.name)
userNameStore.subscribe((name) => {
  console.log(name) // Only fires when name changes
})

Derived Store Optimization

Derived stores automatically optimize by only recomputing when their dependencies change:
const numbers = createStore([1, 2, 3, 4, 5])

// Only recomputes when numbers array changes
const sum = createStore(() => {
  console.log('Computing sum...')
  return numbers.state.reduce((a, b) => a + b, 0)
})

// Accessing sum multiple times doesn't recompute
console.log(sum.state) // "Computing sum..." 15
console.log(sum.state) // 15 (no log, uses cached value)
console.log(sum.state) // 15 (no log, uses cached value)

// Only recomputes when source changes
numbers.setState([1, 2, 3])
console.log(sum.state) // "Computing sum..." 6

Selector Patterns

Creating Efficient Selectors

Create focused derived stores that select specific slices of state:
interface TodoState {
  todos: Todo[]
  filter: 'all' | 'active' | 'completed'
  searchQuery: string
}

const todoStore = createStore<TodoState>({
  todos: [],
  filter: 'all',
  searchQuery: '',
})

// Selector for filtered todos
const filteredTodos = createStore(() => {
  const { todos, filter } = todoStore.state

  if (filter === 'all') return todos
  if (filter === 'active') return todos.filter((t) => !t.completed)
  return todos.filter((t) => t.completed)
})

// Selector for searched todos
const searchedTodos = createStore(() => {
  const todos = filteredTodos.state
  const query = todoStore.state.searchQuery.toLowerCase()

  if (!query) return todos
  return todos.filter((t) => t.title.toLowerCase().includes(query))
})

// Component only re-renders when searchedTodos changes
const TodoList = () => {
  const todos = useStore(searchedTodos)
  return <div>{todos.map(renderTodo)}</div>
}

Memoization with Selectors

Selectors act as automatic memoization:
// Expensive computation only runs when dependencies change
const expensiveComputation = createStore(() => {
  const data = dataStore.state

  // This only runs when dataStore changes
  return data.map((item) => {
    return complexTransformation(item)
  })
})

Diamond Dependency Optimization

TanStack Store efficiently handles diamond dependencies (where multiple derived stores depend on the same source):
const count = createStore(10)

//        count
//        /   \
//    half    double
//        \   /
//         sum

const half = createStore(() => count.state / 2)
const double = createStore(() => count.state * 2)
const sum = createStore(() => half.state + double.state)

let computations = 0
sum.subscribe(() => computations++)

// Changing count triggers only ONE notification to sum
count.setState(20)
console.log(computations) // 1 (not 2!)
console.log(sum.state) // 30 (10 + 20)

Complex Diamond Dependencies

The reactive system handles even complex dependency graphs efficiently:
//         a
//        / \
//       b   c
//      / \  |
//     d   e f
//      \ / |
//       \ /
//        g

const a = createStore(1)
const b = createStore(() => a.state)
const c = createStore(() => a.state)
const d = createStore(() => b.state)
const e = createStore(() => b.state)
const f = createStore(() => c.state)
const g = createStore(() => d.state + e.state + f.state)

let updates = 0
g.subscribe(() => updates++)

// Changing 'a' triggers only ONE update to 'g'
a.setState(2)
console.log(updates) // 1
console.log(g.state) // 6 (2 + 2 + 2)

Comparison Functions

Custom Equality Checks

Provide custom comparison functions to control when updates trigger:
import { createAtom } from '@tanstack/store'

interface User {
  id: number
  name: string
  lastSeen: Date
}

// Only update when id or name changes, ignore lastSeen
const userAtom = createAtom<User>(
  { id: 1, name: 'John', lastSeen: new Date() },
  {
    compare: (prev, next) => {
      return prev.id === next.id && prev.name === next.name
    },
  }
)

let updates = 0
userAtom.subscribe(() => updates++)

// Doesn't trigger update (only lastSeen changed)
userAtom.set({
  id: 1,
  name: 'John',
  lastSeen: new Date(),
})
console.log(updates) // 0

// Triggers update (name changed)
userAtom.set({
  id: 1,
  name: 'Jane',
  lastSeen: new Date(),
})
console.log(updates) // 1

Shallow Comparison

For array and object stores, use shallow comparison when appropriate:
import { createAtom } from '@tanstack/store'

function shallowEqual<T>(a: T, b: T): boolean {
  if (Object.is(a, b)) return true
  if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
    return false
  }

  const keysA = Object.keys(a)
  const keysB = Object.keys(b)

  if (keysA.length !== keysB.length) return false

  return keysA.every((key) => Object.is(a[key], b[key]))
}

const atom = createAtom({ a: 1, b: 2 }, { compare: shallowEqual })

Performance Measurement

Tracking Computations

Monitor how often derived stores recompute:
let computeCount = 0

const expensiveStore = createStore(() => {
  computeCount++
  console.log(`Computed ${computeCount} times`)
  return expensiveCalculation(dataStore.state)
})

// Monitor computeCount to ensure minimal recomputations

Benchmarking with Vitest

TanStack Store includes benchmarks for performance testing:
// From packages/store/tests/derived.bench.ts
import { bench, describe } from 'vitest'
import { createStore } from '@tanstack/store'

describe('derived store performance', () => {
  bench('1000 updates with derived stores', () => {
    const count = createStore(0)
    const doubled = createStore(() => count.state * 2)

    for (let i = 0; i < 1000; i++) {
      count.setState(i)
      doubled.state // Access to trigger computation
    }
  })
})

Best Practices

1

Use Derived Stores for Computed Values

Don’t store computed values directly. Let derived stores handle them:
// ❌ Manually computing and storing
const store = createStore({ count: 0, doubled: 0 })
store.setState((s) => ({ ...s, count: 5, doubled: 10 }))

// ✅ Use derived store
const count = createStore(0)
const doubled = createStore(() => count.state * 2)
2

Batch Related Updates

Always batch updates that happen together:
batch(() => {
  store1.setState(value1)
  store2.setState(value2)
  store3.setState(value3)
})
3

Create Focused Selectors

Build small, focused derived stores instead of one large one:
// ✅ Focused selectors
const userName = createStore(() => userStore.state.name)
const userEmail = createStore(() => userStore.state.email)
4

Use Custom Comparisons Sparingly

Only use custom comparison functions when necessary. The default Object.is comparison works well for most cases.

Common Performance Pitfalls

Avoid creating derived stores inside components
// ❌ Bad: Creates new store on every render
function Component() {
  const derived = createStore(() => store.state.value * 2)
  return <div>{derived.state}</div>
}

// ✅ Good: Create stores outside components
const derived = createStore(() => store.state.value * 2)
function Component() {
  return <div>{derived.state}</div>
}
Avoid accessing stores in loops
// ❌ Bad: Accesses store repeatedly
for (let i = 0; i < items.length; i++) {
  process(store.state, items[i])
}

// ✅ Good: Access once, use value in loop
const state = store.state
for (let i = 0; i < items.length; i++) {
  process(state, items[i])
}

Profiling Tools

Browser DevTools

Use React DevTools Profiler to identify unnecessary re-renders:
  1. Open React DevTools
  2. Go to Profiler tab
  3. Record interactions
  4. Look for components that re-render when they shouldn’t

Performance Testing

Write performance tests to catch regressions:
import { expect, test } from 'vitest'
import { createStore } from '@tanstack/store'

test('updates should complete in under 100ms', () => {
  const store = createStore(0)
  const derived = createStore(() => store.state * 2)

  const start = performance.now()

  for (let i = 0; i < 1000; i++) {
    store.setState(i)
    derived.state // Trigger computation
  }

  const duration = performance.now() - start
  expect(duration).toBeLessThan(100)
})

Next Steps