Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Effects are reactive side effects that automatically track their dependencies and clean themselves up when dependencies change or when the effect is aborted. They’re designed for running side effects in response to state changes. Effects:
  • Automatically track atom dependencies
  • Re-run when dependencies change
  • Support async operations with abort handling
  • Clean up automatically on context abort
  • Return an unsubscribe function for manual cleanup

Creating Effects

Basic Usage

import { atom, effect } from '@reatom/core'

const count = atom(0, 'count')

// Create an effect that runs when count changes
effect(() => {
  console.log('Count is:', count())
}, 'logCount')
// Immediately logs: "Count is: 0"

count.set(5)
// Logs: "Count is: 5"

Type Signature

interface Effect<State> extends Computed<State> {
  unsubscribe: Unsubscribe
}

function effect<T>(
  cb: () => T,
  name?: string
): Effect<T>

Automatic Dependency Tracking

Effects automatically track any atoms read during execution:
const firstName = atom('John', 'firstName')
const lastName = atom('Doe', 'lastName')

// Automatically depends on both firstName and lastName
effect(() => {
  console.log(`Full name: ${firstName()} ${lastName()}`)
}, 'logFullName')
// Logs: "Full name: John Doe"

firstName.set('Jane')
// Logs: "Full name: Jane Doe"

lastName.set('Smith')
// Logs: "Full name: Jane Smith"
Effects automatically subscribe and start running immediately upon creation.

Async Effects

Effects work seamlessly with async operations:
import { atom, effect, wrap } from '@reatom/core'

const userId = atom('user123', 'userId')
const userData = atom(null, 'userData')

// Fetch user data whenever userId changes
effect(async () => {
  const id = userId()
  
  console.log('Fetching user:', id)
  
  try {
    const response = await wrap(fetch(`/api/users/${id}`))
    const data = await wrap(response.json())
    userData.set(data)
  } catch (error) {
    if (!isAbort(error)) {
      console.error('Failed to fetch user:', error)
    }
  }
}, 'fetchUserEffect')

// When userId changes, previous fetch is aborted
userId.set('user456')
Use wrap() with promises to make them abortable. When the effect re-runs or is cancelled, wrapped promises are automatically aborted.

Polling Pattern

Effects are perfect for polling data:
import { atom, effect, wrap, sleep } from '@reatom/core'

const isPollingActive = atom(true, 'isPollingActive')
const data = atom(0, 'data')

effect(async () => {
  if (!isPollingActive()) return
  
  console.log('Starting polling...')
  
  while (true) {
    try {
      // Fetch data
      const response = await wrap(fetch('/api/data'))
      const newData = await wrap(response.json())
      data.set(newData.value)
      
      // Wait 5 seconds
      await wrap(sleep(5000))
    } catch (error) {
      if (isAbort(error)) {
        console.log('Polling stopped')
        break
      }
      throw error
    }
  }
}, 'pollingEffect')

// Stop polling by changing the atom
isPollingActive.set(false)

Conditional Effects

Effects can run conditionally based on state:
const isEnabled = atom(true, 'isEnabled')
const value = atom(0, 'value')

effect(() => {
  // Early return if disabled
  if (!isEnabled()) return
  
  console.log('Value is:', value())
}, 'conditionalEffect')

Manual Cleanup

Effects return an unsubscribe function for manual cleanup:
const count = atom(0, 'count')

const counterEffect = effect(() => {
  console.log('Count:', count())
}, 'counterEffect')

// Effect is running...
count.set(1) // Logs: "Count: 1"
count.set(2) // Logs: "Count: 2"

// Stop the effect
counterEffect.unsubscribe()

count.set(3) // No log - effect is stopped

Effect Lifecycle

Effects automatically handle cleanup when context is aborted:
import { atom, effect, wrap, context } from '@reatom/core'

const active = atom(true, 'active')

// Create a new context
const ctx = context.start()

ctx.run(() => {
  effect(async () => {
    if (!active()) return
    
    console.log('Effect started')
    
    try {
      while (true) {
        await wrap(sleep(1000))
        console.log('Tick')
      }
    } catch (error) {
      if (isAbort(error)) {
        console.log('Effect cleaned up')
      }
    }
  }, 'tickEffect')
})

// Reset context - automatically cleans up all effects
context.reset()
// Logs: "Effect cleaned up"

Common Patterns

Local Storage Sync

const settings = atom(
  JSON.parse(localStorage.getItem('settings') || '{}'),
  'settings'
)

// Sync to localStorage on changes
effect(() => {
  const value = settings()
  localStorage.setItem('settings', JSON.stringify(value))
}, 'syncSettings')

Document Title

const pageTitle = atom('Home', 'pageTitle')
const unreadCount = atom(0, 'unreadCount')

effect(() => {
  const title = pageTitle()
  const unread = unreadCount()
  
  document.title = unread > 0 
    ? `(${unread}) ${title}` 
    : title
}, 'updateDocTitle')

WebSocket Connection

const isConnected = atom(false, 'isConnected')
const messages = atom<string[]>([], 'messages')

effect(() => {
  if (!isConnected()) return
  
  const ws = new WebSocket('wss://example.com')
  
  ws.onmessage = (event) => {
    messages.set(prev => [...prev, event.data])
  }
  
  ws.onopen = () => {
    console.log('Connected')
  }
  
  // Cleanup on disconnect
  return () => {
    ws.close()
    console.log('Disconnected')
  }
}, 'websocketEffect')

Scroll to Top

const currentRoute = atom('/home', 'currentRoute')

effect(() => {
  // Scroll to top whenever route changes
  const route = currentRoute()
  window.scrollTo(0, 0)
  console.log('Navigated to:', route)
}, 'scrollToTop')

Auto-save

import { atom, effect, wrap, sleep } from '@reatom/core'

const content = atom('', 'content')
const lastSaved = atom<Date | null>(null, 'lastSaved')

effect(async () => {
  const text = content()
  
  // Don't save empty content
  if (!text) return
  
  // Debounce - wait 2 seconds
  await wrap(sleep(2000))
  
  // Save
  console.log('Auto-saving...')
  await wrap(fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify({ content: text })
  }))
  
  lastSaved.set(new Date())
  console.log('Saved at:', lastSaved())
}, 'autoSave')

Effects vs Actions

FeatureEffectAction
Auto-subscribesYesNo
Accepts parametersNoYes
Auto-tracks dependenciesYesNo
Returns valueNo (returns subscription)Yes
Use caseReactive side effectsImperative operations

Effects vs Computed

FeatureEffectComputed
Tracks dependenciesYesYes
Can have side effectsYesNo (should be pure)
Return value usedNoYes
Runs immediatelyYesOnly when read
Use caseSide effectsDerived state

Abort Handling

Effects integrate with Reatom’s abort system:
import { atom, effect, wrap, isAbort } from '@reatom/core'

const query = atom('', 'query')
const results = atom([], 'results')

effect(async () => {
  const q = query()
  
  if (!q) {
    results.set([])
    return
  }
  
  try {
    // This will be aborted if query changes
    const response = await wrap(fetch(`/api/search?q=${q}`))
    const data = await wrap(response.json())
    results.set(data)
  } catch (error) {
    // Check if it's an abort (expected) or real error
    if (isAbort(error)) {
      console.log('Search aborted (query changed)')
    } else {
      console.error('Search failed:', error)
    }
  }
}, 'searchEffect')

// Typing triggers multiple searches, but only the last one completes
query.set('r')
query.set('re')
query.set('rea')
query.set('reat')
query.set('reato')
query.set('reatom') // Only this search completes

Best Practices

Always wrap promises to enable automatic cancellation:
// Good - abortable
effect(async () => {
  const response = await wrap(fetch('/api/data'))
})

// Avoid - not abortable
effect(async () => {
  const response = await fetch('/api/data')
})
Check for abort errors to distinguish from real errors:
effect(async () => {
  try {
    await wrap(someAsyncOperation())
  } catch (error) {
    if (isAbort(error)) {
      // Expected - dependency changed
      return
    }
    // Real error - handle it
    handleError(error)
  }
})
Early returns make conditional logic clearer:
// Good
effect(() => {
  if (!isEnabled()) return
  doSomething()
})

// Less clear
effect(() => {
  if (isEnabled()) {
    doSomething()
  }
})

Atom

Store mutable state

Computed

Derive state without side effects

Actions

Imperative operations with parameters

Extend

Customize effect behavior