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 frameabortVar.abortCurrent('Operation cancelled')Example 2
// Trigger abortion of the current frame stackabortVar.get()?.abort('Operation cancelled')Example 3
// Check if current operation (the whole frame stack) is abortedabortVar.throwIfAborted()// continue operation...Example 4
// Get AbortController for fetch APIconst { 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 = logExample 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.logLOG('Debug info:', { foo: 'bar' }, [1, 2, 3])Example 4
export const Input = props => {// Use anywhere in your codeLOG('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 changedconst data = LOG.state('data', useSomeData())Example 6
// Extend LOG with custom behavior using withCallHookimport { 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 colorsconnectLogger({ 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 trueconst 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-catchexport 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 dataexport const doSome = action(async () => { using _ = toast await wrap(fetchUser()) await wrap(updateProfile()) await wrap(syncData())})
// With our framePromise() - clean and declarativeexport 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 changesifChanged(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 effectconst 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 computationif (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 cachememo
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 logicconst 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 computationsconst 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 changeseffect(() => { 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 objectconst 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 arrayconst 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 Mapconst mapAtom = atom(new Map([['key1', 'value1']]))const valueAtom = reatomLens(mapAtom, 'key1')valueAtom() // → 'value1'valueAtom.set('value2') // Updates mapAtom with new MapExample 4
// With custom get/set functionsconst 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 proceedingconst 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 featureconst 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 scopeconst 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 valuesconst currentUser = variable<string>('currentUser')
// Set the valuecurrentUser.set('Alice')
// Get the valueconsole.log(currentUser.get()) // 'Alice'
// Run code with a different valuecurrentUser.run('Bob', () => { console.log(currentUser.get()) // 'Bob'})
// Advanced variable with custom setter logicconst 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/keyconst 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 pointloggerVar.run(console, () => { fetchData('/api/users') // uses console as logger})
// In tests, createAndRun a mock implementationconst 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 createconst 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 lifecycleconst 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:
-
Function wrapping: Returns a new function that preserves context when called
-
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 operationsaction(async () => { const response = await wrap(fetch('/api/data')) const data = await wrap(response.json()) results(data) // Works, context preserved})