What are Subscriptions?
Subscriptions allow you to react to changes in stores and atoms. When you subscribe to a store or atom, your callback function is called whenever the value changes, enabling you to synchronize external systems, update UI, trigger side effects, and more.
Basic Subscriptions
Subscribe to a Store
import { createStore } from '@tanstack/store'
const countStore = createStore ( 0 )
const subscription = countStore . subscribe (( value ) => {
console . log ( 'Count changed to:' , value )
})
// Update the store
countStore . setState (() => 1 ) // Logs: "Count changed to: 1"
countStore . setState (( prev ) => prev + 1 ) // Logs: "Count changed to: 2"
// Clean up when done
subscription . unsubscribe ()
Subscribe to an Atom
import { createAtom } from '@tanstack/store'
const countAtom = createAtom ( 0 )
const subscription = countAtom . subscribe (( value ) => {
console . log ( 'Count changed to:' , value )
})
countAtom . set ( 1 ) // Logs: "Count changed to: 1"
countAtom . set (( prev ) => prev + 1 ) // Logs: "Count changed to: 2"
subscription . unsubscribe ()
Subscription API
Function Signature
// Simple callback
function subscribe ( callback : ( value : T ) => void ) : Subscription
// Observer object
function subscribe ( observer : Observer < T >) : Subscription
interface Observer < T > {
next ?: ( value : T ) => void
error ?: ( err : unknown ) => void
complete ?: () => void
}
interface Subscription {
unsubscribe : () => void
}
Using an Observer Object
For more control, you can pass an observer object with next, error, and complete handlers:
const subscription = store . subscribe ({
next : ( value ) => {
console . log ( 'New value:' , value )
},
error : ( err ) => {
console . error ( 'Error:' , err )
},
complete : () => {
console . log ( 'Subscription complete' )
},
})
The error and complete callbacks are currently not used by the core library but are provided for RxJS interoperability.
When Subscriptions Fire
Subscriptions fire when:
The store/atom value changes (passes comparison check)
For derived stores/atoms, when any dependency changes and the computed value is different
Subscription callbacks are not called immediately upon subscription. Only subsequent changes trigger the callback.
const store = createStore ( 42 )
store . subscribe (( value ) => {
console . log ( 'Value:' , value ) // Not called immediately
})
store . setState (() => 100 ) // Logs: "Value: 100"
Unsubscribing
Always unsubscribe when you’re done to prevent memory leaks:
const subscription = store . subscribe (( value ) => {
console . log ( value )
})
// Later, when you're done
subscription . unsubscribe ()
Automatic Cleanup Pattern
function setupComponent () {
const subscription = store . subscribe (( value ) => {
updateUI ( value )
})
// Return cleanup function
return () => {
subscription . unsubscribe ()
}
}
// Usage
const cleanup = setupComponent ()
// When component unmounts
cleanup ()
Multiple Subscriptions
You can have multiple subscriptions to the same store or atom:
const store = createStore ( 0 )
const sub1 = store . subscribe (( value ) => {
console . log ( 'Subscriber 1:' , value )
})
const sub2 = store . subscribe (( value ) => {
console . log ( 'Subscriber 2:' , value )
})
store . setState (() => 1 )
// Logs:
// "Subscriber 1: 1"
// "Subscriber 2: 1"
// Unsubscribe independently
sub1 . unsubscribe ()
store . setState (() => 2 )
// Logs:
// "Subscriber 2: 2"
// (sub1 no longer called)
Subscribing to Derived Stores
You can subscribe to derived stores just like regular stores:
const countStore = createStore ( 0 )
const doubledStore = createStore (() => countStore . state * 2 )
doubledStore . subscribe (( value ) => {
console . log ( 'Doubled value:' , value )
})
countStore . setState (() => 5 ) // Logs: "Doubled value: 10"
countStore . setState (() => 7 ) // Logs: "Doubled value: 14"
Practical Examples
Sync to LocalStorage
interface UserSettings {
theme : 'light' | 'dark'
language : string
notifications : boolean
}
const settingsStore = createStore < UserSettings >({
theme: 'light' ,
language: 'en' ,
notifications: true ,
})
// Subscribe to persist changes
settingsStore . subscribe (( settings ) => {
localStorage . setItem ( 'user-settings' , JSON . stringify ( settings ))
})
// Update settings
settingsStore . setState (( prev ) => ({
... prev ,
theme: 'dark' ,
})) // Automatically saved to localStorage
Update Document Title
const unreadCountStore = createStore ( 0 )
const pageTitleStore = createStore ( 'My App' )
// Update document title when either changes
const titleStore = createStore (() => {
const count = unreadCountStore . state
const title = pageTitleStore . state
return count > 0 ? `( ${ count } ) ${ title } ` : title
})
titleStore . subscribe (( title ) => {
document . title = title
})
unreadCountStore . setState (() => 5 ) // Title becomes "(5) My App"
Log State Changes
function createLoggedStore < T >( initialValue : T , name : string ) {
const store = createStore ( initialValue )
store . subscribe (( value ) => {
console . log ( `[ ${ name } ] changed:` , value )
})
return store
}
const userStore = createLoggedStore ({ name: 'Alice' }, 'UserStore' )
userStore . setState (() => ({ name: 'Bob' }))
// Logs: "[UserStore] changed: { name: 'Bob' }"
Debounced Subscription
function debounce < T >( fn : ( value : T ) => void , delay : number ) {
let timeoutId : number | undefined
return ( value : T ) => {
clearTimeout ( timeoutId )
timeoutId = setTimeout (() => fn ( value ), delay )
}
}
const searchQueryStore = createStore ( '' )
// Debounced search - only executes after user stops typing
const debouncedSearch = debounce (( query : string ) => {
console . log ( 'Searching for:' , query )
performSearch ( query )
}, 500 )
searchQueryStore . subscribe ( debouncedSearch )
React to Specific Changes
interface Product {
id : string
name : string
price : number
inStock : boolean
}
const productStore = createStore < Product >({
id: '1' ,
name: 'Widget' ,
price: 10 ,
inStock: true ,
})
// Only react to price changes
let previousPrice = productStore . state . price
productStore . subscribe (( product ) => {
if ( product . price !== previousPrice ) {
console . log ( 'Price changed from' , previousPrice , 'to' , product . price )
previousPrice = product . price
}
})
Analytics Tracking
const pageViewStore = createStore ( 0 )
const userActionsStore = createStore < Array <{ type : string ; timestamp : number }>>([])
// Track page views
pageViewStore . subscribe (( count ) => {
analytics . track ( 'page_view' , { count })
})
// Track user actions
userActionsStore . subscribe (( actions ) => {
const latestAction = actions [ actions . length - 1 ]
if ( latestAction ) {
analytics . track ( 'user_action' , {
type: latestAction . type ,
timestamp: latestAction . timestamp ,
})
}
})
Sync Multiple Stores
const celsiusStore = createStore ( 0 )
const fahrenheitStore = createStore ( 32 )
let updating = false
// Sync Celsius to Fahrenheit
celsiusStore . subscribe (( celsius ) => {
if ( ! updating ) {
updating = true
fahrenheitStore . setState (() => celsius * 9 / 5 + 32 )
updating = false
}
})
// Sync Fahrenheit to Celsius
fahrenheitStore . subscribe (( fahrenheit ) => {
if ( ! updating ) {
updating = true
celsiusStore . setState (() => ( fahrenheit - 32 ) * 5 / 9 )
updating = false
}
})
const emailStore = createStore ( '' )
const emailErrorStore = createStore ( '' )
const isEmailValidStore = createStore (() => {
const email = emailStore . state
return / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / . test ( email )
})
// Update error message based on validation
isEmailValidStore . subscribe (( isValid ) => {
const email = emailStore . state
if ( email && ! isValid ) {
emailErrorStore . setState (() => 'Please enter a valid email' )
} else {
emailErrorStore . setState (() => '' )
}
})
// Also clear error when email changes
emailStore . subscribe (() => {
emailErrorStore . setState (() => '' )
})
Subscriptions vs Derived Stores
When should you use a subscription versus a derived store?
Use Subscriptions For
Side effects (API calls, logging)
Syncing to external systems
Imperative updates (DOM, localStorage)
One-way data flow triggers
Use Derived Stores For
Pure computations
Derived state
Data transformations
Reactive data flow
// Use derived store for pure computation
const doubledStore = createStore (() => countStore . state * 2 )
// Use subscription for side effects
countStore . subscribe (( count ) => {
console . log ( 'Count changed:' , count )
sendAnalytics ({ event: 'count_changed' , value: count })
})
Best Practices
Preventing memory leaks is critical. Always call unsubscribe() when you’re done: // Good
const cleanup = () => {
subscription . unsubscribe ()
}
// In React
useEffect (() => {
const sub = store . subscribe ( updateUI )
return () => sub . unsubscribe ()
}, [])
Don’t update the same store inside its own subscription: // Bad - infinite loop
store . subscribe (( value ) => {
store . setState (() => value + 1 ) // DON'T DO THIS
})
// Good - use derived stores instead
const incrementedStore = createStore (() => store . state + 1 )
Use Guards for Conditional Updates
If you must update a store in a subscription, use guards: let isUpdating = false
store . subscribe (( value ) => {
if ( ! isUpdating && someCondition ) {
isUpdating = true
otherStore . setState (() => transform ( value ))
isUpdating = false
}
})
Subscription callbacks should be fast. For expensive operations, debounce or use workers: // Good - debounced
const debouncedUpdate = debounce ( expensiveOperation , 300 )
store . subscribe ( debouncedUpdate )
// Good - async
store . subscribe ( async ( value ) => {
await performAsyncOperation ( value )
})
Stores Learn about the store primitive
Atoms Lower-level reactive primitives
Derived Stores Create computed state
Batching Batch updates to reduce subscription calls