What is an Atom?
Atoms are the foundational building blocks of TanStack Store’s reactivity system. They are low-level primitives that provide fine-grained reactive state management with automatic dependency tracking.
While Stores provide a simple, high-level API, atoms give you more control over reactivity, comparison logic, and performance optimization.
Creating Atoms
Use the createAtom function to create atoms. Like stores, atoms can be created from values or getter functions:
Mutable Atoms (from values)
import { createAtom } from '@tanstack/store'
const countAtom = createAtom ( 0 )
// Set the value
countAtom . set ( 5 )
// Get the current value
console . log ( countAtom . get ()) // 5
Read-only Atoms (from functions)
import { createAtom } from '@tanstack/store'
const doubledAtom = createAtom (() => countAtom . get () * 2 )
// Automatically recomputes when countAtom changes
console . log ( doubledAtom . get ()) // 10
Read-only atoms (computed atoms) automatically track their dependencies and recompute only when necessary.
Type Signatures
// Create from value - returns Atom<T>
function createAtom < T >(
initialValue : T ,
options ?: AtomOptions < T >
) : Atom < T >
// Create from getter - returns ReadonlyAtom<T>
function createAtom < T >(
getValue : ( prev ?: NoInfer < T >) => T ,
options ?: AtomOptions < T >
) : ReadonlyAtom < T >
interface Atom < T > extends BaseAtom < T > {
set : (( fn : ( prevVal : T ) => T ) => void ) & (( value : T ) => void )
}
interface ReadonlyAtom < T > extends BaseAtom < T > {}
interface BaseAtom < T > {
get : () => T
subscribe : (
observerOrFn : Observer < T > | (( value : T ) => void )
) => Subscription
}
interface AtomOptions < T > {
compare ?: ( prev : T , next : T ) => boolean
}
Atom API
set(valueOrUpdater)
Sets the atom’s value. Available only on mutable atoms (not read-only atoms).
const atom = createAtom ( 0 )
// Set directly
atom . set ( 42 )
// Set using an updater function
atom . set (( prev ) => prev + 1 )
get()
Returns the current value of the atom. Automatically tracks dependencies when called inside computed atoms or subscriptions.
const atom = createAtom ( 42 )
console . log ( atom . get ()) // 42
subscribe(observerOrFn)
Subscribes to changes in the atom. Returns a subscription with an unsubscribe method.
const atom = createAtom ( 0 )
const sub = atom . subscribe (( value ) => {
console . log ( 'Atom changed:' , value )
})
// Trigger the subscription
atom . set ( 1 ) // Logs: "Atom changed: 1"
// Clean up
sub . unsubscribe ()
Custom Comparison
By default, atoms use Object.is to determine if a value has changed. You can provide a custom comparison function:
interface Point {
x : number
y : number
}
const pointAtom = createAtom < Point >(
{ x: 0 , y: 0 },
{
compare : ( prev , next ) => {
// Only update if coordinates actually changed
return prev . x === next . x && prev . y === next . y
},
}
)
// This won't trigger subscribers (same values)
pointAtom . set ({ x: 0 , y: 0 })
// This will trigger subscribers
pointAtom . set ({ x: 1 , y: 2 })
Custom comparison functions are useful for avoiding unnecessary re-renders with complex objects or when you want to implement shallow equality.
Computed Atoms (Derived State)
Computed atoms automatically track dependencies and only recompute when their dependencies change:
const firstNameAtom = createAtom ( 'John' )
const lastNameAtom = createAtom ( 'Doe' )
// Automatically tracks both firstName and lastName
const fullNameAtom = createAtom (() => {
return ` ${ firstNameAtom . get () } ${ lastNameAtom . get () } `
})
console . log ( fullNameAtom . get ()) // "John Doe"
firstNameAtom . set ( 'Jane' )
console . log ( fullNameAtom . get ()) // "Jane Doe"
Lazy Evaluation
Computed atoms are lazy - they only compute when their value is accessed:
const expensiveAtom = createAtom (() => {
console . log ( 'Computing...' )
return heavyComputation ()
})
// Nothing logged yet - not computed until accessed
const result = expensiveAtom . get () // Logs: "Computing..."
const result2 = expensiveAtom . get () // No log - cached value returned
Async Atoms
TanStack Store provides createAsyncAtom for handling asynchronous state:
import { createAsyncAtom } from '@tanstack/store'
const userAtom = createAsyncAtom ( async () => {
const response = await fetch ( '/api/user' )
return response . json ()
})
// Subscribe to state changes
userAtom . subscribe (( state ) => {
if ( state . status === 'pending' ) {
console . log ( 'Loading...' )
} else if ( state . status === 'done' ) {
console . log ( 'User:' , state . data )
} else if ( state . status === 'error' ) {
console . error ( 'Error:' , state . error )
}
})
The state type is:
type AsyncAtomState < TData , TError = unknown > =
| { status : 'pending' }
| { status : 'done' ; data : TData }
| { status : 'error' ; error : TError }
Practical Examples
interface CartItem {
id : string
name : string
price : number
quantity : number
}
const cartItemsAtom = createAtom < CartItem []>([])
// Computed total
const cartTotalAtom = createAtom (() => {
const items = cartItemsAtom . get ()
return items . reduce (( sum , item ) => sum + item . price * item . quantity , 0 )
})
// Computed count
const cartCountAtom = createAtom (() => {
const items = cartItemsAtom . get ()
return items . reduce (( sum , item ) => sum + item . quantity , 0 )
})
function addToCart ( item : Omit < CartItem , 'quantity' >) {
cartItemsAtom . set (( items ) => {
const existing = items . find (( i ) => i . id === item . id )
if ( existing ) {
return items . map (( i ) =>
i . id === item . id ? { ... i , quantity: i . quantity + 1 } : i
)
}
return [ ... items , { ... item , quantity: 1 }]
})
}
const emailAtom = createAtom ( '' )
const passwordAtom = createAtom ( '' )
const isEmailValidAtom = createAtom (() => {
const email = emailAtom . get ()
return / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / . test ( email )
})
const isPasswordValidAtom = createAtom (() => {
const password = passwordAtom . get ()
return password . length >= 8
})
const isFormValidAtom = createAtom (() => {
return isEmailValidAtom . get () && isPasswordValidAtom . get ()
})
// Subscribe to form validity
isFormValidAtom . subscribe (( isValid ) => {
submitButton . disabled = ! isValid
})
Filtering and Sorting
interface User {
id : number
name : string
age : number
active : boolean
}
const usersAtom = createAtom < User []>([])
const searchQueryAtom = createAtom ( '' )
const sortByAtom = createAtom < 'name' | 'age' >( 'name' )
const showActiveOnlyAtom = createAtom ( false )
const filteredUsersAtom = createAtom (() => {
let users = usersAtom . get ()
const query = searchQueryAtom . get (). toLowerCase ()
const sortBy = sortByAtom . get ()
const activeOnly = showActiveOnlyAtom . get ()
// Filter by search
if ( query ) {
users = users . filter (( u ) => u . name . toLowerCase (). includes ( query ))
}
// Filter by active status
if ( activeOnly ) {
users = users . filter (( u ) => u . active )
}
// Sort
users = [ ... users ]. sort (( a , b ) => {
if ( sortBy === 'name' ) {
return a . name . localeCompare ( b . name )
}
return a . age - b . age
})
return users
})
How Atoms Work Internally
Atoms use a reactive graph system with automatic dependency tracking:
Dependency Tracking : When you call get() inside a computed atom or subscription, the atom automatically tracks that dependency
Change Propagation : When a mutable atom changes via set(), it notifies all dependent atoms and subscriptions
Dirty Checking : Computed atoms mark themselves as “dirty” and only recompute when accessed
Efficient Updates : Only atoms that actually changed will trigger subscriber notifications
From atom.ts:151-156:
get (): T {
if ( activeSub !== undefined ) {
link ( atom , activeSub , cycle )
}
return atom . _snapshot
}
Atoms vs Stores
Atoms
Lower-level primitive
Custom comparison logic
More explicit API
Better for libraries
Direct control over reactivity
Stores
Higher-level abstraction
Simpler API
Better for applications
Built on top of atoms
Convenient state property
Stores High-level API built on atoms
Derived Stores Create computed state from multiple sources
Subscriptions React to atom changes
Batching Batch multiple atom updates