Skip to content

Methods

ReatomAbortController

Version of abort controller with explicit name for better debugging.

May control variable propagation by setting the spawned flag (used internally to handle abort boundaries).

abortVar

Global abort variable that precess AbortController’s coupled to the current frame stack.

The abortVar is computed from all other abort atoms in the current frame tree, which allows for propagation of abortion signals through the computation hierarchy. This is a critical component for cancellation handling in Reatom’s async operations.

Example 1

// Trigger abortion of the current frame
abortVar.abortCurrent('Operation cancelled')

Example 2

// Trigger abortion of the current frame stack
abortVar.get()?.abort('Operation cancelled')

Example 3

// Check if current operation (the whole frame stack) is aborted
abortVar.throwIfAborted()
// continue operation...

Example 4

// Get AbortController for fetch API
const { controller, unsubscribe } = abortVar.subscribe()
await fetch('/api/data', { signal: controller.signal })
unsubscribe()

race

Races multiple controlled promises and automatically aborts all losers when one settles.

Unlike Promise.race, this function ensures proper cleanup by aborting all remaining promises once a winner is determined. This prevents resource leaks from ongoing async operations that are no longer needed.

Example

const resource = computed(async () => {
return race(
abortVar.createAndRun(() => fetchFromCache()),
abortVar.createAndRun(() => fetchFromNetwork()),
)
})

log

A special logging action for debugging Reatom applications.

log provides an enhanced logging experience with automatic tracing and production-safe output. It forwards all arguments to the native console.log while providing additional context about the call stack and dependencies.

Key Benefits

  • Short and handy name - Easy to type and use throughout your codebase

  • Automatic stack tracing - Shows the relative call stack each time it’s

called

  • Use it everywhere - Logs are only visible when connectLogger() is

active

  • Production-safe - Logs won’t appear in production builds when logger is

not connected

  • Context-aware - Integrates with Reatom’s dependency tracking system

  • Extendable - You can extend it with other extensions to add custom

behavior

Example 1

import { log } from '@reatom/core'
// Make LOG available globally (recommended)
declare global {
var LOG: typeof log
}
globalThis.LOG = log

Example 2

const fetchSome = action(() => {
const payload = a() + b() + c() // ...
// Log intermediate data
LOG('Call api with:', payload) // the log will display that it in the 'fetchSome' action
return api(payload)
}, 'fetchSome')

Example 3

// Multiple arguments like console.log
LOG('Debug info:', { foo: 'bar' }, [1, 2, 3])

Example 4

export const Input = props => {
// Use anywhere in your code
LOG('Render Input', props)
return <input {...props} />
}

Example 5

// Log state changes only when data changes
// This will return the data and log it only if it has changed
const data = LOG.state('data', useSomeData())

Example 6

// Extend LOG with custom behavior using withCallHook
import { withCallHook } from '@reatom/core'
LOG.extend(
withCallHook((payload, params) => {
// Send logs to a remote service
sendToAnalytics({ level: 'debug', args: params })
}),
)

connectLogger

Sets up and connects a logger to the Reatom system for debugging and tracing.

This function enhances all non-private atoms and actions with logging capabilities. When an atom’s value changes or an action is called, it logs the event with relevant information to the console including:

  • Previous and current state for atoms

  • Parameters and return values for actions

  • Complete dependency stack traces

  • Error information when exceptions occur

The logger adapts to the environment, using different formatting for browser and Node.js. Private atoms (those with names starting with _ or containing ._ ) are not logged.

Example 1

connectLogger()

Example 2

connectLogger({ match: (name) => name.startsWith('myFeature.') })

Example 3

// Highlight specific atoms with custom colors
connectLogger({
match: (name) => (name.includes('important') ? '#ff6b6b' : true),
})

deatomize

Recursively unwraps atoms in a value to get their current states

This function deeply traverses a value, including nested objects, arrays, maps, and sets, replacing atoms with their current state values. It’s useful for serialization, debugging, or creating snapshots of state that don’t contain reactive references.

Example

const user = {
id: 42,
name: atom('John', 'userName'),
stats: {
score: atom(100, 'userScore'),
badges: atom(['gold', 'silver'], 'userBadges'),
},
}
// Results in: { id: 42, name: 'John', stats: { score: 100, badges: ['gold', 'silver'] }}
const plainUser = deatomize(user)

effect

Creates a reactive side effect that automatically tracks dependencies and cleans itself up.

effect is similar to computed but designed for running side effects. It automatically subscribes to any atoms read within the callback (cb). When the effect’s reactive context is aborted (e.g., component unmount in reatomFactoryComponent, cancellation in withAbort / withAsyncData), the effect’s execution is stopped, and any ongoing async operations within it (like await wrap(sleep(...))) are cancelled.

Example

import { atom, effect, wrap, sleep, isAbort } from '@reatom/core'
const isActive = atom(true, 'isActive')
const data = atom(0, 'data')
// This effect polls data every 5 seconds while isActive is true
const polling = effect(async () => {
if (!isActive()) return // Depends on isActive
console.log('Polling started...')
while (true) {
const fetchedData = await wrap(fetch('/api/poll'))
const jsonData = await wrap(fetchedData.json())
data(jsonData.value)
await wrap(sleep(5000)) // Abortable sleep == debounce
}
}, 'pollingEffect')
// To manually stop:
// polling.unsubscribe()

framePromise

Request the result of the current atom or action function as a promise.

Returns a promise that resolves to the current execution frame’s state (action payload or atom state). Use it to catch errors from subsequent operations in a cleaner way than traditional try-catch. Use finally to clean up resources and so on. This method respects wrap and abortVar policies.

Inspired by TC39 explicit resource management proposal, but simpler and coupled to Reatom’s async stack for an ergonomic usage.

Example 1

export const processPayment = action(async (orderId: string) => {
framePromise().catch((error) => showErrorNotification(error))
let order = await wrap(fetchOrder(orderId))
await wrap(validateInventory(order))
await wrap(chargeCustomer(order))
await wrap(updateOrderStatus(order, 'completed'))
return order
})

Example 2

// Classic approach - boilerplate with try-catch
export const doSome = action(async () => {
try {
await wrap(fetchUser())
await wrap(updateProfile())
await wrap(syncData())
} catch (error) {
toast(error)
}
})
// native using - no try-catch and no "finally" logic
// only resource management with extra variables
// you can adapt ` toast `, but without the payload / error data
export const doSome = action(async () => {
using _ = toast
await wrap(fetchUser())
await wrap(updateProfile())
await wrap(syncData())
})
// With our framePromise() - clean and declarative
export const doSome = action(async () => {
framePromise().catch((error) => toast(error))
await wrap(fetchUser())
await wrap(updateProfile())
await wrap(syncData())
})

Example 3

// Native using works only within the current function scope=*
export const processOrder = action(async (orderId: string) => {
withErrorLogging() // Impossible with native using
await wrap(fetchOrder(orderId))
})
// But framePromise works with the current action/atom frame!
// Helper functions can use parent's framePromise - powerful composition!
let withErrorLogging = () => {
framePromise().catch((error) => logger.error(error))
}
export const processOrder = action(async (orderId: string) => {
withErrorLogging() // Helper uses the SAME action frame!
await wrap(fetchOrder(orderId))
})

ifChanged

Executes a callback when an atom’s state changes

This utility evaluates if an atom’s state has changed during the current frame execution and calls the provided callback with the new state (and optionally the previous state if available).

Example

// Log when the user's name changes
ifChanged(userName, (newName, oldName) => {
console.log(` Name changed from ${oldName} to ${newName}`)
})

getCalls

Retrieves new action calls that occurred in the current batch.

This utility function tracks action invocations and returns an array of new calls that have been made during the current batch. It’s particularly useful for monitoring action activity within computed atoms or effects without triggering side effects during the action execution itself.

In a computed atom, the function compares the current action state with the previous frame’s state to determine which calls are new. If this is the first time the action is being tracked, all current calls are considered new. Otherwise, only calls that weren’t present in the previous frame are returned. If the computed triggered by some other dependent atom change, the function may return an empty array. The past calls are not stored!

Example

// Monitor API calls in an effect
const apiCall = action((endpoint: string) => fetch(endpoint), 'apiCall')
effect(() => {
const newCalls = getCalls(apiCall)
newCalls.forEach(({ payload, params }) => {
console.log(` API called: ${params[0]}, Response:`, payload)
})
}, 'apiMonitor')

isCausedBy

Determines if an atom is part of the causal chain leading to the current computation

This recursive function checks if the given atom has caused the current computation by traversing the computation tree. It’s useful for determining dependencies and understanding the flow of state changes through your application.

Example

// Check if user atom changes caused the current computation
if (isCausedBy(userAtom)) {
console.log('This computation was triggered by user state change')
}

memoKey

Internal utility for keyed memoization within an atom’s execution context.

Caches values by key within the current atom frame, creating the value on first access and returning the cached value on subsequent calls. This enables persistent memoization across multiple invocations of the same atom.

The cache is scoped per-atom and persists across all calls to that atom within the same context, making it suitable for creating internal computed atoms or other resources that should be created once and reused.

Example

// TODO cache

memo

Memoize additional computation inside a different calls of an atom (computed or an effect) or an action.

It’s useful when you want to avoid recomputing of the whole computed function, especially if the computation is expensive. You could create an external atom by yourself, but it is not handy sometimes.

The memo function takes a callback function cb that returns the value to be memoized, an optional equal function that compares the new and old values to determine if the memoized value should be updated, and an optional key to uniquely identify the memoized value.

Important note*: The created internal atom only uses the first passed callback function. This means it’s unsafe to rely on data from the closure that changes on every recall, as subsequent calls will not update the callback used by the internal atom.

Note for rare cases: A created underhood atom is memorized for each memo by the passed function sources from “toString()” method, so every computed callback in different memos of the same atom should contain different code. However, you can provide a custom key parameter to uniquely identify different memo calls instead of relying on toString().

When a custom key is provided, the toString() duplication check is bypassed, allowing you to use the same callback function multiple times within the same atom by providing different keys for each usage.

Example 1

// This is very useful to memoize not just the end string,
// but, for example, a template computation inside ` reatomComponent ` or so on.
export const listSum = computed(() => {
// Simple call of ` list().length ` will cause extra recomputations for elements sorting or its internal changes.
// correct optimal way, the component will rerender only on ` length ` change
const length = memo(() => list().length)
// you could call different ` memo ` many times in one computed
const sum = memo(() => list().reduce((acc, el) => acc + el().value, 0))
return ` The sum of ${length} elements is: ${sum}`
}, 'listSum')

Example 2

// An example of using the equality function as part of the logic
const scroll = atom(0, 'scroll')
const throttledScroll = computed(() => {
const { state } = memo(
() => ({ state: scroll(), time: Date.now() }),
// Only update if 50ms have passed since the last update
(next, prev) => prev.time + 50 < Date.now(),
)
return state
}, 'throttledScroll')

Example 3

// Using memo in actions for expensive computations
const processData = action((data: string[]) => {
// You can even create a service, but not one that is tied only to this action.
const myService = memo(() => new Service())
myService.send(data)
}, 'processData')

peek

Executes a callback in the current context without reactive bindings (dependencies tracking)

Example 1

// reset paging on search changes
effect(() => {
const searchState = search()
// get page state without subscribing to it!
if (peek(page) > 1) peek(0)
})

Example 2

const query = atom('', 'query')
const someResource = computed(
async () => api.getSome(query()),
'someResource',
).extend(withAsyncData())
const tip = computed(() => {
if (!someResource.ready()) {
return 'Searching...'
}
const list = someResource.data()
if (list.length === 0) {
// no need to subscribe to the query changes!
return peek(query) ? 'Nothing found' : 'Try to search something'
}
return ` Found ${list.length} elements `
})

reatomLens

Creates a lens atom that provides focused access to a nested property within a parent atom’s state.

A lens atom automatically tracks changes in the parent atom and provides immutable updates back to the parent when modified. It supports objects, arrays, and Maps.

Example 1

// With an object
const userAtom = atom({ name: 'John', age: 30 })
const nameAtom = reatomLens(userAtom, 'name')
nameAtom() // → 'John'
nameAtom.set('Jane') // Updates userAtom to { name: 'Jane', age: 30 }

Example 2

// With an array
const listAtom = atom(['a', 'b', 'c'])
const firstAtom = reatomLens(listAtom, 0)
firstAtom() // → 'a'
firstAtom.set('x') // Updates listAtom to ['x', 'b', 'c']

Example 3

// With a Map
const mapAtom = atom(new Map([['key1', 'value1']]))
const valueAtom = reatomLens(mapAtom, 'key1')
valueAtom() // → 'value1'
valueAtom.set('value2') // Updates mapAtom with new Map

Example 4

// With custom get/set functions
const dataAtom = atom({ nested: { deep: { value: 42 } } })
const deepAtom = reatomLens(dataAtom, 'nested', {
get: (parent) => parent.nested?.deep?.value,
set: (parent, _, value) => ({
...parent,
nested: {
...parent.nested,
deep: { ...parent.nested.deep, value },
},
}),
})

withObservable

Extends an existing atom to synchronize with an observable-like data source.

reatomObservable

Creates a Reatom atom from an observable-like data source.

The atom will automatically subscribe to the observable when it gains subscribers and unsubscribe when it loses all subscribers.

Producer fields:

  • initState — static initial value

  • getState — pull current value from the external source

  • next — called when the atom is set, to push values back to the source

  • subscribe — called on connect, receives a callback to push values into the

atom

Function-based producers receive a trigger callback that re-reads getState.

reset

Removes all computed atom dependencies. Useful for resources / effects invalidation.

Note that this method not recall and recompute the atom, it only throws it’s deps. Use retryComputed to reevaluate the computed.

retryComputed

Retries computed atom by resetting its dependencies and re-evaluating the computed function .

schedule

Schedule a callback to execute after all current computations complete.

The callback is added to the specified queue (“effect” by default) and processes alongside subscription callbacks and other side effects. This method respects wrap and abortVar policies.

take

Awaits the next update of an atom or call of an action.

This function returns a Promise that resolves when the specified atom’s state changes or when the specified action is called. This is valuable for orchestrating workflows that depend on future state changes or action calls.

Note: Must be used with wrap() when used in an async context to preserve reactive context.

Awaits the next update of the target AtomLike and maps the result. If the map function executes synchronously without throwing, its result is returned directly. Otherwise, a promise is returned.

Example

// Wait for form validation before proceeding
const submitWhenValid = action(async () => {
while (true) {
const currentData = formData()
const error = validate(currentData)
if (!error) break // Exit loop if valid
formData({ ...currentData, error }) // Show error
// Wait for the next change in formData - need wrap() to preserve context
await wrap(take(formData))
}
// Now formData is valid, proceed with submission...
})

defaultRollback

Default rollback strategy for withRollback extension.

This function determines what state to restore when a rollback occurs. It only restores the beforeState if the currentState hasn’t changed since the transaction started (i.e., currentState === transactionState). If the state was modified after the transaction, it preserves the current state to avoid overwriting unrelated changes.

reatomTransaction

Creates an isolated transaction context with rollback capabilities.

Use this to create feature-specific transaction scopes. For most cases, use the global withRollback and withTransaction exports instead.

Example 1

// Create a custom transaction scope for a specific feature
const formTransaction = reatomTransaction({ name: 'form' })
const formData = atom({ name: '', email: '' }, 'formData').extend(
formTransaction.withRollback(),
)
const submitForm = action(async () => {
// ... form submission logic
}, 'submitForm').extend(withAsync(), formTransaction.withTransaction())

Example 2

// Custom default rollback strategy for all atoms in the scope
const alwaysRestoreTransaction = reatomTransaction({
name: 'alwaysRestore',
defaultRollback: ({ beforeState }) => beforeState, // Always restore
})

transactionVar

Global transaction variable instance.

Variable

Interface for context variables in Reatom

Variables maintain values within the context of a computation tree, allowing for context-aware state similar to React’s Context API but with more granular control and integration with Reatom’s reactive system.

variable

Creates a new context variable with getter and setter functionality

This implementation provides a similar capability to the proposed TC39 AsyncContextVariable, allowing you to maintain values that are specific to a particular execution context. Variables created with this function can be accessed and modified within their frame context.

Also, it can be used as IoC/DI container replacement:

| IoC/DI Concept | Variable Equivalent | | --------------- | ------------------------------- | | Token/Key | variable<T>('name') | | Provider | .run(impl, fn) / .set(impl) | | Inject/Resolve | .require() or .get() | | Container scope | Execution context (frame stack) |

Example 1

// Simple variable with string values
const currentUser = variable<string>('currentUser')
// Set the value
currentUser.set('Alice')
// Get the value
console.log(currentUser.get()) // 'Alice'
// Run code with a different value
currentUser.run('Bob', () => {
console.log(currentUser.get()) // 'Bob'
})
// Advanced variable with custom setter logic
const userRole = variable((role: string, permissions: string[]) => {
return { role, permissions }
}, 'userRole')
userRole.set('admin', ['read', 'write', 'delete'])

Example 2

// Using variable as IoC/DI container replacement
// Step 1: Define service interface (DI contract)
interface Logger {
log: (message: string) => void
}
// Step 2: Create a variable as DI token/key
const loggerVar = variable<Logger>('logger')
// Step 3: Inject dependency in atoms/actions via ` require()`
const fetchData = action(async (url: string) => {
const logger = loggerVar.require()
logger.log(` Fetching ${url}`)
const response = await wrap(fetch(url))
logger.log(` Fetched ${url}`)
return response.json()
}, 'fetchData')
// Step 4: Provide implementation at application entry point
loggerVar.run(console, () => {
fetchData('/api/users') // uses console as logger
})
// In tests, createAndRun a mock implementation
const mockLogger = { log: vi.fn() }
loggerVar.run(mockLogger, () => {
fetchData('/api/users') // uses mock logger
})

Example 3

// Variable with creation function for lazy initialization
// Useful when the dependency needs configuration or is expensive to create
const dbVar = variable(
(connectionString: string) => new Database(connectionString),
'db',
)
// Inside a main process function, use `.set()` instead of `.run()`
// This is handy when you control the whole feature lifecycle
const initApp = action(async () => {
// `.set()` creates and binds the value to the current frame
const db = dbVar.set(process.env.DATABASE_URL)
// All subsequent calls in this frame can access the db
await loadUsers() // uses dbVar.require() internally
await loadPosts() // uses dbVar.require() internally
}, 'initApp')
const loadUsers = action(async () => {
const db = dbVar.require()
return db.query('SELECT * FROM users')
}, 'loadUsers')

wrap

Preserves Reatom’s reactive context across async boundaries or function calls.

This is a CRITICAL function in Reatom that ensures proper context tracking across asynchronous operations like Promises, setTimeout, event handlers, and more. Without proper wrapping, atoms would lose their context after async operations, leading to “Missed context” errors when attempting to update state.

Wrap handles two scenarios:

  1. Function wrapping: Returns a new function that preserves context when called

  2. Promise wrapping: Returns a new promise that preserves context through its

chain

Example

// Wrapping a function (e.g., an event handler)
button.addEventListener(
'click',
wrap(() => {
counter((prev) => prev + 1) // Works, context preserved
}),
)
// Wrapping async operations
action(async () => {
const response = await wrap(fetch('/api/data'))
const data = await wrap(response.json())
results(data) // Works, context preserved
})